Search code examples
wpffull-text-searchricheditbox

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.


Solution

  • 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"/>