文章

WPF使用TextBlock实现查找结果高亮显示

WPF使用TextBlock实现查找结果高亮显示

在应用开发过程中,经常遇到这样的需求:通过关键字查找数据,把带有关键字的数据显示出来,同时在结果中高亮显示关键字。在web开发中,只需在关键字上加一层标签,然后设置标签样式就可以轻松实现。

在WPF中显示文本内容通常采用TextBlock控件,也可以采用类似的方式,通过内联流内容元素Run达到同样的效果:

1
2
3
<TextBlock FontSize="20">
    <Run Text="Hel" /><Run Foreground="Red" Text="lo " /><Run Text="Word" />
</TextBlock>

需要注意的是每个Run之间不要换行,如果换行的话,每个Run之间会有间隙,看起来像增加了空格。

通过这种方式实现查找结果中高亮关键字,需要把查找结果拆分成三部分,然后绑定到Run元素的Text属性,或者在后台代码中使用TextBlockInlines属性添加Run元素

1
2
3
textBlock1.Inlines.Add(new Run("hel"));
textBlock1.Inlines.Add(new Run("lo ") { Foreground=new SolidColorBrush(Colors.Red)});
textBlock1.Inlines.Add(new Run("world"));

这种方法虽然可以达到效果,但显然与MVVM的思想不符。接下来本文介绍一种通过附加属性实现TextBlock中指定内容高亮。
TextblockHighlight

技术要点与实现

通过TextEffectPositionStartPositionCount以及Foreground属性设置字符串中需要高亮内容的起始位置、长度以及高亮颜色。定义附加属性允许TextBlock设置需要高亮的内容位置以及颜色。

  • 首先定义类ColoredLettering(并不要求继承DependencyObject)。
  • ColoredLettering中注册自定义的附加属性,注册附加属性方式与注册依赖属性类似,不过附加属性是用DependencyProperty.RegisterAttached来注册。
  • 给附加属性注册属性值变化事件,事件处理逻辑中设置TextEffectPositionStartPositionCount以及Foreground实现内容高亮。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    
    public class ColoredLettering
    {
      public static void SetColorStart(TextBlock textElement, int value)
      {
          textElement.SetValue(ColorStartProperty, value);
      }
    
      public static int GetColorStart(TextBlock textElement)
      {
          return (int)textElement.GetValue(ColorStartProperty);
      }
    
      // Using a DependencyProperty as the backing store for ColorStart.  This enables animation, styling, binding, etc...
      public static readonly DependencyProperty ColorStartProperty =
          DependencyProperty.RegisterAttached("ColorStart", typeof(int), typeof(ColoredLettering), new FrameworkPropertyMetadata(0, OnColorStartChanged));
    
      private static void OnColorStartChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
          TextBlock textBlock = d as TextBlock;
          if (textBlock != null)
          {
              if (e.NewValue == e.OldValue) return;
                  if (e.NewValue is int)
                  {
                      int count = GetColorLength(textBlock);
                      Brush brush = GetForeColor(textBlock);
                      if ((int)e.NewValue <= 0 || count <= 0 || brush == TextBlock.ForegroundProperty.DefaultMetadata.DefaultValue) return;
                      if (textBlock.TextEffects.Count != 0)
                      {
                          textBlock.TextEffects.Clear();
                      }
                      TextEffect textEffect = new TextEffect()
                      {
                          Foreground = brush,
                          PositionStart = (int)e.NewValue,
                          PositionCount = count
                      };
                      textBlock.TextEffects.Add(textEffect);
                  }
          }
      }
    
      public static void SetColorLength(TextBlock textElement, int value)
      {
          textElement.SetValue(ColorLengthProperty, value);
      }
    
      public static int GetColorLength(TextBlock textElement)
      {
          return (int)textElement.GetValue(ColorLengthProperty);
      }
    
      // Using a DependencyProperty as the backing store for ColorStart.  This enables animation, styling, binding, etc...
      public static readonly DependencyProperty ColorLengthProperty =
          DependencyProperty.RegisterAttached("ColorLength", typeof(int), typeof(ColoredLettering), new FrameworkPropertyMetadata(0, OnColorLengthChanged));
    
      private static void OnColorLengthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
          TextBlock textBlock = d as TextBlock;
              if (textBlock != null)
              {
                  if (e.NewValue == e.OldValue) return;
                  if (e.NewValue is int)
                  {
                      int start = GetColorStart(textBlock);
                      Brush brush = GetForeColor(textBlock);
                      if ((int)e.NewValue <= 0 || start <= 0 || brush == TextBlock.ForegroundProperty.DefaultMetadata.DefaultValue) return;
                      if (textBlock.TextEffects.Count != 0)
                      {
                          textBlock.TextEffects.Clear();
                      }
                      TextEffect textEffect = new TextEffect()
                      {
                          Foreground = brush,
                          PositionStart = start,
                          PositionCount = (int)e.NewValue
                      };
                      textBlock.TextEffects.Add(textEffect);
                  }
              }
      }
    
      public static void SetForeColor(TextBlock textElement, Brush value)
      {
          textElement.SetValue(ColorStartProperty, value);
      }
    
      public static Brush GetForeColor(TextBlock textElement)
      {
          return (Brush)textElement.GetValue(ForeColorProperty);
      }
    
      // Using a DependencyProperty as the backing store for ForeColor.  This enables animation, styling, binding, etc...
      public static readonly DependencyProperty ForeColorProperty =
          DependencyProperty.RegisterAttached("ForeColor", typeof(Brush), typeof(ColoredLettering), new PropertyMetadata(TextBlock.ForegroundProperty.DefaultMetadata.DefaultValue, OnForeColorChanged));
    
      private static void OnForeColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
          TextBlock textBlock = d as TextBlock;
          if (textBlock != null)
          {
              if (e.NewValue == e.OldValue) return;
              if (e.NewValue is Brush)
              {
                  int start = GetColorStart(textBlock);
                  int count = GetColorLength(textBlock);
                  if (start <= 0 || count <= 0) return;
                  if (textBlock.TextEffects.Count != 0)
                  {
                      textBlock.TextEffects.Clear();
                  }
                  TextEffect textEffect = new TextEffect()
                  {
                      Foreground = (Brush)e.NewValue,
                      PositionStart = start,
                      PositionCount = count
                  };
                  textBlock.TextEffects.Add(textEffect);
              }
          }
      }
    }
    

    调用时只需在TextBlock指定需要高亮内容的开始位置,内容长度以及高亮颜色即可。
    ```

```

总结

本文介绍的方法只是高亮第一个匹配到的关键字,如果需要高亮匹配到的所有内容,只需要对附加属性进行改造,以支持传入一组位置和颜色信息。
最后分享一个可以解析一组有限的HTML标记并显示它们的WPF控件HtmlTextBlock ,通过这个控件也可以实现查找结果中高亮关键字,甚至支持指定内容触发事件做一些逻辑操作。

本文由作者按照 CC BY 4.0 进行授权