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