1. 強化高亮的功能 "上一篇文章" 介紹了使用附加屬性實現TextBlock的高亮功能,但也留下了問題:不能定義高亮(或者低亮)的顏色。為瞭解決這個問題,我創建了 這個類,比單純的字元串存儲更多的信息,這個類的定義如下: 相應地,附加屬性的類型也改變為這個類,並且屬性值改變事件改成這樣: 的關鍵代 ...
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