I have now used way too long time, trying to figure out a problem, which I didn't think would be that hard.
Here is the deal:
I am writing a small application using C# and WPF.
I have a RichTextBox containing a FlowDocument.
I have added a small textbox and a button below my richtextbox.
The user then types in the word he/she wishes to search for, and presses the button.
The richtextbox will then jump to the first occurrance of that word.
it is enough that it just jumps to the correct line - it can also select, highlight or place the cursor by the word - anything will do, as long as the richTextBox is scrolled to the word.
Continuing to press the button, will then jump to the next occurance of the word, and so on so forth, till the end of the document.
As I said - I thought it to be a simple task - however I am having serious problems figuring this out.
This should do the job:
public bool DoSearch(RichTextBox richTextBox, string searchText, bool searchNext)
{
TextRange searchRange;
// Get the range to search
if(searchNext)
searchRange = new TextRange(
richTextBox.Selection.Start.GetPositionAtOffset(1),
richTextBox.Document.ContentEnd);
else
searchRange = new TextRange(
richTextBox.Document.ContentStart,
richTextBox.Document.ContentEnd);
// Do the search
TextRange foundRange = FindTextInRange(searchRange, searchText);
if(foundRange==null)
return false;
// Select the found range
richTextBox.Selection.Select(foundRange.Start, foundRange.End);
return true;
}
public TextRange FindTextInRange(TextRange searchRange, string searchText)
{
// Search the text with IndexOf
int offset = searchRange.Text.IndexOf(searchText);
if(offset<0)
return null; // Not found
// Try to select the text as a contiguous range
for(TextPointer start = searchRange.Start.GetPositionAtOffset(offset); start != searchRange.End; start = start.GetPositionAtOffset(1))
{
TextRange result = new TextRange(start, start.GetPositionAtOffset(searchText.Length);
if(result.Text == searchText)
return result;
}
return null;
}
The reason for the for() loop in FindTextInRangeUnfortunately the range.Text strips out non-text characters, so in some cases the offset computed by IndexOf will be slightly too low.
I have used a Different Approach.
Using a TextBox to Set the Keyword; this Searches for the KeyWord on Button Click.
Finds the Keyword; Highlighs it and Focuses on that KeyWord.
// Index of Current Result Found (Counts Characters not Lines or Results)
private int IndexOfSearchResultFound;
// Start Position Index of RichTextBox (Initiated as 0 : Beggining of Text / 1st Char)
private int StartOfSelectedKeyword;
private int EndOfSelectedKeyword;
private void btnSearch_Click(object sender, EventArgs e)
{
// Reset Keyword Selection Index. (0 is the Staring Point of the Keyword Selection)
IndexOfSearchResultFound = 0;
// Specify the End of the Selected Keyword; using txt_Search.Text.Lenght (Char Ammount).
EndOfSelectedKeyword = txt_Search.Text.Length;
// If txt_Search.Text is not Empty
if (txt_Search.Text.Length > 0)
{
// Find Keyword in RichTextBox.Text
IndexOfSearchResultFound = FindKeyword(txt_Search.Text.Trim(), StartOfSelectedKeyword, rtb_Hosts.Text.Length);
// If string was found in RichTextBox; Highlight it and Focus on Keyword Found Location
if (IndexOfSearchResultFound >= 0)
{
// Focus on Currently Found Result
rtb_Hosts.Focus();
// Highlight the search string
rtb_Hosts.Select(IndexOfSearchResultFound, EndOfSelectedKeyword);
// Sets a new Starting Position (after the Position of the Last Result Found)
// To be Ready to Focus on the Next Result
StartOfSelectedKeyword = IndexOfSearchResultFound + EndOfSelectedKeyword;
}
}
}
private int FindKeyword(string _SearchKeyword, int _KeywordSelectionStart, int _KeywordSelectionEnd)
{
// De-Select Previous Searched String (Keyword)
if (_KeywordSelectionStart > 0 && _KeywordSelectionEnd > 0 && IndexOfSearchResultFound >= 0)
{ rtb_Hosts.Undo(); }
// Set the return value to -1 by default.
int retVal = -1;
// A valid Starting index should be specified.
// if indexOfSearchText = -1, Means that Search has reached the end of Document
if (_KeywordSelectionStart >= 0 && IndexOfSearchResultFound >= 0)
{
// Find Keyword
IndexOfSearchResultFound = rtb_Hosts.Find(_SearchKeyword, _KeywordSelectionStart, _KeywordSelectionEnd, RichTextBoxFinds.None);
// Determine whether the text was found in richTextBox
retVal = IndexOfSearchResultFound;
}
// Return the index to the specified Keyword text.
return retVal;
}
The only thing I couldn't achieve yet is to return to the 1st Search Result
This is a variant that will find the match closest to the Caret position.
private TextRange FindText(string findText)
{
var fullText = DoGetAllText();
if (string.IsNullOrEmpty(findText) || string.IsNullOrEmpty(fullText) || findText.Length > fullText.Length)
return null;
var textbox = GetTextbox();
var leftPos = textbox.CaretPosition;
var rightPos = textbox.CaretPosition;
while (true)
{
var previous = leftPos.GetNextInsertionPosition(LogicalDirection.Backward);
var next = rightPos.GetNextInsertionPosition(LogicalDirection.Forward);
if (previous == null && next == null)
return null; //can no longer move outward in either direction and text wasn't found
if (previous != null)
leftPos = previous;
if (next != null)
rightPos = next;
var range = new TextRange(leftPos, rightPos);
var offset = range.Text.IndexOf(findText, StringComparison.InvariantCultureIgnoreCase);
if (offset < 0)
continue; //text not found, continue to move outward
//rtf has broken text indexes that often come up too low due to not considering hidden chars. Increment up until we find the real position
var findTextLower = findText.ToLower();
var endOfDoc = textbox.Document.ContentEnd.GetNextInsertionPosition(LogicalDirection.Backward);
for (var start = range.Start.GetPositionAtOffset(offset); start != endOfDoc; start = start.GetPositionAtOffset(1))
{
var result = new TextRange(start, start.GetPositionAtOffset(findText.Length));
if (result.Text?.ToLower() == findTextLower)
{
return result;
}
}
}
}
If you want to highlight the match then it'd be as simple as changing this method to void and doing this when you found the match:
textbox.Selection.Select(result.Start, result.End);