一个通用的 WPF 查找/替换对话框
这是“一个通用的 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搜索速度慢且滚动到找到的元素不正确。