C# WPF 为 RichTextBox 文本中的特定单词着色

C# WPF colorize specific words in RichTextBox text

我对FlowDocument有一个误解,请帮我看清楚。 我正在开发一个源代码编辑器,用户可以在其中添加一些特殊变量,然后程序会查找这些变量。对于这个编辑器,我使用的是 RichTextBox(RTB)。我想为这些变量使用颜色。当用户向文本添加新变量时添加颜色不是问题。但是,当用户打开源代码时,首先已经有一些变量,我必须遍历整个文本并为变量着色。


代码如下: 首先,我用正则表达式搜索所有变量及其位置。(变量看起来像:<*variable*>)然后循环并逐个更改颜色,但是当我制作 TextRange 时,GetPositionAtOffset 返回错误的值.我知道这是因为特殊格式字符也由 GetPositionAtOffset 计算。 问题是,我该如何解决?

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;
    }

更新一:

根据 解决方案,我更改了代码。

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);
        }
    }

直接找<*word*>这样的词。它找到了所有单词,但格式化字符仍然有问题。

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

我看到了问题,当我添加颜色时 属性 它会移动数据,但我的匹配包含着色前的位置。

这看起来很简单,如果我在一行中有多个匹配项,我总是将位置移动一个常数值。但是格式化字符看起来并不总是相同的长度。正如您在第二次尝试中看到的那样,第一个可变颜色是正确的。比第二个有 5 个字符移位,第三个变量也有 5 个字符移位,第四个变量有 9 个字符移位,第五个变量有 13 个字符移位,第六个......(我不知道这里发生了什么) ,最后的第七个变量也有很好的颜色位置。

我并不是说这是最迷人的方式,但 RichTextBox 控件在标准 WPF 工具包中不是很容易使用。所以这是我之前完成你想要完成的事情的方法。

本质上,这种拆分会获取您的原始内容,将其拆分为流文档元素,然后将文档中的每个单词作为文本范围进行迭代。然后,如果每个单词符合条件,它就会将格式应用于每个单词,如 Foreach 中所述。希望这有帮助。

P.S 考虑之后,可能不需要所有代码,因为我的实现也有跳转到行的功能,因此我将文档分成几行。祝你好运!

     //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;      
  }

使用此方法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);
     }
  }

我也找到了问题和解决方案。

问题: 当正则表达式在一行中找到所有匹配项时,没有颜色格式。但是当我将颜色格式添加到第一个匹配项时,它会移动文本,但正则表达式匹配结果仍然是旧位置。

解法: 始终仅更改第一场比赛。完成后,获取新的文本指针,它将返回彩色单词之前的文本。然后再把你填色的字还给你(必须跳过,因为两次填色)。而不是在你的彩色单词之后返回文本,其中包含该行中的其他特殊单词。

示例: 我想将单词着色为:<*word*>.

要着色的文本:"This is a <*test*> <*sentence*>."

  • 第一步:归还整行。正则表达式给出 2 个匹配项 (<*测试*>,<*句子*>)。给第一个上色。

  • 第二步:回馈:"This is a "。什么都不做

  • 第三步:回馈:“<*test*>”。正则表达式给出 1 个匹配项 (<*test*>)。跳过 它,因为它已经上色了。
  • 第四步:回馈:“”。什么都不做。
  • 第五步:回馈:“<*句子*>”。正则表达式给出 1 场比赛 (<*句子*>)。着色。
  • 第六步:回馈:“<*句子*>”。正则表达式给出 1 场比赛 (<*句子*>)。跳过它,因为它已经上色了。
  • 结束

    {
        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);
        }
    }