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

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

程序员文章站 2022-07-05 10:36:59
1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用“素颜”的外观,然后再提供一些主题包。这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分人来说模仿原生的主题也比自己设计一套好看的UI容易得多。 WPF有以下几种原生 "主题" : |主题文件|桌面主题| | ......

1. 为什么选择aero2

除了以外观为卖点的控件库,wpf的控件库都默认使用“素颜”的外观,然后再提供一些主题包。这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分人来说模仿原生的主题也比自己设计一套好看的ui容易得多。

wpf有以下几种原生:

主题文件 桌面主题
classic.xaml windows xp 操作系统上的经典 windows 外观(windows 95、windows 98 和 windows 2000)。
luna.normalcolor.xaml windows xp 上的默认蓝色主题。
luna.homestead.xaml windows xp 上的橄榄色主题。
luna.metallic.xaml windows xp 上的银色主题。
royale.normalcolor.xaml windows xp media center edition 操作系统上的默认主题。
aero.normalcolor.xaml windows vista 操作系统上的默认主题。

win8之后wpf更新了aero2和aerolite两种主题,关于aero、aero2、aerolite具体可见这个网页。再之后微软就没有更新wpf主题了

如果不在代码中指定主题,wpf大概就是用这段代码确定主题,也就是说默认是aero,如果在win8或以上自动转为aero2:

_themename = themename.tostring();
_themename = path.getfilenamewithoutextension(_themename);

if(string.compare(_themename, "aero", stringcomparison.ordinalignorecase) == 0 && utilities.isoswindows8ornewer)
{
    _themename = "aero2";
}

由于我暂时不想兼容win7,而且我又不讨厌win8的风格,所以kino.toolkit.wpf直接选择了aero2作为控件库的主题。

2. aero2的设计

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

上面分别是aero2(左)和aero(右)的button在几种状态下的外观,从中可以看出aero2的设计是扁平化的风格,移除圆角、渐变等装饰性元素,以实用为目的。这样一来控件模板的结构更加简单(如button只有border和contentpresenter 两个元素),移除装饰性元素更节省空间,而且渐变在质量较差或阳光下很影响阅读,圆角则是占用更多空间而且在低分辨率下表现不好。

总的来说就是以实用为目的,尽量简单,减少装饰性元素。

3. 以button为例,谈谈aero2中的细节:尺寸、颜色、字体、动画

<style x:key="focusvisual">
    <setter property="control.template">
        <setter.value>
            <controltemplate>
                <rectangle margin="2" snapstodevicepixels="true" stroke="{dynamicresource {x:static systemcolors.controltextbrushkey}}" strokethickness="1" strokedasharray="1 2"/>
            </controltemplate>
        </setter.value>
    </setter>
</style>
<solidcolorbrush x:key="button.static.background" color="#ffdddddd"/>
<solidcolorbrush x:key="button.static.border" color="#ff707070"/>
<solidcolorbrush x:key="button.mouseover.background" color="#ffbee6fd"/>
<solidcolorbrush x:key="button.mouseover.border" color="#ff3c7fb1"/>
<solidcolorbrush x:key="button.pressed.background" color="#ffc4e5f6"/>
<solidcolorbrush x:key="button.pressed.border" color="#ff2c628b"/>
<solidcolorbrush x:key="button.disabled.background" color="#fff4f4f4"/>
<solidcolorbrush x:key="button.disabled.border" color="#ffadb2b5"/>
<solidcolorbrush x:key="button.disabled.foreground" color="#ff838383"/>
<style targettype="{x:type button}">
    <setter property="focusvisualstyle" value="{staticresource focusvisual}"/>
    <setter property="background" value="{staticresource button.static.background}"/>
    <setter property="borderbrush" value="{staticresource button.static.border}"/>
    <setter property="foreground" value="{dynamicresource {x:static systemcolors.controltextbrushkey}}"/>
    <setter property="borderthickness" value="1"/>
    <setter property="horizontalcontentalignment" value="center"/>
    <setter property="verticalcontentalignment" value="center"/>
    <setter property="padding" value="1"/>
    <setter property="template">
        <setter.value>
            <controltemplate targettype="{x:type button}">
                <border x:name="border" borderbrush="{templatebinding borderbrush}" borderthickness="{templatebinding borderthickness}" background="{templatebinding background}" snapstodevicepixels="true">
                    <contentpresenter x:name="contentpresenter" focusable="false" horizontalalignment="{templatebinding horizontalcontentalignment}" 
                                      margin="{templatebinding padding}" recognizesaccesskey="true" snapstodevicepixels="{templatebinding snapstodevicepixels}" verticalalignment="{templatebinding verticalcontentalignment}"/>
                </border>
                <controltemplate.triggers>
                    <trigger property="isdefaulted" value="true">
                        <setter property="borderbrush" targetname="border" value="{dynamicresource {x:static systemcolors.highlightbrushkey}}"/>
                    </trigger>
                    <trigger property="ismouseover" value="true">
                        <setter property="background" targetname="border" value="{staticresource button.mouseover.background}"/>
                        <setter property="borderbrush" targetname="border" value="{staticresource button.mouseover.border}"/>
                    </trigger>
                    <trigger property="ispressed" value="true">
                        <setter property="background" targetname="border" value="{staticresource button.pressed.background}"/>
                        <setter property="borderbrush" targetname="border" value="{staticresource button.pressed.border}"/>
                    </trigger>
                    <trigger property="isenabled" value="false">
                        <setter property="background" targetname="border" value="{staticresource button.disabled.background}"/>
                        <setter property="borderbrush" targetname="border" value="{staticresource button.disabled.border}"/>
                        <setter property="textelement.foreground" targetname="contentpresenter" value="{staticresource button.disabled.foreground}"/>
                    </trigger>
                </controltemplate.triggers>
            </controltemplate>
        </setter.value>
    </setter>
</style>

这是aero2使用blend获取的button控件模板。因为button是最基础最常用最具代表性的控件,所以以它为例谈谈aero2主题中的各种细节。

3.1 尺寸

首先考虑下控件是否有必要有统一的尺寸。

我记得很久很久以前微软有份文档要求桌面按钮的高度是22像素(有可能是23,已经不记得了)。微软自己有没有遵守?真是太看得起微软了。

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

就以ie来说,上图从上到下几组按钮的高度分别是21,28,24像素。

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

这个页面大部分按钮都是28,只有中间那个“将所有区域重置为默认级别”是30像素。

可以看出,微软一直以来开放、包容、拥抱多元化的策略,在ie上可以说是完美体现。作为对比我看了看chrome的类似按钮,统一为32像素,看来有很好地执行material design中"所有距离,尺寸都应该是8dp的整数倍"的要求(到处都是8,可以说深得中国人欢心)。

<rectangle height="1" fill="gray" />
<stackpanel orientation="horizontal"
            horizontalalignment="center">
    <button content="button" verticalalignment="center" />
    <textbox text="textbox" verticalalignment="center" />
    <passwordbox password="password" verticalalignment="center" />
    <combobox verticalalignment="center">
        <comboboxitem content="combobox" isselected="true"/>
    </combobox>
    <datepicker verticalalignment="center"/>
</stackpanel>
<rectangle height="1" fill="gray" />

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

顺便拿button与wpf的其它控件、及uwp的相同控件做横向对比,使用相同的xaml产生的ui如上图所示(上为uwp,下为wpf)。可以看出uwp的表单元素基本上完全统一高度,而wpf则根据内容自适应。

总结来说,wpf原生控件通常没有设置具体的尺寸,所以模仿aero2主题的自定义控件也不应该改变这个行为,只需控件要能够清晰展示数据及容易操作就好(也就是符合基本的ui设计原则)。

我建议在实际项目中根据需要使用样式将按钮的高度统一为24、28、32像素(the sizes, margins, and positions of ui elements should always be in multiples of 4 epx in your uwp apps.,因为windows系统的缩放比例总是5/4(125%)、6/4(150%)、7/4(175%)、8/4(200%),所以尺寸最好是4的倍数,真不吉利)。

3.2 颜色

从button的控件模板可以看到button的字体颜色使用了{dynamicresource {x:static systemcolors.controltextbrushkey}}。wpf为系统环境封装了三个类,用于访问系统环境设置:

  • systemfonts,包含公开有关字体的系统资源的属性。
  • systemcolors,包含与系统显示元素相对应的系统颜色、系统画笔和系统资源键。
  • systemparameters,包含可用来查询系统设置的属性。

使用方式可以参考。

这些设置只应用作参考,可以看到button也只是主要使用了controltextbrushkey,aero2主题有自己的颜色风格,不会跟随系统而改变。

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

再次横向比较一下,这次试用disabled状态作比较,可以看到每个控件的边框无论在enabled或disabled的状态下边框颜色都不一样(除了textbox和passwordbox,他们关系好)。

因为看不到aero2在颜色上有什么要求,我的建议是,如果自定义的控件长得像textbox就使用textbox的颜色设置,长得像button的就用button,总之尽量模仿原生控件,颜色也尽量使用蓝色或灰色就可以了。

3.3 字体

只有menu、statusbar、toolbar等有限几个控件会使用systemfonts的值,其它都可以使用继承值。这样可以方便地通过在根元素设置字体来统一字体的使用。

3.4 动画

几乎、完全、没有。也许是为了兼顾windows的ui,或者照顾低端配置的电脑,aero2里真的几乎完全看不到动画效果,一眼看过去所有storyboard的duration都是0。也好,以和aero2统一风格作借口我也可以不做动画啦。

最近我发现这样介绍我:

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

其实我也并不是那么喜欢亲自写动画,只是wpf和uwp里连最基本的都没提供所以我才在这方面鼓起干劲努力了一把。

4. 提供visualstate

<controltemplate targettype="local:kinobutton">
    <border x:name="border"
            borderbrush="{templatebinding borderbrush}"
            borderthickness="{templatebinding borderthickness}"
            background="{templatebinding background}"
            snapstodevicepixels="true">
        <visualstatemanager.visualstategroups>
            <visualstategroup x:name="commonstates">
                <visualstate x:name="normal" />
                <visualstate x:name="mouseover">
                    <storyboard>
                        <objectanimationusingkeyframes storyboard.targetproperty="(panel.background)"
                                                       storyboard.targetname="border">
                            <discreteobjectkeyframe keytime="0"
                                                    value="{staticresource button.mouseover.background}" />
                        </objectanimationusingkeyframes>
                        <objectanimationusingkeyframes storyboard.targetproperty="(border.borderbrush)"
                                                       storyboard.targetname="border">
                            <discreteobjectkeyframe keytime="0"
                                                    value="{staticresource button.mouseover.border}" />
                        </objectanimationusingkeyframes>
                    </storyboard>
                </visualstate>
                <visualstate x:name="pressed">
                    <storyboard>
                        <objectanimationusingkeyframes storyboard.targetproperty="(border.borderbrush)"
                                                       storyboard.targetname="border">
                            <discreteobjectkeyframe keytime="0"
                                                    value="{staticresource button.pressed.border}" />
                        </objectanimationusingkeyframes>
                        <objectanimationusingkeyframes storyboard.targetproperty="(panel.background)"
                                                       storyboard.targetname="border">
                            <discreteobjectkeyframe keytime="0"
                                                    value="{staticresource button.pressed.background}" />
                        </objectanimationusingkeyframes>
                    </storyboard>
                </visualstate>
                <visualstate x:name="disabled">
                    <storyboard>
                        <objectanimationusingkeyframes storyboard.targetproperty="(textelement.foreground)"
                                                       storyboard.targetname="contentpresenter">
                            <discreteobjectkeyframe keytime="0"
                                                    value="{staticresource button.disabled.foreground}" />
                        </objectanimationusingkeyframes>
                        <objectanimationusingkeyframes storyboard.targetproperty="(panel.background)"
                                                       storyboard.targetname="border">
                            <discreteobjectkeyframe keytime="0"
                                                    value="{staticresource button.disabled.background}" />
                        </objectanimationusingkeyframes>
                        <objectanimationusingkeyframes storyboard.targetproperty="(border.borderbrush)"
                                                       storyboard.targetname="border">
                            <discreteobjectkeyframe keytime="0"
                                                    value="{staticresource button.disabled.border}" />
                        </objectanimationusingkeyframes>
                    </storyboard>
                </visualstate>
            </visualstategroup>
        </visualstatemanager.visualstategroups>
        <grid   horizontalalignment="{templatebinding horizontalcontentalignment}"
                verticalalignment="{templatebinding verticalcontentalignment}"
                margin="{templatebinding padding}">
            <grid.columndefinitions>
                <columndefinition width="auto" />
                <columndefinition />
            </grid.columndefinitions>
            <!--comecode-->

            <contentpresenter x:name="contentpresenter"
                              grid.column="1"
                              focusable="false"
                              recognizesaccesskey="true"
                              verticalalignment="{templatebinding verticalcontentalignment}"
                              snapstodevicepixels="{templatebinding snapstodevicepixels}" />
        </grid>
    </border>
    <controltemplate.triggers>
        <trigger property="isdefaulted"
                 value="true">
            <setter property="borderbrush"
                    targetname="border"
                    value="{dynamicresource {x:static systemcolors.highlightbrushkey}}" />
        </trigger>
    </controltemplate.triggers>
</controltemplate>

出于好玩,我把kinobutton(主要是在button的基础上添加了icon的功能)的控件模板从使用trigger改为尽量使用visualstate,这样做没什么实际意义,真的只是好玩而已,而且xaml的行数还增加了不少。

不过在实现其它自定义控件的时候我也比较倾向提供visualstate,因为这样可以明确指出控件外观有几种状态,避免了混轮,而且提供了visualstate可以更方便扩展。这点wpf原生控件也是一样的,它们很多都没有声明templatevisualstate,而且controltemplate也没有使用visualstate,但使用blend编辑控件模板还是可以在“状态”面板看到它的templatevisualstate(其中focusstates和validationstates可以不使用,如果修改了这两组状态也就是让控件外观更个性化而已)。对最终用户来说多一个选择并不是坏事。

[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

5. 结语

通过这篇文章读者应该对aero2的风格有了一定程度的了解。更多aero和aero2的相关信息可以看这个github项目

很多控件库都会提供额外的主题包,这点可以放到后面再考虑。

6. 参考

control样式和模板

presentationtheme.aero