Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / WPF
Alternative
Article

A Universal WPF Find / Replace Dialog

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
27 Apr 2012CPOL 25.8K   9   4   9
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. 

C++
/// <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)


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugMinor Update Pin
avaleri18-Apr-13 11:13
avaleri18-Apr-13 11:13 
QuestionVery good and fast searching for RichTextBoxes Pin
rrarey16-Dec-12 9:17
rrarey16-Dec-12 9:17 
GeneralMy vote of 5 Pin
rrarey16-Dec-12 8:57
rrarey16-Dec-12 8:57 
This is an excellent implementation of GetPositionAtOffset.
Question1 Pin
Kevin Marois27-Apr-12 6:40
professionalKevin Marois27-Apr-12 6:40 
AnswerRe: 1 Pin
FenRunner27-Apr-12 8:56
FenRunner27-Apr-12 8:56 
GeneralYou could add a revision ! Pin
Mazen el Senih28-Apr-12 9:11
professionalMazen el Senih28-Apr-12 9:11 
SuggestionWhere's the beef? Pin
Slacker00727-Apr-12 5:19
professionalSlacker00727-Apr-12 5:19 
GeneralRe: Where's the beef? Pin
JoseMenendez27-Apr-12 5:27
JoseMenendez27-Apr-12 5:27 
GeneralRe: Where's the beef? Pin
FenRunner27-Apr-12 8:57
FenRunner27-Apr-12 8:57 

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

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