WPF Richtextbox Application.Find Text spanning Multiple runs - wpf

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.

Related

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

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.

Copy list to Flowdocument messes up 1st listitem

I'm writing an extension that allows a user to merge multiple notes into a single note and provides some features like adding periods onto the end of the original notes. I'm writing the code that copies the parts of one flowdocument to another and inserts the periods as it goes.
I am having problems copying lists to the new document. For some reason the first listitem always ends up in the paragraph PREceeding the list instead of in the list.
My code:
foreach (Block b in tempDoc.Blocks)
{
thisBlock++;
if (b is List)
{
pkhCommon.WPF.Helpers.AddBlock(b, mergedDocument);
}
else
{
Paragraph p = b as Paragraph;
foreach (Inline inl in p.Inlines)
{
if (!(inl is LineBreak))
pkhCommon.WPF.Helpers.AddInline(inl, mergedDocument);
}
if (thisElement != lastElement || thisBlock != lastBlock)
if ((bool)cb_AddPeriods.IsChecked)
pkhCommon.WPF.Helpers.AddInline(new Run(". "), mergedDocument);
else
pkhCommon.WPF.Helpers.AddInline(new Run(" "), mergedDocument);
}
}
Below is the function to merge the blocks. The AddIline function works the same way.
public static void AddBlock(Block from, FlowDocument to)
{
if (from != null)
{
using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
{
TextRange range = new TextRange(from.ContentStart, from.ContentEnd);
System.Windows.Markup.XamlWriter.Save(range, stream);
range.Save(stream, DataFormats.XamlPackage);
TextRange textRange2 = new TextRange(to.ContentEnd, to.ContentEnd);
textRange2.Load(stream, DataFormats.XamlPackage);
}
}
}
I can't understand why the flowdocument is deciding to put the listitem into the PREceeding paragraph.
Adding a block to the FlowDocument's block collection should put it at the end.
This works for for me.
Document.Blocks.Add(blockToAdd);
You are doing the save/load to clone the block right? Can you just try adding it this way at the end instead of inserting in the text range?
var blockToAdd = XamlReader.Load(stream) as Block;
Document.Blocks.Add(blockToAdd);
Part of your issue is that after saving, the stream position is at the end of the stream, so there is nothing to Load. There is probably a better way to fix this but I am out of time to help. Setting position to 0 feels wrong. This has no parse exception.
var from = new System.Windows.Documents.List(new ListItem(new Paragraph(new Run("Blah"))));
using (var stream = new MemoryStream())
{
System.Windows.Markup.XamlWriter.Save(from, stream);
stream.Position = 0;
Block b = System.Windows.Markup.XamlReader.Load(stream) as Block;
}

If WPF Richtextbox Highlight word contains hyphen,thin line shown in the word?

I am working in WPF RichTextBox.I have highlighted each word,using the below code.its works fine.But the word contains hyphen means,the highlighted word has some thin lines between the hyphen.
string SelectHighlightWord(RichTextBox rtb, int offset, int length)
{
TextRange fullRange = new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd);
fullRange.ClearAllProperties();
TextPointer startSelect = fullRange.Start.GetPositionAtOffset(offset);
TextPointer endSelect = startSelect.GetPositionAtOffset(length);
TextRange textRange = rtb.Selection;
textRange.Select(startSelect, endSelect);
textRange.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(m_backgroundColor));
textRange.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(m_foregroundColor));
FrameworkContentElement fce = (startSelect.Parent as FrameworkContentElement);
if (fce != null)
{
fce.BringIntoView();
}
return rtb.Selection.Text;
}
Note : I have added images for better understanding.
Is your window setting TextOptions.TextFormattingMode on Ideal? If so, try setting Display.

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);

Highlight special word in a TextBox

How do I highlight all occurrences of a list of words in a text.
For example, i do have a list of strings ("if", "else", "then", "while", "true").
I do need to find them in a TextBox and highlight them (foreground + background color).
Examples as how it should look:
The current approach is to override TextBox and do "something" in the OnTextChange event.
see this question:
How to highlight portion of a Rich TextBox WPF control if there are more than 2 spaces?
and this open source control:
http://wpfsyntax.codeplex.com/
I'm actually using some approaches using the RichTextBox but im making steps slowly.
Realized how i mark things there are still some bugs. For instance, everything gets
marked after the first character to mark. So it looks just like that:
pos is the position of the character i want to mark (+1 for just one character), in OnTextChange
MarkForeground(pos + 2, pos + 2 + 1, Colors.Green); // +2 for some awkward wpf bug probably ;)
private void MarkForeground(int start, int end, Color col)
{
TextPointer startPointer = this.Document.ContentStart.GetPositionAtOffset(start);
TextPointer endPointer = this.Document.ContentStart.GetPositionAtOffset(end);
if (startPointer != null && endPointer != null)
{
TextRange range = new TextRange(startPointer, endPointer);
range.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(col));
}
}

Resources