TreeDragon
TreeDragon

Reputation: 11

Find and scroll to text in WPF RichTextBox

Struggling to find a clear WPF (not Forms) example of finding cursor position of searched text and then scrolling text into view using a RichEditBox.

DocumentViewer has the desired text search ability but is read only and I require write.

Upvotes: 0

Views: 132

Answers (2)

TreeDragon
TreeDragon

Reputation: 11

Faster response

Answer derived from link...

RichTextBox uses a FlowDocument to represent images, formatting... in addition to text. The catch is TextRange.Text.IndexOf(string, start) returns the index within text, not the FlowDocument's TextPointer position!

To get the TextPointer requires indexing through FlowDocument. But this is very slow. By checking each FlowDocument Block for the searched for string and only indexing through this block significantly improves search time.

    private static bool SearchInRichTextBox(RichTextBox rtb, string searchFor, StringComparison stringComparison)
    {                        
        string searchForComparison = MatchStringComparison(searchFor, stringComparison); // Match searchFor to StringComparison
        TextRange searchRange = new(rtb.Document.ContentStart, rtb.Document.ContentEnd);

        foreach (Block block in rtb.Document.Blocks)
        {
            searchRange.Select(block.ContentStart, block.ContentEnd);                
            
            if (searchRange.Text.Contains(searchForComparison, stringComparison))
            {
                if (FindTextInRange(searchRange, searchForComparison) is TextRange textRange)
                {
                    textRange.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold);
                    textRange.ApplyPropertyValue(TextElement.FontSizeProperty, 20.0);
                    textRange.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Green); // TextElement required for BackgroundProperty.
                    textRange.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Red); // TextElement not required for ForegroundProperty?
                    
                    Rect startCharRect = textRange.Start.GetCharacterRect(LogicalDirection.Forward);
                    // Attempt to scroll searchForComparison into midpoint (rtb.ActualHeight / 2.0) of view
                    rtb.ScrollToVerticalOffset(startCharRect.Top - rtb.ActualHeight / 2.0);
                }
                return true;
            }
        }

        return false;
    }

For StringComparison that include IgnoreCase ensure searched for string is lower case.

private static string MatchStringComparison(string searchFor, StringComparison stringComparison)
    {
        string compare;
        
        switch (stringComparison)
        {
            case StringComparison.Ordinal:
            case StringComparison.CurrentCulture:
            case StringComparison.InvariantCulture:
                compare = searchFor;
                break;
            case StringComparison.OrdinalIgnoreCase:
            case StringComparison.CurrentCultureIgnoreCase:
            case StringComparison.InvariantCultureIgnoreCase:
                compare = searchFor.ToLower();
                break;
            default: throw new ArgumentException("Unknown StringComparison");
        }

        return compare;
    }

Find the TextRange by indexing through the searchRange known to contain the searched for text.

private static TextRange? FindTextInRange(TextRange searchRange, string searchText)
    {
        // The start position of the search
        TextPointer current = searchRange.Start.GetInsertionPosition(LogicalDirection.Forward);

        while (current != null)
        {
            // The TextRange that contains the current character
            TextRange text = new(current.GetPositionAtOffset(0, LogicalDirection.Forward), 
                current.GetPositionAtOffset(1, LogicalDirection.Forward));

            // If the current character is the start of the searched searchFor
            if (text.Text == searchText[0].ToString())
            {
                TextRange match = new(current, current.GetPositionAtOffset(searchText.Length, LogicalDirection.Forward));

                if (match.Text == searchText)
                {
                    // Return the match
                    return match;
                }
            }

            // Move to the next character
            current = current.GetPositionAtOffset(1, LogicalDirection.Forward);
        }

        // Return null if no match found
        return null;
    }        

Html for RichTextBox

<!-- Must place in grid cell for scroll bars to function -->
    <RichTextBox x:Name="rtb"                        
                    HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
                    SpellCheck.IsEnabled="True"/>

Upvotes: 1

CodeconValley
CodeconValley

Reputation: 66

// Function to search a string in a RichTextBox and scroll it into view
public void SearchAndScroll(RichTextBox richTextBox, string searchText)
{
    // Get a TextRange for the entire document
    TextRange textRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);

    // Find the first match
    TextRange searchRange = FindTextInRange(textRange, searchText);

    if (searchRange != null)
    {
        // Select the searched text in the RichTextBox
        richTextBox.Selection.Select(searchRange.Start, searchRange.End);

        // Scroll the searched text into view
        richTextBox.ScrollToVerticalOffset(richTextBox.VerticalOffset + richTextBox.Selection.Start.GetCharacterRect(LogicalDirection.Forward).Top);
        richTextBox.Focus();
    }
}

// Function to find a string in a TextRange

public TextRange FindTextInRange(TextRange searchRange, string searchText)
{
    // The start position of the search
    TextPointer current = searchRange.Start.GetInsertionPosition(LogicalDirection.Forward);

    while (current != null)
    {
        // The TextRange that contains the current character
        TextRange text = new TextRange(current.GetPositionAtOffset(0, LogicalDirection.Forward), current.GetPositionAtOffset(1, LogicalDirection.Forward));

        // If the current character is the start of the searched text
        if (text.Text == searchText[0].ToString())
        {
            TextRange match = new TextRange(current, current.GetPositionAtOffset(searchText.Length, LogicalDirection.Forward));

            if (match.Text == searchText)
            {
                // Return the match
                return match;
            }
        }

        // Move to the next character
        current = current.GetPositionAtOffset(1, LogicalDirection.Forward);
    }

    // Return null if no match found
    return null;
}

Upvotes: 0

Related Questions