In a C# forms app, how can I search for highlighted text in a RichTextBox, like that available in Microsoft Word using the ‘Find and Replace’ format option.
There does not appear to be a field in the RichTextBoxFinds Enum that has this option. If I search for what I want to do on Google, I can find plenty of examples of how to search for text and highlight it, but nothing to find text that is highlighted. I also cannot find anything relating to this subject on the Stack Overflow site.
I am seeking suggestions on methods I could potentially use to do this, or ways to approach the problem.
Update
I have now created the below code in response to feedback to search for highlighted text (SelectionBackColour not white) in a RichTextBox which works but appears dreadfully slow compared to using Find to search for a string. I can speed things up if I set the HideSelection property to true before I start the loop, but still to slow, and when it enters the loop, the RichTextBox just turns dark grey until the loop is exited and I set HideSelection false again. What could be limiting the speed?
private bool FindNextH(int hightLightColour, int selectionStart, int selectionEnd)
{
int matchColour = hightLightColour;
bool matchHighlight = true;
if (matchColour == -1) // White
{
matchHighlight = false;
}
int state = 0;
int highlightStart = 0;
int highlightLength = 0;
int highlight = -1;
bool found = false;
richTextBox1.SelectionStart = selectionStart;
richTextBox1.SelectionLength = 1;
for (int i = selectionStart; i <= selectionEnd; i++)
{
richTextBox1.SelectionStart = i;
int colour = richTextBox1.SelectionBackColor.ToArgb();
switch (state)
{
case 0:
if (colour != -1)
{
if (matchHighlight)
{
if (colour == matchColour)
{
state = 1;
highlightStart = i;
highlight = colour;
}
}
else
{
state = 1;
highlightStart = i;
highlight = colour;
}
}
break;
case 1:
if ((colour != highlight) || (i == selectionEnd))
{
highlightLength = i - highlightStart;
richTextBox1.SelectionStart = highlightStart;
richTextBox1.SelectionLength = highlightLength;
textBox2.Text = richTextBox1.SelectedText;
found = true;
}
break;
default:
break;
}
if (found)
{
break;
}
}
return found;
}
Further testing has been done to quantify timings as follows:
Rich Text File containing 30,093 words, 137,984 characters (no spaces), 167,844 characters with spaces. ‘TestWord’ is contained in the file just over halfway through and is highlighted green.
Using Microsoft Word’s ‘Find and Replace’ (Ctrl H), with ‘Format’ set to ‘Highlight’ and the ‘Find what’ blank, it takes approximately one second for the document to scroll to the found highlighted word, starting with the cursor at the first character. If I use ‘Find and Replace’ to search for the text, no formatting, then it takes about the same time.
If I run a test using ‘richTextBox1.Find’ to search for ‘TestWord’, it takes between 5 and 15 ms to execute, and the text in the richTextBox1 jumps to it instantaneously, so very quick.
If I run a test using my function ‘FindNextH()’, it takes approximately 3.8 seconds to find the highlighted text, if I set ‘richTextBox1.HideSelection’ to ‘true’ before calling the function, and returning it to ‘false’ afterwards. During the 3.8 seconds, the richTextBox1 window goes dark gey and the text disappears, returning only when the highlighted word has been found.
If ‘richTextBox1.HideSelection’ is set to ‘false’ before calling ‘FindNextH(), it takes approximately 50 seconds to find the highlighted word, during which time you can the cursor scrolling through the text.
What can I do to speed this up? Clearly Microsoft have cracked it in MS Word. It is a pity that this feature is not a part of the ‘Find’ method.
Code that calls the two search functions below for reference:
richTextBox1.HideSelection = false;
stopWatch.Start();
int IndexOfSearchResultFound = richTextBox1.Find(_SearchKeyword, _KeywordSelectionStart, _KeywordSelectionEnd,
RichTextBoxFinds.None | RichTextBoxFinds.WholeWord | RichTextBoxFinds.MatchCase);
stopWatch.Stop();
richTextBox1.HideSelection = false;
TimeSpan timeDelay = stopWatch.Elapsed;
richTextBox1.HideSelection = true;
stopWatch.Start();
found = FindNextH(colour, selectionStart, selectionEnd);
stopWatch.Stop();
richTextBox1.HideSelection = false;
TimeSpan timeDelay = stopWatch.Elapsed;
Related
After setting my RichTextBox's text to the string T, the Caret Position in the RichTextBox is "lost" (it goes to the start of it). Here's what I'm doing to try to "restore" it after it is "lost":
public static int GetCaretIndex(RichTextBox C)
{
return new TextRange(C.Document.ContentStart, C.CaretPosition).Text.Length;
}
...
int CaretIndex = GetCaretIndex(C); // Get the Caret position before setting the text of the RichTextBox
new TextRange(C.Document.ContentStart, C.Document.ContentEnd).Text = T; // Set the text of the RichTextBox
C.CaretPosition = C.Document.ContentStart.GetPositionAtOffset(CaretIndex, LogicalDirection.Forward); // Set the Caret Position based on the "Caret Index" variable
This code, however, does not work. The "restored" Caret is at a different position than the "original" one (always behind the "original" one for some reason).
"Saving" the RichTextBox's CaretPosition as a TextPointer doesn't seem to work either.
Can anyone provide me with an alternative way of "restoring" the Caret, or a way to fix the code above?
Seems to work (for me):
C.CaretPosition = C.Document.ContentStart;
C.CaretPosition = C.CaretPosition.GetPositionAtOffset(CaretIndex, LogicalDirection.Forward);
(I hate RichTextBox by the way.)
I was dealing with a similar issue recently and there is my solution. In my case, I'm creating a new RichTextBox.Document content and when I do this, I want to keep the caret position.
My idea was that caret offset functions are biased thanks to data structures used for text representation (Paragraphs, Runs, ...) which are also somehow calculated to offset position.
TextRange is a good approach to get exact caret position in the text. The problem lays in its restoration. But it gets easy when I know from which components my document is constructed. In my case, there are just Paragraphs and Runs.
What remains is to visit document structure, find an exact run where the caret should be and set the caret to correct position of found run.
Code:
// backup caret position in text
int backPosition =
new TextRange(RichTextBox.CaretPosition.DocumentStart, RichTextBox.CaretPosition).Text.Length;
// set new content (caret position is lost there)
RichTextBox.Document.Blocks.Clear();
SetNewDocumentContent(RichTextBox.Document);
// find position and run to which place caret
int pos = 0; Run caretRun = null;
foreach (var block in RichTextBox.Document.Blocks)
{
if (!(block is Paragraph para))
continue;
foreach (var inline in para.Inlines){
{
if (!(inline is Run run))
continue;
// find run to which place caret
if (caretRun == null && backPosition > 0)
{
pos += run.Text.Length;
if (pos >= backPosition){
caretRun = run;
break;
}
}
}
if (caretRun!=null)
break;
}
// restore caret position
if (caretRun != null)
RichTextBox.CaretPosition =
caretRun.ContentEnd.GetPositionAtOffset(backPosition - pos, LogicalDirection.Forward);
The code is not tested. I assembled it from various parts of my application. Let me know if you find any issue.
In my situation I have a RichTextBox with a single Paragraph that only allows entering text and line breaks. I change the structure of the RichTextBox ( by creating different coloured Run instances ) but not the text and restore after the change.
public static class CaretRestorer
{
public static void Restore(RichTextBox richTextBox, Action changer)
{
var caretPosition = GetCaretPosition(richTextBox);
changer();
Restore(richTextBox, caretPosition);
}
private static string GetFullText(RichTextBox richTextBox)
{
return new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd).Text;
}
private static int GetInlineTextLength(Inline inline)
{
if(inline is LineBreak)
{
return 2;
}
return new TextRange(inline.ContentStart, inline.ContentEnd).Text.Length;
}
private static void Restore(RichTextBox richTextBox,int caretPosition)
{
var inlines = GetInlines(richTextBox);
var accumulatedTextLength = 0;
foreach (var inline in inlines)
{
var inlineTextLength = GetInlineTextLength(inline);
var newAccumulatedTextLength = accumulatedTextLength + inlineTextLength;
if (newAccumulatedTextLength >= caretPosition)
{
TextPointer newCaretPosition = null;
if(inline is LineBreak)
{
newCaretPosition = inline.ContentEnd;
}
else
{
var diff = caretPosition - accumulatedTextLength;
newCaretPosition = inline.ContentStart.GetPositionAtOffset(diff);
}
richTextBox.CaretPosition = newCaretPosition;
break;
}
else
{
accumulatedTextLength = newAccumulatedTextLength;
}
}
}
private static int GetCaretPosition(RichTextBox richTextBox)
{
return new TextRange(richTextBox.Document.ContentStart, richTextBox.CaretPosition).Text.Length;
}
private static Paragraph GetParagraph(RichTextBox RichTextBox)
{
return RichTextBox.Document.Blocks.FirstBlock as Paragraph;
}
private static InlineCollection GetInlines(RichTextBox RichTextBox)
{
return GetParagraph(RichTextBox).Inlines;
}
}
I'm pulling the content from a text file into a RichTextBox. I've got the RichTextBox set up to where it only shows 6 lines at a time. I've got a search method that finds the text I need within the RichTextBox, but what I am needing it to do is display 6 specific lines. Each "item" in my text file consists of 6 lines. No matter which of the six lines the search method finds the text on, I need the RichTextBox to only display the 6 lines of each "item" with the currently selected "found" text remaining highlighted.
I've gotten it working reasonably well thanks to a few code examples I've pull from this site. But every now and then, it doesn't work entirely well, and am looking for some advice from a fresh set of eyes looking at my code and perhaps even be told an easier/more efficient way to go about it. But here is my code so far. Thanks in advance!
try
{
string s = txtFindPlaylistEntry.Text;
rtxEditPlaylistEntry.Focus();
findPosEntry = rtxEditPlaylistEntry.Find(s, findPosEntry, RichTextBoxFinds.None);
// Jump to the line we need.
int count = rtxEditPlaylistEntry.GetLineFromCharIndex(findPosEntry);
count = (count - (count % 6)) + 1; // Must be divisible by 6 then plus 1
rtxEditPlaylistEntry.SelectionStart = rtxEditPlaylistEntry.Find(rtxEditPlaylistEntry.Lines[count]);
rtxEditPlaylistEntry.ScrollToCaret();
rtxEditPlaylistEntry.Select(findPosEntry, s.Length);
findPosEntry += txtFindPlaylistEntry.Text.Length;
}
catch
{
MessageBox.Show("No occurences found");
findPosEntry = 0;
}
As of right now, I'm attempting to use a line count with modulus plus 1 to get the line I need. Like I said, it works, just not 100% of the time and I can't figure out why.
EDIT to try to accommodate Minimal, Complete, Verifiable.
I've already posted my "find" function. Here is other related code that might be useful. First, here is my code for creating the various controls I am using.
rtxEditPlaylistEntry = new RichTextBox();
rtxEditPlaylistEntry.Location = new System.Drawing.Point(15, 90);
rtxEditPlaylistEntry.Size = new System.Drawing.Size(375, 85);
rtxEditPlaylistEntry.Multiline = true;
rtxEditPlaylistEntry.ScrollBars = RichTextBoxScrollBars.None;
Here is my button function to pull text from a file and place it into the RichTextBox.
private void btnBrowseForPlaylistToEditEntry_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "LPL Files|*.lpl";
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
savedFileNameEntry = openFileDialog.SafeFileName;
txtPathToPlaylistToEditEntry.Text = openFileDialog.FileName;
}
// After finding the file, load it into the richtextbox control
using (StreamReader sr = File.OpenText(openFileDialog.FileName))
{
// Initially show the first 6 lines (IE first entry). This should be accomplished by
// the richtextbox control settings
rtxEditPlaylistEntry.Text = sr.ReadToEnd();
}
previousNextCount = 0;
}
I hope this is sufficient. If not please let me know!
I am developing one Windows phone app. In my app I want to get the latest entered word in textbox not the last word. And I want to change that latest entered word on space key pressed. I am getting the last word on a key up event like this:
private async void mytxt_KeyUp_1(object sender, KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Space || e.Key == Windows.System.VirtualKey.Enter)
{
if (string.IsNullOrWhiteSpace(textBox_string) == false)
{
string[] last_words = Regex.Split(textBox_string, #"\s+");
int i = last_words.Count();
last_words = last_words.Where(x => x != last_words[i-1]).ToArray(); last_word = last_words[last_words.Count() - 1];
last_word = last_word.TrimStart();
}
}
}
I am getting the last word by this method but actually I want to get latest entered word by user. Meaning, if the user moves the cursor directly to the middle of textbox and types any word then I want to get that word on space key pressed event; I want the position of that word and can change that word programmatically and update textbox.
For example, if the user types
H!! my name vanani
but then the user moves the cursor directly after 'name' and types 'is sohan'
H!! my name is sohan
then I want to get word and position of 'is' and same for 'sohan' in key up event of textbox. I need the position to replace that word with another word and update textbox with the new replaced text.
I have seen these questions. winforms - get last word.. and C# how to get latest char.. but they didn't help me. Please help me.
Like this:
if (Regex.IsMatch(textBox_string, #"\S*(?=\s?$)"))
{
Match match = Regex.Match(textBox_string, #"\S*(?=\s?$)");
string word = match.Value;
int startingIndex = match.Index;
int length = word.Length;
}
I founded answer to my question.
Here is code worked for me.
Bool isFirst = false;
int mytempindex;
private async void mytxt_KeyUp_1(object sender, KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Space)
{
int i = mytxt.SelectionStart;
if (i < mytxt.Text.Length)
{
if (isfirst == false)
{
mytempindex = mytxt.SelectionStart;
isfirst = true;
}
else
{
int mycurrent_index = mytxt.SelectionStart;
int templength_index = mycurrent_index - mytempindex;
string word = mytxt.Text.Substring(mytempindex, templength_index); //It is the latest entered word.
//work with your last word.
}
}
}
}
I don't think it work in all situation but through this you can get idea about how to get latest entered word from Textbox or RichTextbox.
I use RichTextBox for testing REGEX expression, with the code like:
rtbMain.SelectAll();
rtbMain.SelectionColor = Color.Black;
rtbMain.SelectionBackColor = Color.White;
Regex regex = new Regex(txtRegexPattern.Text, regexOptions);
Match matches = regex.Match(txtTest.Text);
while (matches.Success)
{
rtbMain.Select(matches.Index, match.Length);
rtbMain.SelectionColor = Color.Red;
rtbMain.SelectionBackColor = Color.Black;
}
But this method becomes too slow as soon as there are more than a few thousand (1000+) characters to be highlighted. I know I could delay processing, so that code gives user a chance to enter the whole Regular Expression, but still I think RichTextBox highlighting is working too slow.
I've searched the Google for different approaches and ways to speed up current solution, but I didn't have luck. I noticed that there are a few text editors which allow "syntax highlighting" (like ScintillNET, Avalon,...) but they use XML as input, so I think using them for my project (generating XML on every KeyUp event) wouldn't be the "best practice".
I have found and tested a "Fast colored Textbox" here: https://github.com/PavelTorgashov/FastColoredTextBox ...but the problem with this one is that it replaces the paste content while it uses its own new-line and tab character, and I cant use it in REGEX tester.
Is there any faster way to highlight all matches, maybe using a different user control?
EDIT:
APPROACH 1: Would generating the underlying RTF document be faster? I tried but had some problems with special characters, so I could test highlighting of the whole document, but it seemed to work quite fast with normal characters in a single line. I paused working on this since I read constructing RTF's can be quite hard, and I think I couldn't use none of the existing RTF libraries.
APPROACH 2: I am able to get only the displayed portion of RichTextBox, so I was thinking to only highlight that part. I guess this would significantly reduce processing (depends on RTB size), but I would need to trigger highlighting every time user scrolls; I'm not sure this would work well and create a decent user experience, so haven't tried it out yet.
Would anyone recommend any of the approaches above or maybe any others?
First:
The RichTextBox has an inherent problem: It is very slow in .NET. I found a solution how to make it 120 times faster. May be you try it out: C# RichEditBox has extremely slow performance (4 minutes loading) SOLVED
Second:
Building the RTF code from the scratch is far the fastest solution. Have a look a my article on codeproject. There is a RTF builder class that is reusable: http://www.codeproject.com/Articles/23513/SQL-Editor-for-Database-Developers
Please check Expresso at http://www.codeproject.com/Articles/3669/Expresso-A-Tool-for-Building-and-Testing-Regular-E
I have been using this program for editing and evaluating regex for years.
I have a doubt that you have setup your While loop in an incorrect manner.
Try something like this: (Untested, but will give you an idea how to troubleshoot this problem)
rtbMain.SelectAll();
rtbMain.SelectionColor = Color.Black;
rtbMain.SelectionBackColor = Color.White;
Regex regex = new Regex(txtRegexPattern.Text, regexOptions);
MatchCollection matches = regex.Matches(txtTest.Text);
if(matches.Count > 0)
{
foreach(Match m in matches)
{
rtbMain.Select(m.Index, m.Length);
rtbMain.SelectionColor = Color.Red;
rtbMain.SelectionBackColor = Color.Black;
}
}
else
{
Debug.Print("No matches found"); // See "Output" Window
}
EDIT
I did some workaround related to highlight RTF text and first thing I found is the mostly time taken by the process was these lines:
rtbMain.SelectionColor = Color.Red;
rtbMain.SelectionBackColor = Color.Black;
I tried selecting the text using SelectionStart and SelectionEnd properties instead .Select(), but NO change has been observed.
Regarding your first point which is related to constructing equivalent RTF, I tried that too but it is difficult to construct an equivalent RTF since there are lot of stuff there which needs to be handle. If it can be done the process time will be around < 1.5 seconds for more than 31k matches (a result of basic test on a specific sample).
So, I would suggest you to do it via THREADING and split task in two threads:
Here is an example source code:
(For worst case i found around 31341 matches and process took 4 seconds to highlight)
// declare variables either globally or in the same method
MatchCollection mcoll;
Stopwatch s;
int callbackCount = 0;
List<Match> m1 = null;
List<Match> m2 = null;
private void btnHighlight_Click(object sender, EventArgs e)
{
//reset any exisiting formatting
rtbMain.SelectAll();
rtbMain.SelectionBackColor = Color.White;
rtbMain.SelectionColor = Color.Black;
rtbMain.DeselectAll();
s = new Stopwatch();
s.Start();
Regex re = new Regex(#"(.)", RegexOptions.Compiled); // Notice COMPILED option
mcoll = re.Matches(rtbMain.Text);
// Break MatchCollection object into List<Matches> which is exactly half in size
m1 = new List<Match>(mcoll.Count / 2);
m2 = new List<Match>(mcoll.Count / 2);
for (int k = 0; k < mcoll.Count; k++)
{
if (k < mcoll.Count / 2)
m1.Add(mcoll[k]);
else
m2.Add(mcoll[k]);
}
Thread backgroundThread1 = new Thread(new ThreadStart(() => {
match1(null, null);
}));
backgroundThread1.Start();
Thread backgroundThread2 = new Thread(new ThreadStart(() =>
{
match2(null, null);
}));
backgroundThread2.Start();
}
public void match1(object obj, EventArgs e)
{
for (int i=0; i < m1.Count; i += 1)
{
if (rtbMain.InvokeRequired)
{
EventHandler d = new EventHandler(match1);
rtbMain.Invoke(d);
}
else
{
rtbMain.Select(m1[i].Index, m1[i].Length);
rtbMain.SelectionBackColor = Color.Black;
rtbMain.SelectionColor = Color.Red;
}
}
stopTimer();
}
public void match2(object obj, EventArgs e)
{
for (int j=0; j < m2.Count; j += 1)
{
if (rtbMain.InvokeRequired)
{
EventHandler d = new EventHandler(match2);
rtbMain.Invoke(d);
}
else
{
rtbMain.Select(m2[j].Index, m2[j].Length);
rtbMain.SelectionBackColor = Color.Black;
rtbMain.SelectionColor = Color.Red;
}
}
stopTimer();
}
void stopTimer()
{
callbackCount++;
if (callbackCount == 2) // 2 because I am using two threads.
{
s.Stop();
// Check Output Window
Debug.Print("Evaluated in : " + s.Elapsed.Seconds.ToString());
}
}
Since as you posted it takes around 30 sec to manipulate, hope 4 sec is bearable and user can be engaged by some loading screen as the other online converters do like Rubular and DerekSlager's .Net regex tester does.
Don't forget to have a look at Why Regex.Compiled preferred.
I want to implement something that programmatically changes the background of the text when provided with a documentline.(Something that looks very similar to a block selection of a text. I'm going to be using this for debug breakpoints of an IDE I'm designing). I don't want to have to use selection as it causes the textbox to scroll.
I think I need to make use of DocumentColorizingTransformer but I'm not 100% sure how to go about this.
public class ColorizeAvalonEdit : ICSharpCode.AvalonEdit.Rendering.DocumentColorizingTransformer
{
protected override void ColorizeLine(ICSharpCode.AvalonEdit.Document.DocumentLine line)
{
int lineStartOffset = line.Offset;
string text = CurrentContext.Document.GetText(line);
int start = 0;
int index;
if (line.LineNumber == LogicSimViewCodeWPFCtrl.currentLine)
{
while ((index = text.IndexOf(text, start)) >= 0)
{
base.ChangeLinePart(
lineStartOffset + index, // startOffset
lineStartOffset + index + text.Length, // endOffset
(VisualLineElement element) =>
{
element.TextRunProperties.SetBackgroundBrush(Brushes.Red);
});
start = index + 1; // search for next occurrence
}
}
}
}
currentLine is the portion that will be highlighted.
The above code does work properly.. only problem is if the currentLine ever changes while I am viewing that line, it doesn't highlight the updated line until I scroll to another portion of the document (hiding the updated line), and come back to the updated line.
Also, how do I make the line numbers start from zero?
Since it was their creation, I peeked at SharpDevelop's source and how they did it.
They defined a bookmark type (BreakpointBookmark) and added bookmark to the line.
bookmark itself sets the color of the line in CreateMarker method. It is strange that it is not possible to configure colors of the break-point in SharpDevelop.
Hope it helps.
protected override ITextMarker CreateMarker(ITextMarkerService markerService)
{
IDocumentLine line = this.Document.GetLine(this.LineNumber);
ITextMarker marker = markerService.Create(line.Offset, line.Length);
marker.BackgroundColor = Color.FromRgb(180, 38, 38);
marker.ForegroundColor = Colors.White;
return marker;
}
I found the answer
TxtEditCodeViewer.TextArea.TextView.Redraw();
Isn't this a duplicate of this question?
However it looks like you should call InvalidateArrange() on the editor or InvalidateVisual() on each changed visual.