textuwpvisiblerichtextblock

Get Visible Text from RichTextBlock


In a UWP app, I am using a RichTextBlock that gets populated with some content. It has word wrapping enabled and has a max lines set so that regardless of the length of its content, it will only show a certain number of lines of rich text.

I'd like to know if there is a way to figure out what is the visible text?

So if I have:

<RichTextBlock TextWrapping="Wrap" MaxLines="2">
    <RichTextBlock.Blocks>
        <Paragraph>
            <Paragraph.Inlines>
                A bunch of runs go in here with text that are several lines
            </Paragraph.Inlines>
        </Paragraph>
    </RichTextBlock.Blocks>
</RichTextBlock>

I'd like to know how much of the text is actually visible.

I'm trying to detect cases where the text is longer than a set number of lines and append a "... Read More" at the end of the last line (replacing the last 13 chars with "... Read More")


Solution

  • So I wrote some code to get the behavior that I want, but unfortunately this is rather slow and inefficient. So if you're using it in an app that is primarily to show a lot of text that needs to be truncated (like a ListView with a lot of text items) then this would slow down your app perf. I still would like to know if there is a better way to do this.

    Here's my code (which only handles Run and Hyperlink inlines so you'll have to modify to handle other types that you need):

    private static void TrimText_Slow(RichTextBlock rtb)
    {
        var paragraph = rtb?.Blocks?.FirstOrDefault() as Paragraph;
        if (paragraph == null) { return; }
    
        // Ensure RichTextBlock has passed a measure step so that its HasOverflowContent is updated.
        rtb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
        if (rtb.HasOverflowContent == false) { return; }
    
    
        // Start from end and remove all inlines that are not visible
        Inline lastInline = null;
        var idx = paragraph.Inlines.Count - 1;
        while (idx >= 0 && rtb.HasOverflowContent)
        {
            lastInline = paragraph.Inlines[idx];
            paragraph.Inlines.Remove(lastInline);
            idx--;
            // Ensure RichTextBlock has passed a measure step now with an inline removed, so that its HasOverflowContent is updated.
            rtb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
        }
    
        // The last inline could be partially visible. The easiest thing to do here is to always
        // add back the last inline and then remove characters from it until everything is in view.
        if (lastInline != null)
        {
            paragraph.Inlines.Add(lastInline);
        }
    
        // Make room to insert "... Read More"
        DeleteCharactersFromEnd(paragraph.Inlines, 13);
    
        // Insert "... Continue Reading"
        paragraph.Inlines.Add(new Run { Text = "... " });
        paragraph.Inlines.Add(new Run { Text = "Read More", Foreground = new SolidColorBrush(Colors.Blue) });
    
        // Ensure RichTextBlock has passed a measure step now with the new inlines added, so that its HasOverflowContent is updated.
        rtb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
    
        // Keep deleting chars until "... Continue Reading" comes into view
        idx = paragraph.Inlines.Count - 3; // skip the last 2 inlines since they are "..." and "Read More"
        while (idx >= 0 && rtb.HasOverflowContent)
        {
            Run run;
    
            if (paragraph.Inlines[idx] is Hyperlink)
            {
                run = ((Hyperlink)paragraph.Inlines[idx]).Inlines.FirstOrDefault() as Run;
            }
            else
            {
                run = paragraph.Inlines[idx] as Run;
            }
    
            if (string.IsNullOrEmpty(run?.Text))
            {
                paragraph.Inlines.Remove(run);
                idx--;
            }
            else
            {
                run.Text = run.Text.Substring(0, run.Text.Length - 1);
            }
    
            // Ensure RichTextBlock has passed a measure step now with the new inline content updated, so that its HasOverflowContent is updated.
            rtb.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
        }
    }
    
    private static void DeleteCharactersFromEnd(InlineCollection inlines, int numCharsToDelete)
    {
        if (inlines == null || inlines.Count < 1 || numCharsToDelete < 1) { return; }
    
        var idx = inlines.Count - 1;
    
        while (numCharsToDelete > 0)
        {
            Run run;
    
            if (inlines[idx] is Hyperlink)
            {
                run = ((Hyperlink)inlines[idx]).Inlines.FirstOrDefault() as Run;
            }
            else
            {
                run = inlines[idx] as Run;
            }
    
            if (run == null)
            {
                inlines.Remove(inlines[idx]);
                idx--;
            }
            else
            {
                var textLength = run.Text.Length;
                if (textLength <= numCharsToDelete)
                {
                    numCharsToDelete -= textLength;
                    inlines.Remove(inlines[idx]);
                    idx--;
                }
                else
                {
                    run.Text = run.Text.Substring(0, textLength - numCharsToDelete);
                    numCharsToDelete = 0;
                }
            }
        }
    }