What is the quickest way to get the background color of a single character in a RichTextBox? - winforms

Using WinForms, I have a RichTextBox which will hold the contents of a text file whose length will be limited only by what a RichTextBox can manage.
A section within it will be marked using a certain background color. I need to quickly identify where the section begins and ends. I don't trust that the section will be completely unbroken, and the marked area might cover the entire text--so I need to examine the background color of every character.
The obvious way to do this is to select each character in turn and get its SelectionBackColor:
private TextRange? CalcMarkedTextRange() {
var rtb = RichTextBox;
var textLength = rtb.TextLength;
// Store the range of whatever the user currently has selected
var currentSelectionRange = new TextRange( rtb );
rtb.Visible = false; // <--- To prevent slow screen updates
// Find where the first and last color-marked characters are
var markBegins = -1;
var markEnds = -1;
for ( int ix = 0; ix < textLength; ++ix ) {
rtb.Select( ix, 1 );
if ( rtb.SelectionBackColor == SelectedTextBackColor ) {
if ( markBegins == -1 ) {
markBegins = ix;
}
markEnds = ix;
}
}
// Put back user's selection
rtb.Select( currentSelectionRange );
rtb.Visible = true;
// See what we found
if ( markBegins > -1 ) {
// Return a single range encompassing all marked characters
return new TextRange( markBegins, markEnds - markBegins + 1 );
}
return null;
}
In the code above, TextRange is a structure that stores the start and length of a selection, just as you might expect.
If I don't hide the control by messing with its Visible property before scanning the colors, this code is unbelievably slow. You can see it scanning through, even with a relatively small amount of text. My test using about 4000 characters exhibited unacceptably poor performance.
I do hide it, it operates much more quickly, but there is still an ugly flash as the control disappears for a few seconds then comes back with its scroll position slightly off.
There must be a way to know the background color of an individual character without having to select it, though I imagine one would have to call a native Win32 method. But I have had no luck finding one.

Related

Trying to reset the cursor position in a WPF Rich Text Box control

I have a WPF Rich Text Box control where I am replacing the text with new text of the same length and I simply want to get the original caret position before I replace the text, replace the text and then reset the caret position.
I think that the problem is that it uses a TextPointer that is tied to the RichTextBox and is reset when I clear the original text so that when reapply it the position has changed. What I actually want to do is to reset it to either the character position index in the original text or I am happy to deal with Lined and Columns.
I have scoured the internet for an answer to what should be a very simple problem and nothing seems to answer it. What I really want is an X/Y coordinate or anything that will help.
You can use GetOffsetToPosition() and GetPositionAtOffset() to save the relative position of the Caret and to restore it.
In other words, assuming that the RichTextBox initializes like this:
RichTextBox rtb;
int paragraphIndex = -1;
int indexInParagraph;
public MainWindow()
{
InitializeComponent();
rtb = new RichTextBox();
rtb.Document = new FlowDocument();
Paragraph para = new Paragraph(new Run("some text some text some text."));
rtb.Document.Blocks.Add(para);
// sets the caret at a specific (random) position in the paragraph:
rtb.CaretPosition = para.ContentStart.GetPositionAtOffset(5);
this.Content = rtb;
}
Note the three private fields in the class.
You should save the caret's paragraph index and caret index in the paragraph, before you replace the text:
public void SaveCaretState()
{
//enumerate and get the paragraph index
paragraphIndex = -1;
foreach (var p in rtb.Document.Blocks)
{
paragraphIndex++;
if (p == rtb.CaretPosition.Paragraph)
break;
}
//get index relative to the start of the paragraph:
indexInParagraph = rtb.CaretPosition.Paragraph.ElementStart.GetOffsetToPosition(rtb.CaretPosition);
}
and restore it whenever you liked:
public void RestoreCaretState(MouseEventArgs e)
{
// you might need to insure some conditions here (paragraph should exist and ...)
Paragraph para = rtb.Document.Blocks.ElementAt(paragraphIndex) as Paragraph;
rtb.CaretPosition = para.ElementStart.GetPositionAtOffset(indexInParagraph);
}
Please note that its a simple example and there might be other Blocks in RichTextBox.Document. However, the idea and implementation is not that much different.

Haxe, OpenFL: draw TextField on Bitmap in a loop

So, this app I am making for mobile phones with Haxe, and OpenFL.
I have a very long text which I load from a text file, and put it into a very tall TextField. However I want to convert this into a Bitmap, due to performance issues.
Again, however, a very tall Bitmap drawn from a text field just shows blank (maybe too much data?), so I decided to split the bitmap data into "pages" bitmaps, which the user can swipe on screen.
When I add the first "page" to display, it does. But rest of the "pages" just show as a blank image.
Here's my code:
images = new Array();
var contentHeight:Float = 560;
field = new TextField();
var fieldFont = Assets.getFont("fonts/Kreon-Regular.ttf");
var format:TextFormat = new TextFormat(fieldFont.fontName, 26 /*currentZoom*/, 0xffffff);// 0x4F4F4F);
format.align = TextFormatAlign.LEFT;
field.defaultTextFormat = format;
var fieldWidth:Float = 410;
field.embedFonts = true;
field.text = fullText;
field.selectable = false;
field.wordWrap = true;
field.border = false;
field.autoSize = TextFieldAutoSize.LEFT;
field.width = fieldWidth;
//field.x = 0;
//addChild(field);
//loop through lines, if line within reach, increase clip height, else make new bd
var clipY:Float = 0;
var clipHeight:Float = 0;
trace(field.numLines);
var h_:Float = field.getLineMetrics(0).height;
var bd:BitmapData;
var mainBd:BitmapData = new BitmapData(Std.int(field.width), Std.int(field.height), true, 0x00000000);
mainBd.draw(field);
for (i in 0... field.numLines)
{
try {
h_ = field.getLineMetrics(i).height+0.2;
} catch (e:Dynamic) {}
if (clipHeight < contentHeight + h_)//line can be accomodated
{
clipHeight += h_;
}
else { //can't be accomodated, do clipping
bd = new BitmapData(Std.int(field.width), Std.int(clipHeight + 5), true, 0x00000000);
trace("clip: clipY:" + clipY + " height:" + clipHeight);
bd.copyPixels(mainBd, new Rectangle(0, clipY, field.width, clipHeight), new Point(0, clipY));
//bd.draw(field, new Matrix(), new ColorTransform(), BlendMode.NORMAL, new Rectangle(0, clipY, field.width, clipHeight), true);
images.push(new Bitmap(bd, PixelSnapping.AUTO, true));
clipY += clipHeight;
clipHeight = 0;
}
}
addChild(images[1]);
You only add one of your images (addChild(images[1]);) to view.
Also, I'd recommend, you to :
Find the exact problem with displaying it as a single TextField, start with the exact amount of text needed for it to break.
Check if there are some weird Unicode characters in your text, they may just plain break the renderer layouting for example(which should be counted as a bug in renderer, but still the way to fix it is to remove them).
If you can't fix that problem, use multiple textfields instead of bitmaps, since with a large text you have a high chance to exceed memory limits(one page of your text currently takes around 1Mb of memory, which is A LOT.
The way you do this currently wouldn't work in any case since you do the same renderer normally does. If you want to render it to partial bitmaps(and there is a real problem with rendering big texts), you need to divide the text in parts and render each part individually. (Basically the same as using multiple textfields with cacheAsBitmap).
NOTE: Bitmaps shouldn't actually be any faster than TextFields, unless you use a VERY fancy font or a lot of filters. In any case, cacheAsBitmap property should do what you want automagically, without writing all this code. But I'm 99% sure thats not the case and you don't need that.

WPF Richtextbox Application.Find Text spanning Multiple runs

I'm trying to implement the Application.Find command for the WPF richtextbox. Let's say I'm searching for "expert". Sounds easy enough. But due to the nature of wpf, if every other letter in "expert" is bolded, then the richtextbox contains e*x*p*e*r*t* and that means six runs exist. I have a starting textPointer. What I'm trying to figure out is how to get the ending textPointer so that I can create a TextRange that I can use to create the Selection.
In this example, the starting textpointer is in the first run, and the ending textpointer should be in the last run. Is there a simple way to generate a textpointer if you know the run and the offset within the run? I tried generating it using a offset from the first textpointer but that did not work because the offset was not within the first run.
As a relative newbie to the WPF richtextbox, this one has me stumped. I imagine that this problem has already been tackled and solved. I did find one partial solution but it only worked on a single run and does not address the multiple run situation.
The idea is to find the offset of the first character (IndexOf) and then to find the TextPointer at this index (but by counting only text characters).
public TextRange FindTextInRange(TextRange searchRange, string searchText)
{
int offset = searchRange.Text.IndexOf(searchText, StringComparison.OrdinalIgnoreCase);
if (offset < 0)
return null; // Not found
var start = GetTextPositionAtOffset(searchRange.Start, offset);
TextRange result = new TextRange(start, GetTextPositionAtOffset(start, searchText.Length));
return result;
}
TextPointer GetTextPositionAtOffset(TextPointer position, int characterCount)
{
while (position != null)
{
if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
int count = position.GetTextRunLength(LogicalDirection.Forward);
if (characterCount <= count)
{
return position.GetPositionAtOffset(characterCount);
}
characterCount -= count;
}
TextPointer nextContextPosition = position.GetNextContextPosition(LogicalDirection.Forward);
if (nextContextPosition == null)
return position;
position = nextContextPosition;
}
return position;
}
This is how to use the code:
TextRange searchRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
TextRange foundRange = FindTextInRange(searchRange, "expert");
foundRange.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.Red));
I found a more complete solution to be here on this GitHub page.
https://github.com/manasmodak/WpfSearchAndHighlightText
It is able to deal with the \n and \r just fine and didn't have the errors I was dealing with from other solutions.

Silverlight Richtextbox obtain cursor position

I'm trying create a user control using the silverlight 5 richtextbox. I need to be able to insert "inline ui" and can't work out how to get the current cursor position.
I trigger my code like this:
this.GetAbsolutePos(this.richText.Selection.Start);
The guts of this method is here:
private int GetAbsolutePos(TextPointer textPointer)
{
int index = 0;
TextPointer pos = this.richText.ContentStart;
while (pos.CompareTo(textPointer) != 0)
{
if (pos.IsAtInsertionPosition)
{
index++;
}
pos = pos.GetNextInsertionPosition(LogicalDirection.Forward);
}
return index;
}
Given the following text in a richtextbox control....
If the cursor is between 5 & 6 on the first line, then the above function correctly returns 5. But as the cursor is further into the text the position becomes more inaccurate. ie between 5 & 6 on the second line returns 16 and the third line returns 27.
It also becomes more difficult as I'm inserting inline elements at these positions, which then count as a "symbol" and further cause the count to go wrong.
This image shows what is happening when I "insert" the inline ui between 5 & 6 on each line.
Just for completeness here is the Xaml from richtext.Xaml (I've removed all the extra attributes from the Section/Paragraph elements to make it clearer)
<Section>
<Paragraph>
<Run>1234567890</Run>
<LineBreak />
<Run>1234567890</Run>
<LineBreak />
<Run>1234567890</Run>
</Paragraph>
</Section>
Based on the remarks on this page MSDN Silverlight TextPointer Class
Symbol - For TextPointer operations, any of the following is considered to be a symbol:
An opening or closing tag for a TextElement element.
A UIElement element contained within an InlineUIContainer. Note that
a UIElement is always counted as exactly one symbol. Any additional
content or elements contained by the UIElement are not considered
symbols.
Each 16-bit Unicode character inside of a text Run element.
I think I need to "know" what kind of "symbol" I am currently on but cannot figure out how.
Seems like it should be easy, but working with TextPointers seems very unintuitive.
I had an idea to parse the Xaml to find the cursor position but that seems like a real hack.
Any help would be appreciated.
Thanks
In the end we just just manually adjust the index by the number of SubstituteEdits (our representation of the UIElement) that are before the text point. Its still flaky and we have other issues with our control, but it's good enough for the moment.
Cheers
Tim
private int GetAbsolutePos(SubstituteEdit newSub)
{
int index = 0;
TextPointer textPointer = this.richText.Selection.Start;
TextPointer caretWhere = this.richText.ContentStart;
while (true)
{
caretWhere = caretWhere.GetNextInsertionPosition(LogicalDirection.Forward);
if (caretWhere == null || caretWhere.CompareTo(textPointer) == 0)
{
break;
}
index++;
}
foreach (SubstituteEdit sub in this.Substitutes)
{
if (sub.Position < index && sub != newSub)
{
index--;
}
}
return index;
}
I am not sure if this will help, but I ended up letting the user insert where the selection is instead of caret position and it seems to work well. Here is some of my code:
InlineUIContainer MyUI = new InlineUIContainer();
TextBlock tblx = new TextBlock() { Text = addedItem.Title, FontWeight = FontWeights.Bold };
MyUI.Child = tblx;
Paragraph myParagraph = new Paragraph();
myParagraph.Inlines.Add(MyUI);
rtb.Selection.Insert(myParagraph);

Selecting an object on a WPF Canvas?

I have a WPF Canvas with some Ellipse objects on it (displayed as circles). Each circle is from a collection class instance which is actually a custom hole pattern class. Each pattern has a certain number of circles, and each circle then gets added to the canvas using an iteration over the collection using the code below.
So, the canvas is populated with a bunch of circles and each circle belongs to a certain pattern instance. You can see a screenshot here: http://twitpic.com/1f2ci/full
Now I want to add the ability to click on a circle on the canvas, and be able to determine the collection it belongs to, so that I can then do some more work on the selected pattern to which that circle belongs.
public void DrawHoles()
{
// Iterate over each HolePattern in the HolePatterns collection...
foreach (HolePattern HolePattern in HolePatterns)
{
// Now iterate over each Hole in the HoleList of the current HolePattern...
// This code adds the HoleEntity, HoleDecorator, and HoleLabel to the canvas
foreach (Hole Hole in HolePattern.HoleList)
{
Hole.CanvasX = SketchX0 + (Hole.AbsX * _ZoomScale);
Hole.CanvasY = SketchY0 - (Hole.AbsY * _ZoomScale);
canvas1.Children.Add(Hole.HoleEntity);
}
}
}
All FrameworkElements have a Tag property which is of type object that can be used to hold arbitrary information. You could assign the HolePattern to the Tag property and easily use that later to get the associated collection.
i.e.:
...
Hole.HoleEntity.Tag = HolePattern as object;
canvas1.Children.Add(Hole.HoleEntity);
later on in the click event:
event(object sender,....)
{
Ellipse e = sender as Ellipse;
HolePattern hp = e.Tag as HolePattern;
...
}
So you probably already read my reply where I said I had it working. And it does work perfectly, (except that it requires great precision with the mouse), but I want to ask this: is it really smart to add an event handler to EVERY ellipse that gets added to a canvas? Now I don't know what kind of memory bog that could be, or maybe it is a piece of cake for WPF and Windows to handle.
In a practical case, I guess there would be not more that 30-50 holes even on a screen that had multiple patterns, but still; FIFTY event handlers? It just seems scary. And actually, each "Hole" is visually represented by two concentric circles and a text label (see the screenshow here: http://twitpic.com/1f2ci/full ), and I know the user would expect to be able to click on any one of those elements to select a hole. That means an event handler on 3 elements for every hole. Now we could be talking about 100 or more event handlers.
It seems like there should be a solution where you could have just one event handler on the Canvas and read the element reference under the mouse, then work off of that to get the .Tag property of that elment, and so on.
I thought I'd post my final and more refined solution in case it helps anyone else.
void canvas1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
int ClickMargin = 2;// Adjust here as desired. Span is in both directions of selected point.
var ClickMarginPointList = new Collection<Point>();
Point ClickedPoint = e.GetPosition(canvas1);
Point ClickMarginPoint=new Point();
for (int x = -1 * ClickMargin; x <= ClickMargin; x++)
{
for (int y = -1 * ClickMargin; y <= ClickMargin; y++)
{
ClickMarginPoint.X = ClickedPoint.X + x;
ClickMarginPoint.Y = ClickedPoint.Y + y;
ClickMarginPointList.Add(ClickMarginPoint);
}
}
foreach (Point p in ClickMarginPointList)
{
HitTestResult SelectedCanvasItem = System.Windows.Media.VisualTreeHelper.HitTest(canvas1, p);
if (SelectedCanvasItem.VisualHit.GetType().BaseType == typeof(Shape))
{
var SelectedShapeTag = SelectedCanvasItem.VisualHit.GetValue(Shape.TagProperty);
if (SelectedShapeTag!=null && SelectedShapeTag.GetType().BaseType == typeof(Hole))
{
Hole SelectedHole = (Hole)SelectedShapeTag;
SetActivePattern(SelectedHole.ParentPattern);
SelectedHole.ParentPattern.CurrentHole = SelectedHole;
return; //Get out, we're done.
}
}
}
}

Resources