2

I am having an issue retaining multi-line color in a RichTextBox control.

Issue: When appending new messages to the RichTextBox control and the message needs to be painted a certain color, all messages prior to the new message are painted a gray color and remain that way.

Question: How do I only paint the messages that need to be painted, when they are appended.

I have a status update event fired from a background thread that adds the status and message for that update to a List<StatusEventArgs>. Then, I clear my RichTextBox control and begin adding the updates to it. I should probably move this to just append the text instead of clearing it every time and rebuilding it; however, I did this in an attempt to retain color. The messages are a form of log that is represented to the user so they know what is currently happening during this long running process. For example:

1. Running process.
2. Starting something.
3. Doing something else.
4. Doing something. SUCCESS.
5. Doing something. SUCCESS.
6. Doing something. SUCCESS.
7. Doing something. FAIL. Attempting to continue.
8. Doing some other thing.
9. Complete.

In the example above, lines 4 - 7 have additional appended text such as SUCCESS. The beginning of these lines should be the normal Color.Black color for the RichTextBox control. The appended message SUCCESS should be colored based on the Status supplied for that message. So for example, lines 4 and 6 say SUCCESS and were received with a successful status. Line 5 says success but encountered a warning or minor issue and was received with a warning status. Line 7 was received with an error status. As you will see in the code supplied below, each status has it's own color associated with it. The text prior to that status on the same line should remain the Color.Black color.

Important Note

The message and its status are sent separately, this is due to each operation taking different amounts of time, thus the user should know that a process is happening and so far there hasn't been a status report from that process.

Current Code

private List<StatusEventArgs> events = new List<StatusEventArgs>();
private void StatusUpdate(object sender, StatusEventArgs e) {
    events.Add(e);
    rtb.Text = string.Empty;

    foreach (StatusEventArgs sea in events) {
        Status s = sea.Status;
        rtb.SelectionStart = rtbConsole.TextLength;
        rtb.SelectionLength = 0;
        rtb.SelectionColor = rtbConsole.ForeColor;

        if (s == UpdateStatus.Error || s == UpdateStatus.Warning || s == UpdateStatus.Success) {
            rtb.Text = rtb.Text.TrimEnd('\n');
            rtb.SelectionStart = rtbConsole.TextLength;
            rtb.SelectionLength = 0;

            switch (s) {
                case Status.Error: rtb.SelectionColor = Color.DarkRed; break;
                case Status.Warning: rtb.SelectionColor = Color.DarkGoldenrod; break;
                case Status.Success: rtb.SelectionColor = Color.DarkGreen; break;
            }
        }
        rtb.AppendText($"{sea.Message}\n");
    }
    rtb.ScrollToCaret();
}

Dependencies

public enum UpdateStatus { NoOperation, Error, Warning, Success }
public class StatusEventArgs {
    public UpdateStatus Status { get; set; }
    public string Message { get; set; }
}

I have looked at several of the related questions here on StackOverflow and (this one) is the closest to what I need; however, the post recommends selecting text in a specific range, which still did not work. It did the exact same thing my current code does. Keep in mind, I did not have the loop or list of events when I implemented the answer. I also cant utilize the recommendation to paint specific text in the RichTextBox because as I stated in the example above, the SUCCESS message can have two different colors depending on if warnings are received or not.

Attempt Code

int previousLength = rtb.TextLength;
UpdateStatus s = e.Status;
bool status = s == UpdateStatus.Error || s == UpdateStatus.Warning || s == UpdateStatus.Success;
if (status)
    rtb.Text = rtb.Text.TrimEnd('\n');
rtb.AppendText($"{e.Message}\n");
rtb.ScrollToCaret();

if (status) {
    rtb.Select(previousLength, rtb.TextLength);
    switch (s) {
        case Status.Error: rtb.SelectionColor = Color.DarkRed; break;
        case Status.Warning: rtb.SelectionColor = Color.DarkGoldenrod; break;
        case Status.Success: rtb.SelectionColor = Color.DarkGreen; break;
    }
    rtb.Select(0, 0);
}

I honestly feel like I am just missing a step or needing to do something just a little different. Recoloring all status messages each time a new status message is received seems like a bit much for such a trivial task.

Update

I tested the Dictionary<int[], UpdateStatus> method and it works the way I need it to; however, I believe this is quite over the top for something so simple:

private Dictionary<int[], UpdateStatus> selections = new Dictionary<int[], UpdateStatus>();
private void StatusUpdate(object sender, StatusEventArgs e) {
    int previousLength = rtbConsole.TextLength;
    UpdateStatus s = e.Status;
    bool status = s != UpdateStatus.NoOperation;
    if (status)
        rtb.Text = rtb.Text.TrimEnd('\n');
    rtb.AppendText($"{e.Message}\n");
    rtb.ScrollToCaret();
    if (status)
        selections.Add(new int[] { previousLength, rtb.TextLength }, s);

    // Set all basic text to black.
    rtb.Select(0, rtb.TextLength);
    rtb.SelectionColor = Color.Black;

    // Color all status messages.
    foreach (int[] selection in selections.Keys) {
        rtb.Select(selection[0], selection[1]);
        switch (selections[selection])
            case Status.Error: rtb.SelectionColor = Color.DarkRed; break;
            case Status.Warning: rtb.SelectionColor = Color.DarkGoldenrod; break;
            case Status.Success: rtb.SelectionColor = Color.DarkGreen; break;
        }

        // Prevent messages in-between status messages from being colored.
        rtb.Select(selection[1], rtb.TextLength);
        rtb.SelectionColor = Color.Black;
    }
}

Update 2

This is my implementation of LarsTech's post below. It still paints everything prior to the current status message black:

UpdateStatus s = e.Status;
if (s != UpdateStatus.NoOperation)
    rtb.Text = rtb.Text.TrimEnd('\n');
Color textColor = Color.Black;
switch (selections[selection]) {
    case Status.Error: textColor = Color.DarkRed; break;
    case Status.Warning: textColor = Color.DarkGoldenrod; break;
    case Status.Success: textColor = Color.DarkGreen; break;
}
rtb.Select(rtb.TextLength, 0);
rtb.SelectionColor = textColor;
rtb.AppendText($"{e.Message}\n");
rtb.ScrollToCaret();

Update 3

So the Update 2 method above works, the issue is the following line of code:

rtb.Text = rtb.Text.TrimEnd('\n');

This line of code causes the entire control to remove all current formatting Which makes me wonder, if I wanted to keep the formatting I already had, should I use the Rtf property? I suppose I'll try that and find out. Per MSDN:

The Text property does not return any information about the formatting applied to the contents of the RichTextBox. To get the rich text formatting (RTF) codes, use the Rtf property. The amount of text that can be entered in the RichTextBox control is limited only by available system memory.

Final Update

The Rtf property did not work in my case (simply swapped rtb.Text to rtb.Rtf. Tried a couple other ways but none worked. However, for those who (like me) are passing in a straight message and appending a new line as you print it, you can take the approach of prefixed new line. Then you can add some logic to prevent it when it shouldn't be there. This removes the need for TrimEnd and thus the accepted answer will work just fine:

// Field
bool firstUpdate = true;

private void StatusUpdate(...) {
    UpdateStatus s = e.Status;

    Color textColor = Color.Black;
    switch (selections[selection]) {
        case Status.Error: textColor = Color.DarkRed; break;
        case Status.Warning: textColor = Color.DarkGoldenrod; break;
        case Status.Success: textColor = Color.DarkGreen; break;
    }
    string newline = firstUpdate || s != Status.NoOperation ? string.Empty : "\n";
    rtb.Select(rtb.TextLength, 0);
    rtb.SelectionColor = textColor;
    rtb.AppendText($"{newline}{e.Message}");
    rtb.ScrollToCaret();

    if (firstUpdate)
        firstUpdate = false;
}
Hazel へいぜる
  • 2,751
  • 1
  • 12
  • 44

1 Answers1

1

The issue is caused by replacing the string in the Text property:

rtb.Text = rtb.Text.TrimEnd('\n');

This line of code causes the entire control to remove all current formatting Per MSDN:

The Text property does not return any information about the formatting applied to the contents of the RichTextBox. To get the rich text formatting (RTF) codes, use the Rtf property. The amount of text that can be entered in the RichTextBox control is limited only by available system memory.

Otherwise, the code can be simplified to just:

Color textColor = Color.Black;
switch (e.Status) {
    case Status.Error: textColor = Color.DarkRed; break;
    case Status.Warning: textColor = Color.DarkGoldenrod; break;
    case Status.Success: textColor = Color.DarkGreen; break;
}
rtb.Select(rtb.TextLength, 0);
rtb.SelectionColor = textColor;
rtb.AppendText($"{e.Message}\n");
rtb.ScrollToCaret();
LarsTech
  • 80,625
  • 14
  • 153
  • 225
  • So I wish I could say that worked; however, it did not. I removed all of my old code and refactored to bare essentials and your implementation. This does the same as the code in my question just in a much shorter version. I'll be updating my question with the `Dictionary` implementation that works, but definitely seems over the top. – Hazel へいぜる Jul 26 '18 at 19:03
  • @davisj1691 I would kill that dictionary. You are just appending status messages to a RichTextBox, so you don't need to clear the box and reformat everything every time. Your class has the message and the status — that's all you need. When a new message pops up, use this code to update the RichTextBox. – LarsTech Jul 26 '18 at 19:19
  • As I stated previously, using the `Dictionary` is going to be my last resort; however, the code you supplied paints everything prior to the message being appended. Might I just be missing something about the implementation? I essentially replaced what I had with what you supplied to no avail. – Hazel へいぜる Jul 26 '18 at 19:46
  • @davisj1691 My code is for adding one message at a time. Don't clear the box. Don't loop through previous messages. – LarsTech Jul 26 '18 at 19:48
  • I will edit my post with my implementation of your code. Maybe that will clarify things. As I stated, it is still coloring everything prior to the status message black. – Hazel へいぜる Jul 26 '18 at 19:51
  • @davisj1691 Boom, there it is. Don't do this: `rtb.Text = rtb.Text.TrimEnd('\n');` It erases ALL of the formatting when you replace the Text property. – LarsTech Jul 26 '18 at 20:04
  • So, that worked. I'll update my post to include that information and mark yours as the answer. – Hazel へいぜる Jul 26 '18 at 20:17