周银辉的开发博客(WPF)
在WPF中自定义控件(1)
一, 不一定需要自定义控件
在使用WPF以前,动辄使用自定义控件几乎成了惯性思维,比如需要一个带图片的按钮,但在WPF中此类任务却不需要如此大费周章,因为控件可以嵌套使用以及可以为控件外观打造一套新的样式就可以了.是否需要我们来自定义控件,这需要你考虑目前已有控件的真正逻辑功能而不要局限于外观,如果目前的控件都不能直觉地表达你的想法,那么你可以自己来打造一个控件,否则,也许我们仅仅改变一下目前控件的模板等就可以完成任务.很多人在自定义控件上经常犯的错误是:重复撰写已有的逻辑
二,UserControl还是CustomControl?
要在WPF中自定义一个控件,使用UserControl与CustomControl都是不错的选择(除此之外,还有更多选择,比如打造一个自定义的面板,但这不在本文的讨论范围),他们的区别在于:
UserControl,其更像WinForm中自定义控件的开发风格,在开发上更简单快速,几乎可以简单地理解为:利用设计器来将多个已有控件作为子元素来拼凑成一个UserControl并修改其外观,然后后台逻辑代码直接访问这些子元素.其最大的弊端在于:其对模板样式等支持度不好,其重复使用的范围有限.
CustomControl, 其开发出来的控件才真正具有WPF风格,其对模板样式有着很好的支持,这是因为打造CustomControl时做到了逻辑代码与外观相分离,即使换上一套完全不同的视觉树其同样能很好的工作,就像WPF内置的控件一样.
在使用Visual Studio打造控件时,UserControl与CustomControl的差别就更加明显,在项目中添加一个UserControl时,我们会发现设计器为我们添加了一个XAML文件以及一个对应的.CS文件(或.VB等),然后你就可以像设计普通窗体一样设计该UserControl; 如果我们是在项目中添加一个CustomControl,情况却不是这样,设计器会为我们生成一个.CS文件(或.VB等),该文件用于编写控件的后台逻辑,而控件的外观却定义在了软件的应用主题(Theme)中了(如果你没有为软件定义通用主题,其会自动生成一个通用主题themes\generic.xaml, 然后主题中会自动为你的控件生成一个Style),并将通用主题与该控件关联了起来.这也就是CustomControl对样式的支持度比UserControl好的原因.
三,继承于UserContorl,Control还是其它?
如果你准备打造一个控件,并使用像Visual Studio这样的工具来开发的话,打造UserControl时其会自动为你从System.Windows.Controls.UserControl继承,打造CustomControl时其会为从System.Windows.Controls.Control继承.但实际情况下,也许我们从他们的衍生类别开始继承会得到更多的好处(更好的重用已有的逻辑),比如你的控件拥有更多的类似于Button的某些特性,那么从Button开始继承就比从Control继承少写很多代码.
在接下来的几节中,我们会逐步讨论如何打造UserControl与CustomControl以及让它们更好支持WPF新特性.
在WPF中自定义控件(2) UserControl
在这里我们将将打造一个UserControl(用户控件)来逐步讲解如何在WPF中自定义控件,并将WPF的一些新特性引入到自定义控件中来.
我们制作了一个带语音报时功能的钟表控件, 效果如下:
在VS中右键单击你的项目,点击"添加新项目",在出现的选择列表中选择"UserControl",VS会自动为你生成一个*.xaml文件以及其对应的后台代码文件(*.cs或其它).
值得注意的是,自动生成的代码中,你的控件是继承于System.Windows.Controls.UserControl类的,这对应你的控件而言并不一定是最恰当的基类,你可以修改它,但注意你应该同时修改*.cs文件和*.xaml文件中的基类,而不只是修改*.cs文件,否则当生成项目时会报错"不是继承于同一基类".修改*.xaml文件的方法是:将该文件的第一行和最后一行的"UserControl"改成与你认为恰当的基类名称.
1,为控件添加属性(依赖属性,DependencyProperty)
正如下面的代码所示:
DependencyProperty.Register( " Time " , typeof (DateTime), typeof (ClockUserCtrl),
new FrameworkPropertyMetadata(DateTime.Now, new PropertyChangedCallback(TimePropertyChangedCallback)));
关于参数中传递的元数据:如果是普通的类则应该传递 PropertyMetadata,如果是FrameworkElement则可以传递 FrameworkPropertyMetadata,其中 FrameworkPropertyMetadata中可以制定一些标记表明该属性发生变化时控件应该做出什么反应,比如某属性的变化会影响到该控件的绘制,那么就应该像这样书写该属性的元数据: new FrameworkPropertyMetadata(defauleValue, FrameworkPropertyMetadataOptions.AffectsRender);这样当该属性发生变化时系统会考虑重绘该控件.另外元数据中还保护很多内容,比如默认值,数据验证,数据变化时的回调函数,是否参与属性"继承"等.
然后,我们将该依赖属性包装成普通属性:
[Category( " Common Properties " )]
public DateTime Time
{
get
{
return (DateTime)this.GetValue(TimeProperty);
}
set
{
this.SetValue(TimeProperty, value);
}
}
注意:在将依赖属性包装成普通属性时,在get和set块中除了按部就班的调用GetValue和SetValue方法外,不要进行任何其它的操作.下面的代码是 不恰当的:
[Category( " Common Properties " )]
public DateTime Time
{
get