问题
I've a misunderstanding about FlowDocument, please help me to see clear. I'm working on a source code editor, where user can add few special variables, and later the program looking for this variables. For this editor, I'm using RichTextBox(RTB). I'd like to use color for these variables. It was not a problem to add color when the user add new variable to the text. But when the user open a source code what has already some variables first I've to go trough on the whole text and colorize the variables.
The code below: First I'm search all variables and their position with regex.(Variables looks like: <*variable*>) Then loop trough and change the color one by one, but when I'm making the TextRange, the GetPositionAtOffset gives back wrong value. I know it is because of the special formatting characters also counting by GetPositionAtOffset. The question is, how can I solve this?
private void ColorizeAllVariable(TextRange TR_Input)
{
Regex regex = new Regex(@"(<\*.[^<\*>]*\*>)");
MatchCollection matches = regex.Matches(TR_Input.Text);
NoRTBChangeEvent = true;
for (int i = 0; i < matches.Count; i++)
{
TextRange TR_Temp = new TextRange(TR_Input.Start.GetPositionAtOffset(matches[i].Index), TR_Input.Start.GetPositionAtOffset(matches[i].Index + matches[i].Length));
TR_Temp.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.DodgerBlue);
}
NoRTBChangeEvent = false;
}
Update 1:
Following user8478480 solution, I changed my code.
private void ColorizeAllVariable(RichTextBox richTextBox)
{
IEnumerable<TextRange> WordRanges = GetAllWordRanges(richTextBox.Document, @"(<\*.[^<\*>]*\*>)");
foreach (TextRange WordRange in WordRanges)
{
WordRange.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.DodgerBlue);
}
}
private static IEnumerable<TextRange> GetAllWordRanges(FlowDocument document, string pattern)
{
TextPointer pointer = document.ContentStart;
while (pointer != null)
{
if (pointer.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
string textRun = pointer.GetTextInRun(LogicalDirection.Forward);
MatchCollection matches = Regex.Matches(textRun, pattern);
foreach (Match match in matches)
{
int startIndex = match.Index;
int length = match.Length;
TextPointer start = pointer.GetPositionAtOffset(startIndex);
TextPointer end = start.GetPositionAtOffset(length);
yield return new TextRange(start, end);
}
}
pointer = pointer.GetNextContextPosition(LogicalDirection.Forward);
}
}
It is directly looking for the words what looks like <*word*>. And it finds all the word, but still have problem with the formatting characters.
This is the result. The second word in the line has wrong coloring position
This is how the line looks like, when it search for the word
This is another trying
I see the problem, when I add the color property it shifts the data but my match contains the position before the colorization.
It looks easy, if I have more than one match in one line I always shifting the position by constant value. But the formatting characters doesn't looks like always the same length. As you can see on the second try, the first variable color is correct. Than the second has 5 character shifting, the third variable also has 5 character shifting, the fourth variable has 9 character shifting,the fifth variable has 13 character shifting, the sixth... (I've no idea what is going on here), and the last the seventh variable has also good color position.
回答1:
I found the problem and the solution also.
Problem: When the regex find all matches in a line, there is no color formatting. But when I add the color format to the first match, it shift the text, but the regex match result has still the old positions.
Solution: Always change only the first match. When it is done, get the new text pointer what will give you back the text before your colored word. Then gives back again you colored word (It must skip, because of the two times coloring). And than gives back the text after your colored word, what contains the other special words in the line.
Example: I want to colorize the words looks like: <*word*>.
Text to colorize: "This is a <*test*> <*sentence*>."
First step: Gives back the whole line. Regex gives 2 matches (<*test*>,<*sentence*>). Colorize the first one.
Second step: Gives back: "This is a ". Do nothing with that
- Third step: Gives back: "<*test*>". Regex gives 1 match (<*test*>).Skip it, because it is already colorized.
- Fourth step: Gives back: " ". Do nothing with that.
- Fifth step: Gives back: "<*sentence*>". Regex gives 1 match (<*sentence*>). Colorize.
- Sixth step: Gives back: "<*sentence*>". Regex gives 1 match (<*sentence*>). Skip it, because it is already colorized.
End
{ TextPointer pointer = document.ContentStart; bool Skip = false; string textRun = ""; while (pointer != null) { if (pointer.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) { do { if (!Skip) { textRun = pointer.GetTextInRun(LogicalDirection.Forward); MatchCollection Matches = Regex.Matches(textRun, pattern); if (Matches.Count > 0) { Skip = true; int startIndex = Matches[0].Index; int length = Matches[0].Length; TextPointer start = pointer.GetPositionAtOffset(startIndex); TextPointer end = start.GetPositionAtOffset(length); yield return new TextRange(start, end); } } else { pointer = pointer.GetNextContextPosition(LogicalDirection.Forward); if (pointer.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) { textRun = pointer.GetTextInRun(LogicalDirection.Forward); if(Regex.IsMatch(textRun,pattern)) { Skip = false; } } } } while (Skip); } pointer = pointer.GetNextContextPosition(LogicalDirection.Forward); } }
回答2:
I am not saying this is the most glamorous way but the RichTextBox
control isn't very easy to use in the standard WPF
toolkit. So here is a way I've done what you're trying to accomplish before.
Essentially this splits takes your original content, splits it into flow document elements and then iterates over every word from the document as a text range. It then applies the formatting to each word if it matches the criteria, as outlined in the Foreach
. Hope this helps.
P.S after thinking about it not all of the code may be needed as my implementation also had a jump to line function hence why I was splitting the document into lines. Good luck!
//new doc.
var doc = new FlowDocument();
//loop all lines from text.(split on \r\n)
string[] lines = RichTextBoxExtraControl.Text.Split(new string[] { "\r\n" }, StringSplitOptions.None);
for (int i = 0; i < lines.Length; i++)
{
//make new paragraph
var run = new Run(lines[i]);
var par = new Paragraph(run);
par.LineHeight = 1;
doc.Blocks.Add(par);
}
//Searches a list of all words to highlight in place the words below
IEnumerable<TextRange> wordRanges = GetAllWordRanges(doc);
foreach (TextRange wordRange in wordRanges)
{
if (wordRange.Text == ">WORD YOU WANT TO HIGHLIGHT<")
{
wordRange.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Red); //Effect to apply.
}
}
//Set document.
RichTextBox1.Document = doc;
}
using this method Highlighting keywords in a richtextbox in WPF
public static IEnumerable<TextRange> GetAllWordRanges(FlowDocument document)
{
string pattern = @"[^\W\d](\w|[-']{1,2}(?=\w))*";
TextPointer pointer = document.ContentStart;
while (pointer != null)
{
if (pointer.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
string textRun = pointer.GetTextInRun(LogicalDirection.Forward);
MatchCollection matches = Regex.Matches(textRun, pattern);
foreach (Match match in matches)
{
int startIndex = match.Index;
int length = match.Length;
TextPointer start = pointer.GetPositionAtOffset(startIndex);
TextPointer end = start.GetPositionAtOffset(length);
yield return new TextRange(start, end);
}
}
pointer = pointer.GetNextContextPosition(LogicalDirection.Forward);
}
}
来源:https://stackoverflow.com/questions/54810368/c-sharp-wpf-colorize-specific-words-in-richtextbox-text