把Resource嵌入到Generic.xaml文件中,并把该文件放到应用程序的Themes主题文件夹下面,这们Generic.xaml文件中的资源就可以被系统识别为默认主题一部分,从而进行使用。
为快速地为你的应用定制一个零部件,你需要的是UserControl, 为了让你打造的控件更标准化,更灵活以及更具有普遍意义,你需要用到的CustomControl,这正是本文要介绍的.
1,新建CustomControl
在选择控件基类后,第一件事情便是在你的项目中新建"CustomControl",我们会发现在项目中自动生成了一个*.CS(或*.VB或其他)文件以及/Themes/Generic.xaml (如果原来没有的话),他们分别是CustomControl的后台代码文件(Code Behind)与控件的默认主题文件,打开/Themes/Generic.xaml ,你会发现其中自动生成了一个Style,这是你的控件的默认样式,正如WPF内置控件也有它的默认样式一样.这时,我们的工作就被分成了两个部分,一是在XXX.cs文件中编辑控件逻辑,而是在Generic.xaml中编写其UI.
2,Generic.xaml中的Style是如何与我们的控件联系在一起的
打开XXX.cs文件,你会发现静态构造方法中,VS自动地帮你覆盖了控件的DefaultStyleKey值:
{
DefaultStyleKeyProperty.OverrideMetadata( typeof (CustomControl1), new FrameworkPropertyMetadata( typeof (CustomControl1)));
}
我们知道DefaultStyleKeyProperty是FrameworkElement以及FrameworkContentElement类用来 指示控件的默认样式键值的属性,该属性有一个很特别的地方就是我们不能够用继承的思想来思考它,比如说Button的默认样式键值是Style1,其子类 MyButton的默认样式键值是Style2(或者没有指定默认样式),尽管MyButton可以向上转型成Button类,但我们并不希望其转型后的 默认样式键值为Style1.所以WPF采用了在子类控件的静态构造方法中重写DefaultStyleKey元数据的方式来指定该子类控件的默认样式. 上面代码中,我们将new FrameworkPropertyMetadata(typeof(CustomControl1)) 指定为其新的元数据值,这个值代表着,我们将在资源字典中查找一个键值为typeof(CustomControl1)的Style来做为控件的默认样式.而这个样式刚好被我们定义在了Generic.xaml中:
< Setter Property = " Template " >
< Setter.Value >
< ControlTemplate TargetType = " {x:Type local:CustomControl1} " >
< Border Background = " {TemplateBinding Background} "
BorderBrush = " {TemplateBinding BorderBrush} "
BorderThickness = " {TemplateBinding BorderThickness} " >
</ Border >
</ ControlTemplate >
</ Setter.Value >
</ Setter >
</ Style >
这是大家可能有个疑问,上面XAML中的Style并没有指定Key值啊,而我们的控件要求的默认样式Key值为 typeof(CustomControl1), 并且资源字典中的元素肯定是要有Key的? 这是Style的基本知识了,在WPF中,为Style指定Key时有两种方式:一是明确指定Key,而是在没有明确指定Key的情况下指定 TargetType,WPF会自动地将其可Key设置为typeof(TargetType).如果你有在Blend中为控件打造Style的经验的 话,你会注意到新建一个Style时,Blend会提供一个"Apply to ALL"选项,这也是为什么你打造的Style可以"Apply to all"的奥秘所在.
3, "Generic.xaml"这个名称并非偶然
通过上面的叙述,你可能会有冲动将Generic.xaml中的Style代码剪切出来,粘贴到任何一个我们的控件可以找到的地方,然后把 Generic.xaml删掉或改成更优雅的名称,如果你运气好的话,这是可行的,因为控件会自下而上(Page,App,Theme)去查找其所需要的 Style,但此时你已经犯了一个潜在的错误:你没有为控件提供默认的样式.这里的默认样式其实是说"在默认主题中或没有为该控件找到当前操作系统对应的 主题时采用的的样式".这涉及到WPF中Theme的相关话题了,有兴趣可以参考msdn相关SDK.
4,打造你的控件逻辑
这是必然的,添加属性,添加事件,方法等等,这些你可以参考在WPF中自定义控件(2) UserControl ,这里就不重复叙述了.
5,打造控件UI
这里值得一提的是我非常佩服在VS的XAML海洋里"裸泳"的兄弟们,不过我更推荐使用Microsoft Expression Blend来完成这项艰巨的任务.另外,如果你发现WPF内置控件在Blend中很好用而我们自己打造的控件却不是这样,那么请注意了,你的控件逻辑可能 设计得不规范.
6,控件UI部分与逻辑部分的耦合度.
这是一个容易被忽略但却非常重要的问题, 我们之所以使用CustomControl而不是UserControl,是因为我们希望自 己的控件能向WPF内置控件一样,其UI能轻易地被其他用户定制或我们将来所改变.也就是说其视觉树不能与后台逻辑纠缠在一起,因为其视觉树中的元素完全 可能被你的控件用户改变.比如,如果你的控件的视觉树中有一个Button,而你在该Button的Click事件中做了一些控件的逻辑处理,那么很可能 你的控件打造失败了,因为该Button可能会在用户重新定义控件Template时被删除.
1, 控件UI部分与逻辑部分的耦合.
这是一个容易被忽略但却非常重要的问题, 我们之所以使用CustomControl而不是UserControl,是因为我们希望自 己的控件能向WPF内置控件一样,其UI能轻易地被其他用户定制或我们将来所改变.也就是说其视觉树不能与后台逻辑纠缠在一起,因为其视觉树中的元素完全 可能被你的控件用户改变.比如,如果你的控件的视觉树中有一个Button,而你在该Button的Click事件中做了一些控件的逻辑处理,那么很可能 你的控件打造失败了,因为该Button可能会在用户重新定义控件Template时被删除.
在讨论解决方案之前,需要提醒的是:一定要注意控件的逻辑与UI表现(Style,Template)各自职责的区分.不属于后台逻辑管的事情后台逻辑就 不要管,不属于界面管的事情界面基本上也管不了或者说做起来很麻烦.一个简单的例子是:比如说你想鼠标移动到你的控件上的事情,控件稍稍变大一点,鼠标离 开控件时控件大小又还原(或其他比较绚丽的效果),那么你在控件上的后台逻辑中添加的MouseEnter与MouseLeave事件的处理来达到这一效 果.这时你的后台逻辑就管得过宽了,因为这种效果是Style的事情,你可以把它放在控件的默认Style中(在Generic.xaml中,你可以参考在WPF中自定义控件(3) CustomControl (上) )来提供给控件用户而不应该加在后台逻辑中而费力不讨好.这不但增加了耦合,而且在用户看来这也有些"强奸民意",因为他没有办法通过自定义的Style来覆盖掉你认为比较漂亮的控件效果.
虽然WPF将UI与后台逻辑的隔离已经做得很不错了,以便UI设计师能和我们更好的沟通和分工协助,但这并不意味着,WPF可以将UI与后台完全的隔离而 互不影响.事实上,我们在编写后台逻辑的时候常常需要用到控件UI树中的某些元素才能完成,比如在编写ProgressBar时我们需要知道视觉树中的某 个表示"总量"的元素的长度或高度,以便根据ProgressBar的当前Value来确定视觉树中另外一个表示"当前量"的元素的长度或高度.还有一种 情况是,我们后台写好了一个不错的逻辑,但需要视觉树中的某个UI元素来明确调用,比如说,我们在ScrollBar控件中写好了LineDown()方 法,但该方法需要用户点击控件视觉树中某个表示"向下滚动一行"的元素(比如一个向下的箭头)时来调用.
WPF提供了两种方案,一是利用TemplatePartAttribute,二是使用Command.
1.1 TemplatePartAttribute
TemplatePart适用于上面所说的第一种情况,其用于告知用户,在目前的情况下必须在控件的视觉树中存在指定类型和名称的元素 才能是控件发挥完整的功能,否则可能导致功能丧失或需要用户自行处理删除视觉树中的该元素而带来的后遗症.如果我们是某个控件的使用者,且其注明了该属 性,那么我们在修改控件的Template时就应该保证控件中是指存在其指明的特定类型和名称的元素,除非了了解自己的确不需要其关联的相关功能或你已另 有处理.
在WPF内置控件中,这种类型的控件很多,比如ComboBox,PasswordBox,ProgressBar等等.
我们看看ComboxBox:
[TemplatePartAttribute(Name = " PART_Popup " , Type = typeof (Popup))]
[LocalizabilityAttribute(LocalizationCategory.ComboBox)]
[StyleTypedPropertyAttribute(Property = " ItemContainerStyle " , StyleTargetType = typeof (ComboBoxItem))]
public class ComboBox : Selector
我们发现其有两个TemplatePart属性,一个是TextBox类型的"PART_EditableTextBox",另一个是Popup类型的 PART_Popup,前者用于编辑文本,后者用于弹出列表项,如果某个用户在自定义该控件的Template时缺少了这两个元素,将失去相应的功能.
我们的控件也可以仿照ComboBox来规定必须的部件,并Override一些OnApplyTemplate ()方法来取得相应元素:
{
base .OnApplyTemplate();
Button mybtn = base .GetTemplateChild( " PART_BTN " );
if (mybtn != null )
{
mybtn.Click += new RoutedEventHandler(mybtn_Click);
}
}
1.2 Command
这适合上面提到的第二种情况,即是我们后台写好了一个不错的逻辑,但需要视觉树中的某个UI元素来明确调用.比如ScrollBar的上端和下端的两个小 箭头用来上下翻行,我们明显不能在这两个小箭头的鼠标点击事件中调用LineDown方法.那么正确的做法是,将后台逻辑中的LineDown和 LineUp方法包装成LineDownCommand和LineUpCommand,然后将视觉树中的元素的Command属性绑定到相应的 Command上.这样一来,即便用户修改视觉树中的上下小箭头为其他类型的元素,用户也可以通过命令绑定来与相应的功能联系起来.比如WPF内置的 ScroolBar控件的向下小箭头的XAML代码便是如下书写的:
关于Command你可以参考这里: WPF中的命令与命令绑定(一) WPF中的命令与命令绑定(二)
2,"鹤立鸡群"并不总是好事
如果某天艺术细胞大爆发,打造了一个非常漂亮的控件,这自然是好事情,但我担心这与用户当前操作系统下的大多数界面显得过于鹤立鸡群而格格不入,毕竟在还 是又不少人在Vista下使用"Windows经典"主题而非"Aero".为了打造与用户操作系统当前主题相容的控件UI,你可能需要为控件提供几套 Style,比如一个比较相当较华丽的用于Aero主题,另一个较朴实用于Windows classical.为了实现效果随着用户操作系统主题改变而 动态改变,你至少有两种方法来实现:(1)监听系统消息WM_THEMECHANGE,然后切控件界面.(2)将系统主题对应的Style放置在控件解决 方案的themes文件夹下,比如与Vista Aero向对应的放在themes/Aero.NormalColor.xaml,与蓝色的Windows XP主题对应的放在themes/Luna.NormalColor.xaml,与Window经典主题相对应的放在themes /Classic.xmal,相信大家已经看出规律:themes/主题名.颜色名.xaml,其中经典主题没有颜色名.这样当用户切换主题时我们的控件 就会切换到对应的Style,如果我们没有提供用户当前的主题所对应的样式则调用themes/Generic.xaml(这也就是为什么我在在WPF中自定义控件(3) CustomControl (上) 中说"Generic.xaml这个名称并非偶然 "的原因)
3,关于控件资源的存储位置
一般说来,为了不破坏控件的封装性,我们不会把控件的资源放到控件以外的位置,比如,有一些资源在我们的应用中被频繁的使用,我们共享这些资源,我们可能 会将这些资源移动到APP的资源字典中,但我们控件中的资源也被移出去,会破坏封装,并且这不利于控件被重用到其他APP. 但我们常常又会面临这样的问题:如果我把控件的资源完全放在该控件的资源字典中,但我们的应用很多地方使用了该控件,这就造成资源的频繁复制.一个典型的 例子是,我们制作一个扑克牌游戏,我们的美工为我制作了一套漂亮的扑克牌图片,共54张图片,然后我建立一个扑克牌控件,控件实例将根据其当前点数和花色 来选择其中一张图片并呈现出来,最后生成54个扑克牌控件实例来构成一套完整的扑克牌.如果我将美工提供的54张图片放置在控件的资源字典中,事实上对于 一个扑克牌控件实例来说只使用了其中的一张图片,其余53张完全是多余的.而生成54张扑克牌控件实例时则相当于保存了54*54=2916张美工提供的 图片.解决的办法是将资源转移到控件的themes/Generic.xaml中,这样既没有破坏封装又然资源得到了共享.