Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Universal WPF Find / Replace Dialog

0.00/5 (No votes)
27 Apr 2012 1  
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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here