[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题
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的设计
上面分别是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,已经不记得了)。微软自己有没有遵守?真是太看得起微软了。
就以ie来说,上图从上到下几组按钮的高度分别是21,28,24像素。
这个页面大部分按钮都是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" />
顺便拿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主题有自己的颜色风格,不会跟随系统而改变。
再次横向比较一下,这次试用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和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可以不使用,如果修改了这两组状态也就是让控件外观更个性化而已)。对最终用户来说多一个选择并不是坏事。
5. 结语
通过这篇文章读者应该对aero2的风格有了一定程度的了解。更多aero和aero2的相关信息可以看这个github项目。
很多控件库都会提供额外的主题包,这点可以放到后面再考虑。