欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-09 13:45:56
1. 强化高亮的功能 "上一篇文章" 介绍了使用附加属性实现TextBlock的高亮功能,但也留下了问题:不能定义高亮(或者低亮)的颜色。为了解决这个问题,我创建了 这个类,比单纯的字符串存储更多的信息,这个类的定义如下: 相应地,附加属性的类型也改变为这个类,并且属性值改变事件改成这样: 的关键代 ......

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