A Universal WPF Find / Replace Dialog
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.