构建Windows控件并不是一件特别复杂的事情。我曾在以前的文章中讲过如何通过最专业的技术来构建复杂的控件,但这并不意味着构建所有控件都是那么复杂。本文我将用一种曾在我的工作中遇到的简单方法来解决一个真实领域中的问题。就算你只有一些或者完全没有什么构建控件的经验,你也可以用它来实现在你的桌面应用程序中加入复杂的功能。
我需要一个带有不同图象的弹出式按钮,用于实现常规的、mouse-hover和mouse-down状态。我可以用一个常规的WinForm按钮来实现大多数我想要的效果,但却不能实现给边框加上颜色。我还想要让图象移到按钮的右边缘,就象菜单按钮那样。确切地说,我是需要一个能够代表其本身功能的菜单按钮。
你可以用大约150行的代码来构建这个控件;最长的过程包含约25行代码。这个方法是一个很好的起点;你可以给它添加许多性能并可以将它当作一个其他类型控件的模式。该过程的属性或许是这个项目中最为复杂的一个地方了――对.NET提供的经过深思熟虑的基类的一个确实的证明。
基本的方法是以一个已经存在的控件开始并通过继承来添加或改变其行为。控件的Paint事件允许你在窗体中进行随意绘制。对listbox或treeview来说,完成这个功能可能需要做很多工作,但对按钮来说,只需用图象作为表面就可以了。你可以通过从Button类中派生出你所需要的ImageButton类,用一个Button控件的Paint事件来绘制出适当的图象。然而,对于一个弹出式按钮来说,象Image、FlatStyle和AutoSize这样的Button属性是没有意义的。作为替代,你可以从Control基类中派生它并自己为它加上边框。这样做并不需要你编写额外的代码,它会生成一个更有效的控件和一个用于构建其他控件窗体的通用模板。
一个弹出式按钮的行为是很简单的。它有三种状态,每种状态都带有一个边框和一个图象。Control基类支持一组可以被覆盖(override)的Mouse过程,以及Paint程序。你可以通过简单地从Windows.Forms.Control派生来开始一个程序。奇怪的是,Control基类不是一个“必须继承类”(通常被成为抽象类),就是说以该类为基类进行派生时,你无需覆盖任何方法。覆盖是指Windows和.NET允许你在某人或某个东西(即系统)调用了基类的方法时执行你自己的代码。这一点非常有用。
在绘制时进行选择
当一个终端用户切换到另一个页面时, ImageButton、Windows以及.NET会通知Control类。Control类将Windows的信息传递给继承者的OnPaint程序。在编写覆盖程序时你可以运行自己的代码,而不需要完全按照基类的做法。尽管Control类不是一个抽象基类,但它自己并不完成任何绘制。然而,在你需要继承一个类时,――比如Button或Label类,通常你会取代基类的painting,而不是将它添加到你的程序中。OnPaint 覆盖中包括一个对MyBase的调用,这不是因为基类需要进行处理来实现绘制,而是为了给用户提供一个自己的Paint事件。继承类不会直接代表其基类来触发事件,对MyBase.OnPaint的调用导致基类触发客户端Paint事件。
这一点会对你将来构建控件有所影响,因此为了让你有更全面的了解我将从另一个角度对它进行讲述。如果你通过覆盖一个OnPaint 来支持你自己的作品(就是说用于一个标准的Button基类),而且你不仅仅想要实现基类所完成绘制,那么你的OnPaint覆盖中就不应该包含MyBase.OnPaint调用。在这个场景中,如果你还想为使用派生控件的开发人员提供一个Paint事件,则必须在基类中提供一个Paint事件声明。如果基类中已存在了一个Paint事件,你则必须用Shadows关键字来声明你自己的事件从而将基类的事件隐藏起来。不要轻易尝试使用Shadows,因为它容易让使用该控件的开发人员搞糊涂,虽然在一个事件中使用这种方法看起来似乎更安全。
Shadows只是用一个和基类相似的名称向用户显示一种方法的派生版本。它所存在的潜在问题是用户仍然可以通过用CType将你的类中的对象转化为基类来得到基类中的方法。Control类中的一些方法对ImageButton来说是没有用的。比如,不需要Text属性。你可以在Visual Studio的 Properties窗口中将Control.Text用一个ReadOnly属性替换掉,返回一个空串。用户可能会觉得很麻烦,但这样却能避免出现一些问题:Dim pop1 As New ImageButton
CType(pop1, Control).Text = "Hi"
前面这段代码不会导致出错,但却不会真正起什么作用;ImageButton不会通过其基类的Text属性来绘制控件。然而,如果用户尝试填写ImageButton的Text属性则会导致产生一个design-time(编译)只读错误:Dim pop1 As New ImageButton
pop1.Text = "Hi" 'Error
最后,通过将<Browsable(False)>属性添加到声明中来把Text属性隐藏起来。它还要求你给用户提供一个新的缺省属性,否则是无效的,因为Control的缺省属性是Text。通过将<DefaultProperty("DisplayImageIndex")>添加到类声明中来将DisplayImageIndex属性作为新的缺省属性。
涂成蓝色
和菜单按钮一样,ImageButton必须带有不同的图象和边框式样,这取决于鼠标的位置。和菜单按钮不同的是,ImageButton必须能够获得焦点并显示焦点矩形框。所有的特性都必须通过代码来实现,因为Control类不会处理。然而,你只需一小段代码就可以实现它,就像你从OnPaint过程中看到的那样。
你可以通过OnMouseEnter、Leave、Up和Down覆盖过程从系统中获得鼠标通知。你可以象使用一般的mouse事件一样来使用它们,但是用覆盖意味着你能够在基类提供行为之前或之后添加新的行为,或者取代基类的行为。通过设置一个MouseButtonState变量,你可以用每个过程来决定将哪个图象拖到控制界面。OnMouseDown还会设定焦点: Overrides Sub OnMouseDown(ByVal ma As _
MouseEventArgs)
MyBase.OnMouseDown(ma)
_MouseButtonState = Down
Me.Focus()
MyBase.Invalidate()
End Sub
Control.Invalidate调用用于告知基类该控件需要被重画。基类依次调用覆盖的OnPaint方法,它通过PaintEventArgs来提供一个GDI+ 图象对象。你可以用该对象共享的DrawImage方法用一行代码绘制一个位图(bitmap),给DrawImage提供图象、位置和大小,选择将哪个图象绘制到鼠标位置。一个很方便的设计态专用的DisplayImageIndex属性会让用户自己选择将哪种图象显示出来。你可以将两种属性用在该方法的声明中:<Category ("Design")>用于告诉Visual Studio属性窗口该在哪里列出该属性,<DesignOnly(True)>用于在运行时将它隐藏起来。给DisplayImageIndex值添加一个枚举,使用户可以通过简单地点击这个值来查看到Down、Up和Hover。DisplayImageIndex使用户无需打开ImageList控件来确保他们选择了正确的用于Down、Up和Hover的ImageIndex值。
你可以用鼠标位置来选取需要绘制的边框颜色。当焦点集中在控件上时,代码将边框厚度设置为两个象素点,只用于UP状态。建立一个新的Pen对象(不要用缺省的系统的画笔)画出大于一个象素点的一行。不要忘记在完成时调用Dispose方法。你应该根据边框的宽度调整边框矩形的大小,因为控件不能随意在窗体以外进行绘制。
我从来不喜欢用按钮图象的算法操作来显示up、over和down状态,每个ImageButton均用了三个单独的位图。在一个form中使用许多ImageButtons会导致产生大量的图象,因此我给ImageButton提供了一个ImageList属性,而不是三个图象属性。将该属性作为Forms.ImageList来声明,则NET和VS.NET IDE会为你处理大量的工作。你不需要通过编写代码来检测ImageList,属性窗口会将它显示出来。使用ImageList的另一个好处是它排除了用代码处理用户提供图象大小的可能性。当用户以不同的大小加载它时,ImageList代表的是一个单一大小和比例的图象。
图象