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.
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);
Rect screenPos = rtb.Selection.Start.GetCharacterRect(LogicalDirection.Forward);
double offset = screenPos.Top + rtb.VerticalOffset;
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;
}
private TextPointer GetPositionAtOffset(TextPointer startingPoint, int offset, LogicalDirection direction)
{
TextPointer binarySearchPoint1 = null;
TextPointer binarySearchPoint2 = null;
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;
}
}
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));
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))
{
binarySearchPoint2 = binarySearchPoint1;
offset2 = offset1;
halfOffset = direction == LogicalDirection.Backward ? -(offset2 / 2) : offset2 / 2;
binarySearchPoint1 = startingPoint.GetPositionAtOffset(halfOffset, direction);
offset1 = Math.Abs(GetOffsetInTextLength(startingPoint, binarySearchPoint1));
}
else
{
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);
if (Math.Abs(offset) < Math.Abs(offsetMiddle))
{
binarySearchPoint2 = binarySearchPointMiddle;
offset2 = offsetMiddle;
}
else
{
binarySearchPoint1 = binarySearchPointMiddle;
offset1 = offsetMiddle;
}
}
}
}
return resultTextPointer;
}
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.