Select Range of Text in WPF RichTextBox (FlowDocument) Programmatically

33,341

Solution 1

Public Function GoToPoint(ByVal start As TextPointer, ByVal x As Integer) As TextPointer
    Dim out As TextPointer = start
    Dim i As Integer = 0
    Do While i < x
        If out.GetPointerContext(LogicalDirection.Backward) = TextPointerContext.Text Or _
             out.GetPointerContext(LogicalDirection.Backward) = TextPointerContext.None Then
            i += 1
        End If
        If out.GetPositionAtOffset(1, LogicalDirection.Forward) Is Nothing Then
            Return out
        Else
            out = out.GetPositionAtOffset(1, LogicalDirection.Forward)
        End If


    Loop
    Return out
End Function

Try this, this should return a text pointer for the given char offset. (Sorry its in VB, but thats what I am working in...)

Solution 2

Try that :

var textRange = MyRichTextBox.Selection;
var start = MyRichTextBox.Document.ContentStart;
var startPos = start.GetPositionAtOffset(3);
var endPos = start.GetPositionAtOffset(8);
textRange.Select(startPos, endPos);
textRange.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.Blue));
textRange.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold);

Solution 3

I tried using the solution posted by KratzVB but found that it was ignoring newlines. If you want to count \r and \n symbols then this code should work:

private static TextPointer GetPoint(TextPointer start, int x)
{

        var ret = start;
        var i = 0;
        while (ret != null)
        {
            string stringSoFar = new TextRange(ret, ret.GetPositionAtOffset(i, LogicalDirection.Forward)).Text;
            if (stringSoFar.Length == x)
                    break;
            i++;
            if (ret.GetPositionAtOffset(i, LogicalDirection.Forward) == null)
                return ret.GetPositionAtOffset(i-1, LogicalDirection.Forward)

        }
        ret=ret.GetPositionAtOffset(i, LogicalDirection.Forward);
        return ret;
}

Solution 4

Could not find a solution with acceptable performance solution to this problem for a long time. Next sample works in my case with the highest performance. Hope it will help somebody as well.

TextPointer startPos = rtb.Document.ContentStart.GetPositionAtOffset(searchWordIndex, LogicalDirection.Forward);
startPos = startPos.CorrectPosition(searchWord, FindDialog.IsCaseSensitive);
if (startPos != null)
{
    TextPointer endPos = startPos.GetPositionAtOffset(textLength, LogicalDirection.Forward);
    if (endPos != null)
    {
         rtb.Selection.Select(startPos, endPos);
    }
}

public static TextPointer CorrectPosition(this TextPointer position, string word, bool caseSensitive)
{
   TextPointer start = null;
   while (position != null)
   {
       if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
       {
           string textRun = position.GetTextInRun(LogicalDirection.Forward);

           int indexInRun = textRun.IndexOf(word, caseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase);
           if (indexInRun >= 0)
           {
               start = position.GetPositionAtOffset(indexInRun);
               break;
           }
       }

       position = position.GetNextContextPosition(LogicalDirection.Forward);
   }

   return start; 
}

Solution 5

My version based on cave_dweller's version

private static TextPointer GetPositionAtCharOffset(TextPointer start, int numbertOfChars)
{
    var offset = start;
    int i = 0;
    string stringSoFar="";
    while (stringSoFar.Length < numbertOfChars)
    {
        i++;
        TextPointer offsetCandidate = start.GetPositionAtOffset(
                i, LogicalDirection.Forward);

        if (offsetCandidate == null)
            return offset; // ups.. we are to far

        offset = offsetCandidate;
        stringSoFar = new TextRange(start, offset).Text;
    }

    return offset;
}

To omit some characters add this code inside loop:

stringSoFar = stringSoFar.Replace("\r\n", "")
                         .Replace(" ", "")

Instead of this (slow):

var startPos = GetPoint(start, offset);
var endPos = GetPoint(start, offset + length);

You should do this (faster)

var startPos = GetPoint(start, offset);
var endPos = GetPoint(startPos, length);

Or create separate method to get TextRange:

private static TextRange GetTextRange(TextPointer start, int startIndex, int length)
{
    var rangeStart = GetPositionAtCharOffset(start, startIndex);
    var rangeEnd = GetPositionAtCharOffset(rangeStart, length);
    return new TextRange(rangeStart, rangeEnd);
}

You can now format text without Select()ing:

var range = GetTextRange(Document.ContentStart, 3, 8);
range.ApplyPropertyValue(
    TextElement.BackgroundProperty, 
    new SolidColorBrush(Colors.Aquamarine));
Share:
33,341
Johan Danforth
Author by

Johan Danforth

Father, husband, software architect, aikidoka

Updated on July 09, 2022

Comments

  • Johan Danforth
    Johan Danforth almost 2 years

    I have this WPF RichTextBox and I want to programmatically select a given range of letters/words and highlight it. I've tried this, but it doesn't work, probably because I'm not taking into account some hidden FlowDocument tags or similar. For example, I want to select letters 3-8 but 2-6 gets selected):

    var start = MyRichTextBox.Document.ContentStart;
    var startPos = start.GetPositionAtOffset(3);
    var endPos = start.GetPositionAtOffset(8);
    var textRange = new TextRange(startPos,endPos);
    textRange.ApplyPropertyValue(TextElement.ForegroundProperty,
        new SolidColorBrush(Colors.Blue));
    textRange.ApplyPropertyValue(TextElement.FontWeightProperty, 
        FontWeights.Bold);
    

    I've realised RichTextBox handling is a bit trickier than I thought :)

    Update: I got a few answers on the MSDN forums: This thread where "dekurver" seid:

    The offsets you're specifying are not character offsets but symbol offsets. What you need to do is get a TextPointer that you know is adjacent to text, then you can add character offsets.

    And "LesterLobo" said:

    you will need to loop through the paragraphs and inlines to find the Next and then their offsets in a loop to apply for all appearances of the specific text. note that when you edit your text would move but your highlight wouldnt move as its associated with the offset not the text. You could however create a custom run and provide a highlight for it...

    Would still LOVE to see some sample code for this if someone knows their way around FlowDocuments...

    EDIT I got a version of Kratz VB code working, it looks like this:

    private static TextPointer GetPoint(TextPointer start, int x)
    {
        var ret = start;
        var i = 0;
        while (i < x && ret != null)
        {
            if (ret.GetPointerContext(LogicalDirection.Backward) == 
    TextPointerContext.Text ||
                ret.GetPointerContext(LogicalDirection.Backward) == 
    TextPointerContext.None)
                i++;
            if (ret.GetPositionAtOffset(1, 
    LogicalDirection.Forward) == null)
                return ret;
            ret = ret.GetPositionAtOffset(1, 
    LogicalDirection.Forward);
        }
        return ret;
    }
    

    And I use it like this:

    Colorize(item.Offset, item.Text.Length, Colors.Blue);
    
    private void Colorize(int offset, int length, Color color)
    {
        var textRange = MyRichTextBox.Selection;
        var start = MyRichTextBox.Document.ContentStart;
        var startPos = GetPoint(start, offset);
        var endPos = GetPoint(start, offset + length);
    
        textRange.Select(startPos, endPos);
        textRange.ApplyPropertyValue(TextElement.ForegroundProperty, 
    new SolidColorBrush(color));
        textRange.ApplyPropertyValue(TextElement.FontWeightProperty, 
    FontWeights.Bold);
    }
    
  • Johan Danforth
    Johan Danforth over 14 years
    @Tomas not for me I'm afraid. letters 2-6 gets selected/colored for me using that code. I'm going to try something else and get back here.
  • Johan Danforth
    Johan Danforth over 14 years
    Nice! I got a version of that code working, will add it to the question. Cheers.
  • devios1
    devios1 almost 14 years
    This is also handy for counting the characters in a RichTextBox: just do the loop while out is not null and return i at the end.
  • a7madx7
    a7madx7 almost 11 years
    This method gets me every single character that comes after any of my specified tokens i need it to just get a pointer to my word only because when i try to make it bold for example it makes the whole sentence after my token " bold" i just need it to operate on my token!
  • a7madx7
    a7madx7 almost 11 years
    like the visual studio does with its preserved keywords !
  • Nick
    Nick almost 11 years
    This doesn't work for single characters or for two adjacent elements.
  • CoperNick
    CoperNick over 10 years
    Making i+=1 for backward context == None (no character) is wrong. Backward in this case means you are looking to character before start pointer - this is also wrong - you should look forward. This also do not work for line breaks. See @cave_dweller and my solution.
  • BrainSlugs83
    BrainSlugs83 over 10 years
    Right, this will give you a text pointer -- but how do you SELECT it?
  • BrainSlugs83
    BrainSlugs83 over 10 years
    Thanks for that -- I knew I would either have to set .Selection or call .Select(...) -- but having to call .Selection.Select? Completely unexpected.
  • Sameera Kumarasingha
    Sameera Kumarasingha over 8 years
    Yes. this works, i had a problem due MyRichTextBox was out of focus. So if there are any problems, first focus it programmatically, ( MyRichTextBox.focus()).
  • Ed Elliott
    Ed Elliott over 8 years
    this works for me - you need to add a semi-colon after "return ret.GetPositionAtOffset(i-1, LogicalDirection.Forward)" - i tried to edit but the edit is shorter than 6 characters (facepalm)
  • Richard Erickson
    Richard Erickson about 8 years
    Welcome to SO and thank you for posting an answer. Please consider expanding your answer to include an explanation of your code.
  • DanW
    DanW over 7 years
    After the i+=1, I had to add (in C#): "if (i==x) return out;" (of course, I renamed the out variable because "out" is a reserved word)
  • gehbiszumeis
    gehbiszumeis about 5 years
    Please put your answer always in context instead of just pasting code. See here for more details.