3

I would like to highlight the selected text in a RichTextBlock, but when I click the "btnHighlight" button after selecting the text, the text that is highlighted does not match the selection (Perhaps because of hyperlinks but how can you solve it?). Where am I wrong?

MainPage.xaml:

<Grid>
    <RichTextBlock Name="Rtb" Margin="0,150,0,150" Width="300">
        <Paragraph TextIndent="0">
            <Hyperlink UnderlineStyle="None" CharacterSpacing="0">
                <Run Text="1" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text a" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="2" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text b" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="3" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text c" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="4" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text d" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="5" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text e" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="6" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text f" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="7" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text g" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="8" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text h" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="9" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text i" FontSize="20"/>
            <Hyperlink UnderlineStyle="None">
                <Run Text="10" FontSize="20" FontWeight="Bold"/>
            </Hyperlink>
            <Run Text="Text l" FontSize="20"/>
        </Paragraph>
    </RichTextBlock>
    <Button x:Name="btnHighlight" Click="btnHighlight_Click" Content="Highlight" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
    <Button x:Name="btnRemoveHighlight" Click="btnRemoveHighlight_Click" Content="Remove" HorizontalAlignment="Left" Margin="110,10,0,0" VerticalAlignment="Top"/>
</Grid>

MainPage.xaml.cs:

private void btnHighlight_Click(object sender, RoutedEventArgs e)
{
    int selectionStart = Rtb.SelectionStart.Offset;
    int selectionEnd = Rtb.SelectionEnd.Offset;
    int lenght = selectionEnd - selectionStart;

    TextRange textRange = new TextRange() { StartIndex = selectionStart, Length = lenght };
    TextHighlighter highlighter = new TextHighlighter();
    highlighter.Background = new SolidColorBrush(Colors.Yellow);
    highlighter.Ranges.Add(textRange);
    Rtb.TextHighlighters.Add(highlighter);
}

private void btnRemoveHighlight_Click(object sender, RoutedEventArgs e)
{
    Rtb.TextHighlighters.Clear();
}

Thanks in advance..!

Jimmy
  • 45
  • 5
  • 1
    I can reproduce this issue even if I remove the hyperlinks, if I change the TextRange to TextRange textRange = new TextRange() { StartIndex = 3, Length = 10 }, it works fine. I will report this issue to the related team. Thanks for your reporting. – Amy Peng - MSFT Jul 12 '18 at 02:45
  • The question went stale for half a year, so just in case anyone's having the same problem, first thing I'd verify is if `length` is calculated and used correctly in this code. AFAIR `TextPointer.Offset` may include non-displayed positions, while `TextRange.Length` is measured in unicode characters thus using one to calculate another is not supposed to work correctly. `Rtb.SelectedText.Length` would probably make more sense there instead of `Offset` difference. – DK. Dec 05 '18 at 18:18

1 Answers1

1

The problem here is that the TextPointer points into the rich text structure; it is not simply an index into the plain-text version of the string. The rich text is organized as follows: the RichTextBlock has a collection of Block, each Block is a Paragraph that has a collection of Inline, and each Inline is either a Run (containing text) or a Span (containing a collection of Inline) or a LineBreak (representing a newline) or an InlineUIContainer (representing UI content).

The RichTextBlock is also represented as a sequence which is obtained by an in-order traversal of this tree. We count one unit of offset (it can be thought of as a special character) for the start of each element, then we count the units of offset required for the children or the text content, then we add one more unit of offset for the closure of the element. The "Offset" in the TextPointer is, conveniently, the number of units of offset up to the selected point.

The following shows how to traverse the rich text and pick up the text that precedes the TextPointer. Each element in the tree has ElementStart/ElementEnd indicating it's location, and ContentStart/ContentEnd indicating the location of the content within it. Content, such as text from Runs, that is positioned to the left of the TextPointer.Offset are included in the string. Caveats: not tested with Linebreak or inlineUIContainer; does not deal with right-to-left text; and not particularly efficient.

static class DocumentHelper
{
    static public string TextUpTo(this InlineCollection inlines, TextPointer pointer)
    {
        StringBuilder textUpTo = new StringBuilder();
        foreach (Inline inline in inlines) {
            if (inline.ElementStart.Offset > pointer.Offset) {
                break;
            }
            if (inline is Run run) {
                // Need some more work here to take account of run.FlowDirection and pointer.LogicalDirection.
                textUpTo.Append(run.Text.Substring(0, Math.Max(0, Math.Min(run.Text.Length, pointer.Offset - run.ContentStart.Offset))));
            } else if (inline is Span span) {
                string spanTextUpTo = span.Inlines.TextUpTo(pointer);
                textUpTo.Append(spanTextUpTo);
            } else if (inline is LineBreak lineBreak) {
                textUpTo.Append((pointer.Offset >= lineBreak.ContentEnd.Offset) ? Environment.NewLine : "");
            } else if (inline is InlineUIContainer uiContainer) {
                textUpTo.Append(" "); // empty string replacing the UI content. 
            } else {
                throw new InvalidOperationException($"Unrecognized inline type {inline.GetType().Name}");
            }
        }
        return textUpTo.ToString();
    }

    static public string TextUpTo( this RichTextBlock rtb, TextPointer pointer)
    {
        StringBuilder textUpTo = new StringBuilder();
        foreach (Block block in rtb.Blocks) {
            if (block is Paragraph paragraph) {
                textUpTo.Append(paragraph.Inlines.TextUpTo( pointer)); 
            } else {
                throw new InvalidOperationException($"Unrecognized block type {block.GetType().Name}");
            }
        }
        return textUpTo.ToString();
    }
}

Now … as to the original question, we can do it this way:

    private void BtnHighlight_Click(object sender, RoutedEventArgs e)
    {
        string textUpToStart = this.Rtb.TextUpTo(this.Rtb.SelectionStart);
        string textUpToEnd = this.Rtb.TextUpTo(this.Rtb.SelectionEnd);
        Debug.WriteLine($"Text up to start: '{textUpToStart}'; text up to end: '{textUpToEnd}'");

        TextRange textRange = new TextRange { StartIndex = textUpToStart.Length, Length = (textUpToEnd.Length - textUpToStart.Length) };
        TextHighlighter highlighter = new TextHighlighter() { Ranges = { textRange }, Background = new SolidColorBrush(Colors.Yellow) };
        this.Rtb.TextHighlighters.Add(highlighter);
    }

    private void BtnRemoveHighlight_Click(object sender, RoutedEventArgs e)
    {
        this.Rtb.TextHighlighters.Clear();
    }
sjb-sjb
  • 1,112
  • 6
  • 14