[WPF自定義控件庫]使用TextBlockHighlightSource強化高亮的功能,以及使用TypeConverter簡化調用

1. 強化高亮的功能

上一篇文章介紹了使用附加屬性實現TextBlock的高亮功能,但也留下了問題:不能定義高亮(或者低亮)的顏色。為了解決這個問題,我創建了TextBlockHighlightSource這個類,比單純的字符串存儲更多的信息,這個類的定義如下:

相應地,附加屬性的類型也改變為這個類,並且屬性值改變事件改成這樣:

private static void OnHighlightTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    var oldValue = (TextBlockHighlightSource)args.OldValue;
    var newValue = (TextBlockHighlightSource)args.NewValue;
    if (oldValue == newValue)
        return;

    void OnPropertyChanged(object sender,EventArgs e)
    {
        if (obj is TextBlock target)
        {
            MarkHighlight(target, newValue);
        }
    };

    if(oldValue!=null)
        newValue.PropertyChanged -= OnPropertyChanged;

    if (newValue != null)
        newValue.PropertyChanged += OnPropertyChanged;

    OnPropertyChanged(null, null);
}

MarkHighlight的關鍵代碼修改為這樣:

if (highlightSource.LowlightForeground != null)
    run.Foreground = highlightSource.LowlightForeground;

if (highlightSource.HighlightForeground != null)
    run.Foreground = highlightSource.HighlightForeground;

if (highlightSource.HighlightBackground != null)
    run.Background = highlightSource.HighlightBackground;

使用起來就是這樣:

<TextBlock Text="Git hub"
           TextWrapping="Wrap">
    <kino:TextBlockService.HighlightText>
        <kino:TextBlockHighlightSource Text="hub"
                                       LowlightForeground="Black"
                                       HighlightBackground="#FFF37D33" />
    </kino:TextBlockService.HighlightText>
</TextBlock>

2. 使用TypeConverter簡化調用

TextBlockHighlightSource提供了很多功能,但和直接使用字符串比起來,創建一個TextBlockHighlightSource要複雜多。為了可以簡化調用可以使用自定義的TypeConverter

首先來了解一下TypeConverter的概念。XAML本質上是XML,其中的屬性內容全部都是字符串。如果對應屬性的類型是XAML內置類型(即Boolea,Char,String,Decimal,Single,Double,Int16,Int32,Int64,TimeSpan,Uri,Byte,Array等類型),XAML解析器直接將字符串轉換成對應值賦給屬性;對於其它類型,XAML解析器需做更多工作。

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>

如上面這段XAML中的”Auto”和”*”,XAML解析器將其分別解析成GridLength.Auto和new GridLength(1, GridUnitType.Star)再賦值給Height,它相當於這段代碼:

grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

為了完成這個工作,XAML解析器需要TypeConverter的協助。XAML解析器通過兩個步驟查找TypeConverter:
1. 檢查屬性聲明上的TypeConverterAttribute。
2. 如果屬性聲明中沒有TypeConverterAttribute,檢查類型聲明中的TypeConverterAttribute。

屬性聲明上TypeConverterAttribute的優先級高於類型聲明。如果以上兩步都找不到類型對應的TypeConverterAttribute,XAML解析器將會報錯:屬性”*”的值無效。找到TypeConverterAttribute指定的TypeConverter后,XAML解析器調用它的object ConvertFromString(string text)函數將字符串轉換成屬性的值。

WPF內置的TypeConverter十分十分多,但有時還是需要自定義TypeConverter,自定義TypeConverter的基本步驟如下:

  • 創建一個繼承自TypeConverter的類;
  • 重寫virtual bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType);
  • 重寫virtual bool CanConvertTo(ITypeDescriptorContext context, Type destinationType);
  • 重寫virtual object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value);
  • 重寫virtual object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType);
  • 使用TypeConverterAttribute 指示XAML解析器可用的TypeConverter;

到這裏我想TypeConverter的概念已經介紹得夠詳細了。回到本來話題,要簡化TextBlockHighlightSource的調用我創建了TextBlockHighlightSourceConverter這個類,它繼承自TypeConverter,裏面的關鍵代碼如下:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    if (sourceType == typeof(string))
    {
        return true;
    }

    return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    switch (value)
    {
        case null:
            throw GetConvertFromException(null);
        case string source:
            return new TextBlockHighlightSource { Text = value.ToString() };
    }

    return base.ConvertFrom(context, culture, value);
}

然後在TextBlockHighlightSource上使用TypeConverterAttribute:

[TypeConverter(typeof(TextBlockHighlightSourceConverter))]
public class TextBlockHighlightSource : FrameworkElement

這樣在XAML中TextBlockHighlightSource的調用方式就可以和使用字符串一樣簡單了。

<TextBlock Text="Github"
           kino:TextBlockService.HighlightText="hub" />

3. 使用Style

有沒有發現TextBlockHighlightSource繼承自FrameworkElement?這種奇特的寫法是為了讓TextBlockHighlightSource可以使用全局的Style。畢竟要在應用程序里統一Highlight的顏色還是全局樣式最好使,但作為附加屬性,TextBlockHighlightSource並不是VisualTree的一部分,它拿不到VisualTree上的Resources。最簡單的解決方案是讓TextBlockHighlightSource繼承自FrameworkElement,把它放到VisualTree里,用法如下:

<StackPanel>
    <FrameworkElement.Resources>
        <Style TargetType="kino:TextBlockHighlightSource">
            <Setter Property="LowlightForeground" Value="Blue"/>
        </Style>
    </FrameworkElement.Resources>
    <TextBox x:Name="FilterElement3"/>
    <kino:TextBlockHighlightSource Text="{Binding ElementName=FilterElement3,Path=Text}" 
                                   HighlightForeground="DarkBlue"
                                   HighlightBackground="Yellow"
                                   x:Name="TextBlockHighlightSource2"/>
    <TextBlock Text="A very powerful projector with special features for Internet usability, USB" 
               kino:TextBlockService.HighlightText="{Binding ElementName=TextBlockHighlightSource2}"
               TextWrapping="Wrap"/>
</StackPanel>

也許你會覺得這種寫法有些奇怪,畢竟我也覺得在View上放一個隱藏的元素真的很怪。其實在一萬二千年前微軟就已經有這種寫法,在DomainDataSource的文檔里就有用到:

<Grid x:Name="LayoutRoot" Background="White">  
    <Grid.RowDefinitions>
        <RowDefinition Height="25" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <riaControls:DomainDataSource x:Name="source" QueryName="GetProducts" AutoLoad="true">
        <riaControls:DomainDataSource.DomainContext>
            <domain:ProductDomainContext />
        </riaControls:DomainDataSource.DomainContext>   
        <riaControls:DomainDataSource.FilterDescriptors>
            <riaData:FilterDescriptorCollection LogicalOperator="And">
              <riaData:FilterDescriptor PropertyPath="Color" Operator="IsEqualTo" Value="Blue" />
              <riaData:FilterDescriptor PropertyPath="ListPrice" Operator="IsLessThanOrEqualTo">
                  <riaControls:ControlParameter 
                      ControlName="MaxPrice" 
                      PropertyName="SelectedItem.Content" 
                      RefreshEventName="SelectionChanged" />
              </riaData:FilterDescriptor>
            </riaData:FilterDescriptorCollection>
        </riaControls:DomainDataSource.FilterDescriptors>
    </riaControls:DomainDataSource>
    <ComboBox x:Name="MaxPrice" Grid.Row="0" Width="60" SelectedIndex="0">
        <ComboBoxItem Content="100" />
        <ComboBoxItem Content="500" />
        <ComboBoxItem Content="1000" />
    </ComboBox>
    <data:DataGrid Grid.Row="1" ItemsSource="{Binding Data, ElementName=source}" />
</Grid>

把DataSource放到View上這種做法可能是WinForm的祖傳家訓,結構可恥但有用。

4. 結語

寫這篇博客的時候我才發覺這個附加屬性還叫HighlightText好像不太好,但也懶得改了。

這篇文章介紹了使用TypeConverter簡化調用,以及繼承自FrameworkElement以便使用Style。

5. 參考

TypeConverter 類
TypeConverters 和 XAML
Type Converters for XAML Overview
TypeConverterAttribute Class
如何:實現類型轉換器

6. 源碼

TextBlock at master · DinoChan_Kino.Toolkit.Wpf

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

您可能也會喜歡…