3

I have a richTextBox and a Regex with some words. Once, I find all the words I want to change their color to blue. I can use SelectionColor = Blue, but when it comes to coloring thousands of words it becomes quite slow.

After some search, I read that changing the RTF of the richTextBox is a faster way to change the text (e.g. it's size and/or color).

Here is my unfinished code:

MatchCollection matches = myRegex.Matches(richTextBox.text);
foreach (Match match in matches)
{
    richTextBox.Select(match.Index, match.Length);
    string addColor = @"{\colortbl ;\red0\green0\blue255;}" + Environment.NewLine;

    richTextBox.SelectionColor = Color.Blue; //Must be replaced
}

I also found out that in every case (in my case, the entire text uses the same font and has the same size, only the color of some words changes) the SelectedRtf is:

{\rtf1\ansi\deff0{\fonttbl{\f0\fnil\fcharset0 Consolas;}}
\uc1\pard\lang1033\f0\fs18 word} // richTextBox.SelectedRtf

Moreover, using the Selection.Color = Blue changes the SelectedRtf to:

{\rtf1\ansi\deff0{\fonttbl{\f0\fnil\fcharset0 Consolas;}}
{\colortbl ;\red0\green0\blue255;}  // The addColor string!
\uc1\pard\lang1033\f0\fs18 word}

To get the above string, I use this: richTextBox.SelectedRtf.Insert(59, addColor), so what I need to do is to replace SelectedRtf with that. However, after some attempts, nothing seems to happen. The color of the words remains the same. Any ideas?

OC_
  • 446
  • 6
  • 19
  • I don't know how to directly modify the RTF. As an alternative, however, consider using your old method but temporarily turn off updates with WM_SETREDRAW as in [this](http://stackoverflow.com/q/192413/2330053) question. So turn it off, use your loop, then turn it back on and refresh the RTB. – Idle_Mind Jun 06 '15 at 16:25
  • At least surrounding the code by a Suspend- & ResumeLayout did nothing.. – TaW Jun 06 '15 at 16:43

1 Answers1

2

Yes, it is possible and about twice as fast than the 'regular' way..:

Changing 30k words in a 3M text takes 28 seconds over 60 seconds before..

Here are the steps I would recommend, assuming your words are identifyable in the richTextBox.Rtf (*):

  • You could create your own color table, but it seems safer to let the system do it for you: I cheat by coloring the 1st letter before and resetting it after coloring the matches..

  • I pre- and postfix the search word by the rtf code for a foreground color index into the table. My code assumes that there is only one extra color in addition to the default one.

  • If you have more you should keep track and/ or analyze the colortable..

Here is a RTF reference, btw..

I do the replacement with the RegEx in my RichTextBox RTB like this:

string search "find me!";

RTB.SelectionStart = 0;
RTB.SelectionLength = 1;
RTB.SelectionColor = Color.HotPink;

Regex RX = new Regex(search);
MatchCollection matches = RX.Matches(RTB.Rtf);

RTB.Rtf = RX.Replace(RTB.Rtf, "\\cf1 " + search + "\\cf0 ");

RTB.SelectionStart = 0;
RTB.SelectionLength = 1;
RTB.SelectionColor = RTB.ForeColor;

(*) Note that modifying the Rtf property like this assumes that your search texts are identifiable in the Rtf. You can and should check this by comparing the matches count when searching the Rtf and the Text ! when they don't agree you probably need to use the 'regular' way..

Note that this only deals with Colors. For Font sizes etc you will have to add \fn (which index into the stylesheet) commands in a similar way..

Update: I have wrapped the code above in an expanded function, also taking care of more colors, word boundaries and some checks..:

enter image description here

int colorWords(RichTextBox RTB, String searchWord, Color color)
{
    string wordChar = @"\w*"; // or  @"\b*" for stricter search
    Regex RX = new Regex(wordChar + searchWord + wordChar);

    RTB.SelectionStart = 0;
    RTB.SelectionLength = 0;
    RTB.SelectedText = "~";  // insert a dummy character
    RTB.SelectionStart = 0;
    RTB.SelectionLength = 1;
    RTB.SelectionColor = color;  // and color it

    MatchCollection matches = null;
    matches = RX.Matches(RTB.Text);
    int textCount = matches.Count;
    matches = RX.Matches(RTB.Rtf);
    // we should not find more in the rtf code, less is ok
    if (textCount < matches.Count) return -1;
    if (matches.Count <= 0) return 0;

    List<Color> colors = getRtfColorTable(RTB);
    int cIndex = 1;
    Color cRGB = Color.FromArgb(255, color);
    if (colors.Contains(cRGB) )
        cIndex = colors.FindIndex(x => x == cRGB) + 1;

    RTB.Rtf = RX.Replace(RTB.Rtf, "\\cf" + cIndex + " " + searchWord + "\\cf0 ");

    RTB.SelectionStart = 0;
    RTB.SelectionLength = 1;
    RTB.Cut();                 // remove the dummy
    return matches.Count;
}

Here is a function that pulls out the current colors from the Rtf color table. (Hopefully, the full spec is not exactly very small and tackling it with two simple IndexOf is a little optimistic.. ;-)

List<Color> getRtfColorTable(RichTextBox RTB)
{   // \red255\green0\blue0;
    List<Color> colors = new List<Color>();
    string tabString = @"\colortbl ;"; 
    int ct0 = RTB.Rtf.IndexOf(tabString);
    if (ct0 >= 0)
    {
        ct0 += tabString.Length;
        int ct1 = RTB.Rtf.IndexOf(@"}", ct0);
        var table = RTB.Rtf.Substring(ct0, ct1 - ct0).Split(';');
        foreach(string t in table)
        {
            var ch = t.Split('\\');
            if (ch.Length == 4)
            {
                int r = Convert.ToInt16(ch[1].Replace("red", ""));
                int g = Convert.ToInt16(ch[2].Replace("green", ""));
                int b = Convert.ToInt16(ch[3].Replace("blue", ""));

                colors.Add(Color.FromArgb(255, r, g, b));
            }
        }
    }
    return colors;
}

The example was called like this:

colorWords(RTB, "<DIR>", Color.SaddleBrown);
colorWords(RTB, "Verzeichnis", Color.BlueViolet);
colorWords(RTB, "2012", Color.OrangeRed);
TaW
  • 53,122
  • 8
  • 69
  • 111
  • Hm, gave it a try last night, but it didn't seem to work. I probably did something wrong... I'll let you once I get this to work. If it really is twice as fast as you say, then it's exactly what I'm looking for! – OC_ Jun 07 '15 at 14:10
  • Actually, forget what I just said. I managed to make it work for one color & one word. Next step is same color for multiple words. I think I'll just put that code in a loop and replace the value of the `search` string with the word that I'm looking for... – OC_ Jun 07 '15 at 14:30
  • 1
    Yes, that's the way. I have wrapped the code in a function you may find useful.. Yet another update, inserting a dummy character to handle the case where the first match is a the start.... – TaW Jun 07 '15 at 16:27
  • 1
    Sorry for the late reply, I've quite busy,but I finally tested it out for multiple words and colors and it works pretty well. Not only you gave an excellent answer, but you even helped me with making it work with several colors. Thank you for your time. – OC_ Jun 08 '15 at 18:00