[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用

释放双眼,带上耳机,听听看~!

1. 强化高亮的功能

上一篇文章介绍了使用附加属性实现TextBlock的高亮功能,但也留下了问题:不能定义高亮(或者低亮)的颜色。为了解决这个问题,我创建了TextBlockHighlightSource这个类,比单纯的字符串存储更多的信息,这个类的定义如下:

[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用

相应地,附加属性的类型也改变为这个类,并且属性值改变事件改成这样:

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>

[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用

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

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

双针针技巧汇总

2020-11-9 5:33:47

随笔日记

我的第一本书,被选作大学教材了!

2020-11-9 5:33:49

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索