Search code examples
wpftextcolorstextboxbackground-color

WPF TextBox change background color of text and not whole TextBox


I would like to know if there is a way to change the color of the background of the text inside a TextBox and not for the whole TextBox. Like when you highlight/select the text.


Solution

  • TextBox doesn't support coloured text or rich text formatting in general. Depending on your scenario, you would have to go with a TextBlock or RichtextBox.

    TextBlock

    You can either handle the text elements directly:

    <TextBlock>
      <Run Text="This is some" />
      <Run Text="red"
           Background="Red" />
      <Run Text="text." />
    </TextBlock>
    

    Or in case you need to find a particular text, handle the text pointers:

    <TextBlock x:Name="ColouredTextBlock" />
    
    private void OnLoaded(object sender, EventArgs e)
    {
      var text = "This is some red text.";
      var highlightText = "red";
    
      this.ColouredTextBlock.Text = text;
    
      int highlightTextIndex = this.ColouredTextbox.Text.IndexOf(highlightText);
      TextPointer textStartPointer = this.ColouredTextbox.ContentStart.DocumentStart.GetInsertionPosition(LogicalDirection.Forward);
      var highlightTextRange = new TextRange(textStartPointer.GetPositionAtOffset(highlightTextIndex), textStartPointer.GetPositionAtOffset(highlightTextIndex + highlightText.Length));
      highlightTextRange.ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Red);
    }
    

    To make highlighting dynamic you can use a MultiBinding to create the inline elements using a text-to-Inline converter. Since we can't bind directly to the TextBlock.Inlines property, we use the TextBlock.Text property as a dummy binding target:

    HighlightInfo.cs

    public struct HighlightInfo
    {
      /// <summary>
      ///  Set Range parameter: inclusive start index and exclusive end index
      /// </summary>
      /// <param name="highlightRange">inclusive start index and exclusive end index</param>
      public HighlightInfo(Range highlightRange, Color foreground, Color background)
      {
        HighlightRange = highlightRange;
        Foreground = foreground;
        Background = background;
      }
    
      public System.Range HighlightRange { get; }
      public int HighlightRangeLength => this.HighlightRange.End.Value - this.HighlightRange.Start.Value;
      public Color Foreground { get; }
      public Color Background { get; }
    }
    

    TextToInlineConverter.cs

    public class TextToInlineConverter : IMultiValueConverter
    {
      public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
      {
        string sourceText = values.OfType<string>().First();
        if (string.IsNullOrWhiteSpace(sourceText))
        {
          return Binding.DoNothing;
        }
    
        TextBlock textBlock = values.OfType<TextBlock>().First();
    
        IEnumerable<HighlightInfo> highlightInfos = values.OfType<IEnumerable<HighlightInfo>>().First();
        List<HighlightInfo> sortedHighlightInfos = highlightInfos
          .OrderBy(highlightInfo => highlightInfo.HighlightRange.Start.Value)
          .ToList();
    
        int highlightStartIndex = sortedHighlightInfos.FirstOrDefault().HighlightRange.Start.Value;
        bool hasPreccedingNonHighlightText = highlightStartIndex > 0;
        if (hasPreccedingNonHighlightText)
        {
          string preceedingText = sourceText.Substring(0, highlightStartIndex);
          textBlock.Inlines.Add(preceedingText);
        }
    
        CreateHighlightTextElements(sourceText, textBlock, sortedHighlightInfos);
    
        int highlightEndIndex = sortedHighlightInfos.LastOrDefault().HighlightRange.End.Value;
        bool hasTrailingNonHighlightText = highlightEndIndex < sourceText.Length;
        if (hasTrailingNonHighlightText)
        {
          string trailingText = sourceText.Substring(highlightEndIndex);
          textBlock.Inlines.Add(trailingText);
        }
    
        // We are binding to the 'TextBlock.Text' property as a dummy target, so we don't want to set it.
        // We have already modified the 'TextBlcok.Inlines' collection.
        return Binding.DoNothing;
      }
    
      private void CreateHighlightTextElements(string sourceText, TextBlock textBlock, List<HighlightInfo> sortedHighlightInfos)
      {
        for (int index = 0; index < sortedHighlightInfos.Count; index++)
        {
          HighlightInfo highlightInfo = sortedHighlightInfos[index];
          var highlightText = new Run(sourceText.Substring(highlightInfo.HighlightRange.Start.Value, highlightInfo.HighlightRangeLength))
          {
            Foreground = new SolidColorBrush(highlightInfo.Foreground),
            Background = new SolidColorBrush(highlightInfo.Background),
          };
          textBlock.Inlines.Add(highlightText);
          if (index + 1 < sortedHighlightInfos.Count)
          {
            HighlightInfo nextHighlightInfo = sortedHighlightInfos[index + 1];
            var nonHighlightTextRangeLength = nextHighlightInfo.HighlightRange.Start.Value - highlightInfo.HighlightRange.End.Value;
            bool hasEnclosedNonHighlightText = nonHighlightTextRangeLength > 0;
            if (hasEnclosedNonHighlightText)
            {
              highlightText = new Run(sourceText.Substring(highlightInfo.HighlightRange.End.Value, nonHighlightTextRangeLength));
              textBlock.Inlines.Add(highlightText);
            }
          }
        }
      }
    
      public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 
        => throw new NotSupportedException();
    }
    

    MainWindow.xaml.cs

    partial class MainWIndow : Window
    {
      public ObservableCollection<HighlightInfo> HighlightInfos { get; }
      public string TextValue { get; }
    
      public MainWindow()
      {
        InitializeComponent();
    
        this.TextValue = "This is some red and orange and bluegreen text.";
    
        this.HighlightInfos = new ObservableCollection<HighlightInfo>()
        {
          new HighlightInfo(new Range(
            this.TextValue.IndexOf("red"), 
            this.TextValue.IndexOf("red") + "red".Length), 
            Colors.Black, 
            Colors.Red),
          new HighlightInfo(new Range(
            this.TextValue.IndexOf("orange"), 
            this.TextValue.IndexOf("orange") + "orange".Length), 
            Colors.Black, 
            Colors.Orange),
          new HighlightInfo(new Range(
            this.TextValue.IndexOf("blue"), 
            this.TextValue.IndexOf("blue") + "blue".Length), 
            Colors.Black, 
            Colors.Blue),
          new HighlightInfo(new Range(
            this.TextValue.IndexOf("green"), 
            this.TextValue.IndexOf("green") + "green".Length),
            Colors.Black, 
            Colors.Green),
        };
      }
    }
    

    MainWindow.xaml

    <Window>
      <Window.Resources>
        <local:TextToInlineConverter x:Key="TextToInlineConverter" />
      </Window.Resources>
    
      <TextBlock>
        <TextBlock.Text>
          <MultiBinding  Converter="{StaticResource TextToInlineConverter}">
            <Binding Path="TextValue" />
            <Binding Path="HighlightInfos" />
            <Binding RelativeSource="{RelativeSource Self}" />
          </MultiBinding>
        </TextBlock.Text>
      </TextBlock>
    </Window>
    

    RichtextBox

    In case you need to allow user input, you would have to use a RichTextBox:

    <RichTextBlock x:Name="ColouredRichTextBox" />
    
    private void OnLoaded(object sender, EventArgs e)
    {
      var text = "This is some red text.";
      var highlightText = "red";
    
      this.ColouredRichTextBox.Document = new FlowDocument(new Paragraph(new Run(text)));
    
      TextPointer textStartPointer = this.ColouredRichTextBox.Document.ContentStart.DocumentStart.GetInsertionPosition(LogicalDirection.Forward);
      var highlightTextRange = new TextRange(textStartPointer.GetPositionAtOffset(highlightTextIndex), textStartPointer.GetPositionAtOffset(highlightTextIndex + highlightText.Length));
      highlightTextRange .ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.Red);
    }