Search code examples
wpfstylestextblock

Changing the colors of substrings within the bound Text of a TextBlock


I am binding some property into my TextBlock:

<TextBlock 
    Text="{Binding Status}" 
    Foreground="{Binding RealTimeStatus,Converter={my:RealTimeStatusToColorConverter}}" 
    />

Status is simple text and RealTimeStatus is enum. For each enum value I am changing my TextBlock Foreground color.

Sometimes my Status message contains numbers. That message gets the appropriate color according to the enum value, but I wonder if I can change the colors of the numbers inside this message, so the numbers will get different color from the rest of the text.

Edit.

XAML

<TextBlock my:TextBlockExt.XAMLText="{Binding Status, Converter={my:RealTimeStatusToColorConverter}}"/>

Converter:

public class RealTimeStatusToColorConverter : MarkupExtension, IValueConverter
{
    // One way converter from enum RealTimeStatus to color. 
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value is RealTimeStatus && targetType == typeof(Brush))
        {
            switch ((RealTimeStatus)value)
            {
                case RealTimeStatus.Cancel:
                case RealTimeStatus.Stopped:
                    return Brushes.Red;

                case RealTimeStatus.Done:
                    return Brushes.White;

                case RealTimeStatus.PacketDelay:
                    return Brushes.Salmon;

                default:
                    break;
            }
        }

        return null;
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public RealTimeStatusToColorConverter()
    {
    }

    // MarkupExtension implementation
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

Solution

  • Here's an attached property which parses arbitrary text as XAML TextBlock content, including Run, Span, Bold, etc. This has the advantage of being generally useful.

    I recommend you write a ValueConverter which replaces the numbers in your Status text with appropriate markup, such that when you give it this text...

    Error number 34: No custard for monkey kitty.

    ...it would convert that into this text:

    Error number <Span Foreground="Red">34</Span>: No custard for monkey kitty.

    You already know how to do value converters, and text substitution with regular expressions is a different subject entirely.

    XAML usage:

    <TextBlock
        soex:TextBlockExt.XAMLText={Binding Status, Converter={my:redNumberConverter}}"
        />
    

    If it were me I'd go hog wild and make the color a ConverterParameter.

    Here's the C# for that attached property:

    using System;
    using System.IO;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace StackOverflow.Examples
    {
        public static class TextBlockExt
        {
            public static String GetXAMLText(TextBlock obj)
            {
                return (String)obj.GetValue(XAMLTextProperty);
            }
    
            public static void SetXAMLText(TextBlock obj, String value)
            {
                obj.SetValue(XAMLTextProperty, value);
            }
    
            /// <summary>
            /// Convert raw string from ViewModel into formatted text in a TextBlock: 
            /// 
            /// @"This <Bold>is a test <Italic>of the</Italic></Bold> text."
            /// 
            /// Text will be parsed as XAML TextBlock content. 
            /// 
            /// See WPF TextBlock documentation for full formatting. It supports spans and all kinds of things. 
            /// 
            /// </summary>
            public static readonly DependencyProperty XAMLTextProperty =
                DependencyProperty.RegisterAttached("XAMLText", typeof(String), typeof(TextBlockExt),
                                                     new PropertyMetadata("", XAMLText_PropertyChanged));
    
            private static void XAMLText_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                if (d is TextBlock)
                {
                    var ctl = d as TextBlock;
    
                    try
                    {
                        //  XAML needs a containing tag with a default namespace. We're parsing 
                        //  TextBlock content, so make the parent a TextBlock to keep the schema happy. 
                        //  TODO: If you want any content not in the default schema, you're out of luck. 
                        var strText = String.Format(@"<TextBlock xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">{0}</TextBlock>", e.NewValue);
    
                        TextBlock parsedContent = System.Windows.Markup.XamlReader.Load(GenerateStreamFromString(strText)) as TextBlock;
    
                        //  The Inlines collection contains the structured XAML content of a TextBlock
                        ctl.Inlines.Clear();
    
                        //  UI elements are removed from the source collection when the new parent 
                        //  acquires them, so pass in a copy of the collection to iterate over. 
                        ctl.Inlines.AddRange(parsedContent.Inlines.ToList());
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Trace.WriteLine(String.Format("Error in Ability.CAPS.WPF.UIExtensions.TextBlock.XAMLText_PropertyChanged: {0}", ex.Message));
                        throw;
                    }
                }
            }
    
            public static Stream GenerateStreamFromString(string s)
            {
                MemoryStream stream = new MemoryStream();
                StreamWriter writer = new StreamWriter(stream);
                writer.Write(s);
                writer.Flush();
                stream.Position = 0;
                return stream;
            }
        }
    }