Click here to Skip to main content
Click here to Skip to main content
Alternative Article

A Universal WPF Find / Replace Dialog

, 27 Apr 2012
Rate this:
Please Sign up or sign in to vote.
This is an alternative for "A Universal WPF Find / Replace Dialog"

Introduction

This article fixes a couple of bugs found in the find / replace functionality in WPF RichTextBox original article. First bug is an extremely slow search in bigger RTF files, the second bug is incorrect positioning of vertical scroll bar after the text was found.

Background 

I used the original article's code for my own find / replace dialog in WPF RichTextBox, however the searching proved to be too slow.

Using the code 

The changes were made to the RichTextBoxAdapter class in Adapters.cs Instead of using linear searching I adopted a quicker method of binary search. 

/// <summary>
/// Adapter for WPF RichTextBox.
/// The WPF RichTextBox does not have a HideSelection property either.
/// Here the currently selected text is colored yellow, so that it can be seen.
/// </summary>
public class RichTextBoxAdapter : IEditor
{
  public RichTextBoxAdapter(RichTextBox editor) { rtb = editor; }

  RichTextBox rtb;
  TextRange oldsel = null;
  public string Text 
  { 
    get 
    { 
      return new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd).Text;
    }
  }
  
  public int SelectionStart
  {
    get
    {
    return GetPos(rtb.Document.ContentStart, rtb.Selection.Start);
    } 
  }
  
  public int SelectionLength { get { return rtb.Selection.Text.Length; } }
  public void BeginChange() { rtb.BeginChange(); }
  public void EndChange() { rtb.EndChange(); }
  
  public void Select(int start, int length)
  {                         
    TextPointer tp = rtb.Document.ContentStart;
      
    TextPointer tpLeft = GetPositionAtOffset(tp, start, LogicalDirection.Forward);
    TextPointer tpRight = GetPositionAtOffset(tp, start + length, LogicalDirection.Forward);
    rtb.Selection.Select(tpLeft, tpRight);
    rtb.Selection.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Yellow);
            
    // Rectangle corresponding to the coordinates of the selected text.
    Rect screenPos = rtb.Selection.Start.GetCharacterRect(LogicalDirection.Forward);
    double offset = screenPos.Top + rtb.VerticalOffset;
    // The offset - half the size of the RichtextBox to keep the selection centered.
    rtb.ScrollToVerticalOffset(offset - rtb.ActualHeight / 2);           
            
    oldsel = new TextRange(rtb.Selection.Start, rtb.Selection.End);
    rtb.SelectionChanged += rtb_SelectionChanged;            
  }

  void rtb_SelectionChanged(object sender, RoutedEventArgs e)
  {
    oldsel.ApplyPropertyValue(TextElement.BackgroundProperty, null);
    rtb.SelectionChanged -= rtb_SelectionChanged;
  }

  public void Replace(int start, int length, string ReplaceWith) 
  {
    TextPointer tp = rtb.Document.ContentStart;
    TextPointer tpLeft = GetPositionAtOffset(tp, start, LogicalDirection.Forward);
    TextPointer tpRight = GetPositionAtOffset(tp, start + length, LogicalDirection.Forward);
    TextRange tr = new TextRange(tpLeft, tpRight);
    tr.Text = ReplaceWith;
  }

  private static int GetPos(TextPointer start, TextPointer p)
  {
    return (new TextRange(start, p)).Text.Length;
  }

  /// <summary>
  /// this method improves upon a slow and annoying method GetPositionAtOffset()
  /// </summary>
  /// <param name="startingPoint"
  /// <param name="offset"></param>
  /// <param name="direction"></param>
  /// <returns></returns>
  private TextPointer GetPositionAtOffset(TextPointer startingPoint, int offset, LogicalDirection direction)
  {
    TextPointer binarySearchPoint1 = null;
    TextPointer binarySearchPoint2 = null;

    // setup arguments appropriately
    if (direction == LogicalDirection.Forward)
    {
      binarySearchPoint2 = this.rtb.Document.ContentEnd;

      if (offset < 0)
      {
        offset = Math.Abs(offset);
      }
    }

    if (direction == LogicalDirection.Backward)
    {
      binarySearchPoint2 = this.rtb.Document.ContentStart;

      if (offset > 0)
      {
        offset = -offset;
      }
    }

    // setup for binary search
    bool isFound = false;
    TextPointer resultTextPointer = null;

    int offset2 = Math.Abs(GetOffsetInTextLength(startingPoint, binarySearchPoint2));
    int halfOffset = direction == LogicalDirection.Backward ? -(offset2 / 2) : offset2 / 2;

    binarySearchPoint1 = startingPoint.GetPositionAtOffset(halfOffset, direction);
    int offset1 = Math.Abs(GetOffsetInTextLength(startingPoint, binarySearchPoint1));

    // binary search loop

    while (isFound == false)
    {
      if (Math.Abs(offset1) == Math.Abs(offset))
      {
        isFound = true;
        resultTextPointer = binarySearchPoint1;
      }
      else
      if (Math.Abs(offset2) == Math.Abs(offset))
      {
        isFound = true;
        resultTextPointer = binarySearchPoint2;
      }
      else
      {
        if (Math.Abs(offset) < Math.Abs(offset1))
        {
          // this is simple case when we search in the 1st half
          binarySearchPoint2 = binarySearchPoint1;
          offset2 = offset1;

          halfOffset = direction == LogicalDirection.Backward ? -(offset2 / 2) : offset2 / 2;

          binarySearchPoint1 = startingPoint.GetPositionAtOffset(halfOffset, direction);
          offset1 = Math.Abs(GetOffsetInTextLength(startingPoint, binarySearchPoint1));
        }
        else
        {
          // this is more complex case when we search in the 2nd half
          int rtfOffset1 = startingPoint.GetOffsetToPosition(binarySearchPoint1);
          int rtfOffset2 = startingPoint.GetOffsetToPosition(binarySearchPoint2);
          int rtfOffsetMiddle = (Math.Abs(rtfOffset1) + Math.Abs(rtfOffset2)) / 2;
          if (direction == LogicalDirection.Backward)
          {
            rtfOffsetMiddle = -rtfOffsetMiddle;
          }

          TextPointer binarySearchPointMiddle = startingPoint.GetPositionAtOffset(rtfOffsetMiddle, direction);
          int offsetMiddle = GetOffsetInTextLength(startingPoint, binarySearchPointMiddle);

          // two cases possible
          if (Math.Abs(offset) < Math.Abs(offsetMiddle))
          {
            // 3rd quarter of search domain
            binarySearchPoint2 = binarySearchPointMiddle;
            offset2 = offsetMiddle;
          }
          else
          {
            // 4th quarter of the search domain
            binarySearchPoint1 = binarySearchPointMiddle;
            offset1 = offsetMiddle;
          }
        }
      }
    }
  
    return resultTextPointer;
  }

  /// <summary>
  /// returns length of a text between two text pointers
  /// </summary>
  /// <param name="pointer1"></param>
  /// <param name="pointer2"></param>
  /// <returns></returns>
  int GetOffsetInTextLength(TextPointer pointer1, TextPointer pointer2)
  {
    if (pointer1 == null || pointer2 == null)
      return 0;
  
    TextRange tr = new TextRange(pointer1, pointer2);

    return tr.Text.Length;
  }
} 

Points of Interest

I did learn whilst reading the original article more about how WPF RichTextBox control work, and I am a little bit dissapointed by a lot of missing functionality which was available in Windows Forms equivalent.

History 

2012/04/27 - bug fixes to the original article where searching WPF was slow and scrolling to the found element was incorrect.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

FenRunner

United Kingdom United Kingdom
No Biography provided

Comments and Discussions

 
BugMinor Update Pinmemberavaleri18-Apr-13 11:13 
QuestionVery good and fast searching for RichTextBoxes Pinmemberrrarey16-Dec-12 9:17 
GeneralMy vote of 5 Pinmemberrrarey16-Dec-12 8:57 
Question1 PinmemberKevin Marois27-Apr-12 6:40 
AnswerRe: 1 PinmemberFenRunner27-Apr-12 8:56 
GeneralYou could add a revision ! PinmemberMazen el Senih28-Apr-12 9:11 
SuggestionWhere's the beef? PinmemberSlacker00727-Apr-12 5:19 
GeneralRe: Where's the beef? PinmemberJose Menendez Póo27-Apr-12 5:27 
GeneralRe: Where's the beef? PinmemberFenRunner27-Apr-12 8:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 27 Apr 2012
Article Copyright 2012 by FenRunner
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid