WPF使用TextBlock實現查詢結果高亮顯示

2023-08-28 12:01:33

在應用開發過程中,經常遇到這樣的需求:通過關鍵字查詢資料,把帶有關鍵字的資料顯示出來,同時在結果中高亮顯示關鍵字。在web開發中,只需在關鍵字上加一層標籤,然後設定標籤樣式就可以輕鬆實現。

在WPF中顯示文字內容通常採用TextBlock控制元件,也可以採用類似的方式,通過內聯流內容元素Run達到同樣的效果:

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

需要注意的是每個Run之間不要換行,如果換行的話,每個Run之間會有間隙,看起來像增加了空格。

通過這種方式實現查詢結果中高亮關鍵字,需要把查詢結果拆分成三部分,然後繫結到Run元素的Text屬性,或者在後臺程式碼中使用TextBlockInlines屬性新增Run元素

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中指定內容高亮。

技術要點與實現

通過TextEffectPositionStartPositionCount以及Foreground屬性設定字串中需要高亮內容的起始位置、長度以及高亮顏色。定義附加屬性允許TextBlock設定需要高亮的內容位置以及顏色。

  • 首先定義類ColoredLettering(並不要求繼承DependencyObject)。
  • ColoredLettering中註冊自定義的附加屬性,註冊附加屬性方式與註冊依賴屬性類似,不過附加屬性是用DependencyProperty.RegisterAttached來註冊。
  • 給附加屬性註冊屬性值變化事件,事件處理邏輯中設定TextEffectPositionStartPositionCount以及Foreground實現內容高亮。
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指定需要高亮內容的開始位置,內容長度以及高亮顏色即可。

<TextBlock local:ColoredLettering.ColorLength="{Binding Count}"
           local:ColoredLettering.ColorStart="{Binding Start}"
           local:ColoredLettering.ForeColor="{Binding ForeColor}"
           FontSize="20"
           Text="Hello World" />

總結

本文介紹的方法只是高亮第一個匹配到的關鍵字,如果需要高亮匹配到的所有內容,只需要對附加屬性進行改造,以支援傳入一組位置和顏色資訊。
最後分享一個可以解析一組有限的HTML標記並顯示它們的WPF控制元件HtmlTextBlock ,通過這個控制元件也可以實現查詢結果中高亮關鍵字,甚至支援指定內容觸發事件做一些邏輯操作。