65.9K
CodeProject 正在变化。 阅读更多。
Home

一个通用的 WPF 查找/替换对话框

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2012年4月27日

CPOL
viewsIcon

28108

downloadIcon

9

这是“一个通用的 WPF 查找/替换对话框”的替代方案。

引言

本文修复了WPF RichTextBox原始文章中查找/替换功能中的两个错误。第一个错误是在较大的RTF文件中搜索速度极慢,第二个错误是在找到文本后垂直滚动条的位置不正确。

背景 

我使用了原始文章的代码来创建我自己的WPF RichTextBox中的查找/替换对话框,但是搜索证明速度太慢了。

使用代码 

更改是在Adapters.cs中的RichTextBoxAdapter类中进行的。我没有使用线性搜索,而是采用了更快的二分搜索方法。

/// <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;
  }
} 

关注点

在阅读原始文章时,我学到了更多关于WPF RichTextBox控件的工作原理,并且我对Windows Forms等效版本中许多缺失的功能感到有些失望。

历史 

2012/04/27 - 修复了原始文章中的错误,其中WPF搜索速度慢且滚动到找到的元素不正确。

© . All rights reserved.