Changing color of text in a WPF RichTextBox from a different thread

4.1k Views Asked by At

This is probably a really easy question, so sorry in advance if there are duplicates, but I haven't found anything about this specific problem yet.

I have a problem with coloring specific parts of text in a RichTextBox from a different thread. I want to append a line of text to the RichTextBox and at the same time color that line with a specified color. I was able to append text and color it, but haven't been able to do so from another thread, although I was able to append plain text from the Dispatcher.

Here is my code:

private void UpdateTextbox(string message, Brush color)
{
    rtbOutput.Dispatcher.Invoke((Action)(() =>
        {
            rtbOutput.Selection.Select(rtbOutput.Document.ContentEnd, rtbOutput.Document.ContentEnd);
            rtbOutput.Selection.Text = message;
            rtbOutput.Selection.ApplyPropertyValue(TextElement.ForegroundProperty, color);
        })
    );
}

When I try running it from another thread I get an error message telling me the object can't be accessed, because it belongs to another thread. How do I solve this?

Edit: Seems like the RichTextBox isn't the problem, but the Brush object is, because I can change the color just fine if I specify it inside the Invoke method, but not if I pass it as a parameter.

3

There are 3 best solutions below

4
On BEST ANSWER

You need to push the actual change work back on to the GUI thread. During the startup of your application, you can run:

SynchronizationContext ctxt = SynchronizationContext.Current; and then pass that somewhere your multithreaded code can get at it. Then to use it:

//Code that determines the color to use

Color clr = result;

ctxt.Send((clr_obj) =>
{
//Code to set color
}, clr);

Edit:

Here is a nice article that explains SyncronizationContext much more in depth: Article

3
On

You're on the right track using Dispatcher, but you don't always need to call Invoke. I use the following extension method to handle this type of thing:

    public static void InvokeIfNeeded(this Dispatcher dispatcher, Action action)
    {
        if (dispatcher == null || dispatcher.CheckAccess())
            action();
        else
            dispatcher.Invoke(action, DispatcherPriority.Normal);
    }

You would use it as follows:

private void UpdateTextbox(string message, Brush Color)
{
    rtbOutput.Dispatcher.InvokeIfNeeded(() =>
        {
            rtbOutput.Selection.Select(rtbOutput.Document.ContentEnd, rtbOutput.Document.ContentEnd);
            rtbOutput.Selection.Text = message;
            rtbOutput.Selection.ApplyPropertyValue(TextElement.ForegroundProperty, Color);
        }
    );
}
0
On

As it turns out, it wasn't the RichTextBox that was owned by another thread, but the Brush object. I managed to get it to work with all the solutions down below, if I just pass a Color object to the method instead and convert it to a SolidColorBrush inside the Dispatcher.

private void UpdateTextbox(string message, Color color)
{
    rtbOutput.Dispatcher.Invoke((Action)(() =>
        {
            TextRange range = new TextRange(rtbOutput.Document.ContentEnd, rtbOutput.Document.ContentEnd);
            range.Text = message;
            range.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush((Color)color));
        })
    );
}

It also works with this code suggested by pquest:

private void UpdateTextbox(string message, Brush color)
{
    uiContext.Send(o =>
        {
            TextRange range = new TextRange(rtbOutput.Document.ContentEnd, rtbOutput.Document.ContentEnd);
            range.Text = message;
            range.ApplyPropertyValue(TextElement.ForegroundProperty, color);
        }, null);
}