作者:Lieo
前些日子我老弟(哪个老弟?就是经常被我欺负的那个)问我在C#中怎样在TreeView控件中添加背景图片。当时我要他从系统提供的TreeView类里派生出一个新类,复写基类的OnPaint事件。我帮他实现这个功能时发现这样做是不可行的,但对有些控件又能实现这种方法,因为在系统提供的各类里有很多机制是不同的。
前几天由于自己正在开发的软件的需要扩充了两个控件(Button和ListBox),再加上之前实现的带背景图的TreeView,一共三个控件,先简要叙述如何以系统控件为模板进行扩充制作出一个SplitButton。效果如图所示:
.NET的菜单控件和状态栏控件的项中提供了非常好用的控件——ToolStripSplitButton,这种控件在按钮的右方有一个三角形,用户可以通过单击三角形展开下拉菜单,或者单击按钮执行按钮的功能。但是这个控件在工具箱中是没有类似控件的,现在我们以Button类为基础,实现一个类似的控件LieoSplitButton。
对于这样一个控件,我们作出如下的设计:
(1)规定右侧23个像素宽度的区域为下拉按钮区域,如果鼠标停留在这个区域,则三角形和分割线以高亮色显示;如果用户在这个区域单击按钮,则弹出下拉菜单;
(2)文字区域:其他位置均为文字区域。如果鼠标停留,则文字以高亮色显示。如果用户在此处单击菜单,则执行按钮的Click事件;
(3)如果用户定义必须弹出下拉菜单(即下文的MustSplitDown属性为True),则不显示分隔线,用户单击按钮任何区域均弹出下拉菜单。
既然是以Button类为基础,那你就必须要从Button类继承,这个过程不多说。继承了之后,子类便具有了基类的所有功能。之后,我们为LieoSplitButton类定义几个新属性——SplitMenu、ForeColor、HighLightColor和MustSplitDown。SplitMenu是一个类型为ContextMenuStrip的属性,用于记录下拉菜单的引用变量。ForeColor属性记录在正常状态下文字和右方三角形的颜色(包括分隔线的颜色),HighLightColor属性则记录了鼠标悬浮在按钮或三角形部分时高亮的颜色。MustSplitDown属性定义了控件是否能够响应按钮的单击事件,如果此属性为False,则单击按钮的任何部分都将打开下拉菜单;否则只有单击右侧三角形时才打开下拉菜单,单击按钮文字部分时将执行按钮的Click事件。
如何判断用户点击的是按钮的哪个部分呢?可以通过复写父类的OnMouseMove事件来实现。我们先定义一个整数型的类级的私有变量MouseHoverArea,其值为0时表示鼠标在按钮区域外,为1时表示在非三角形部分,为2表示在下拉三角形区域。当鼠标在下拉菜单区域时,则指针变成手形,如果MustSplitDown为True,则在任何区域都显示为手形。
在鼠标移走时,设置MouseHoverArea为0(复写父类的OnMouseLeave即可)。
接下来,我们就要进入最重要的部分——绘制按钮以及右侧的三角形。
按钮出了要像标准的按钮显示形状和文字外,还需要显示自定义的图形。为此,我们必须复写基类的OnPaint事件。首先我们设置按钮上面的文字。根据鼠标的悬浮位置,设置按钮文字的颜色,在显示下拉三角形的情况下,我们在文字后添两个空格,给三角形留出位置,只需要直接设置父类的Text属性即可。
MyBase.OnPaint方法是调用父类的OnPaint事件,这样,就可以显示出一个默认的按钮。之后调用了PaintGraphics方法,这个方法用于在按钮的右侧绘制三角形和分割线,请看文末的参考代码。
特别注意:不要将PaintGraphics的事件代码写在OnPaint事件中,而像这样将绘图语句写到另一个方法中,否则程序会因为在此事件中重绘了图像而再次调用OnPaint事件,从而产生死循环。
最后,处理OnClick事件。按照上面代码的设计,当鼠标为手形时,显示下拉菜单,否则执行Click事件的代码。怎么确定下拉菜单的位置呢?我们只需要调用Control类的PointToScreen静态方法就可以获得按钮相对于屏幕左上方的位置坐标,将纵坐标加上按钮的高度,即可把菜单显示在按钮的正下方:
SplitMenu.Show(PointToScreen(New Point(0, Me.Size.Height)))
如果不显示菜单而需要响应Click事件,只需要简单地调用基类的OnClick方法即可。
注:如果想要为TreeView添加背景图,不能简单地去复写TreeView类的OnPaint事件,因为ListView和TreeView均不响应这个事件。处理办法是接受控件的WndProc消息,在接受到WM_PAINT消息后调用User32.dll库中的相关API函数进行绘制,过程较为复杂。具体请参考这里。
附:LieoSplitButton类的完整代码。您只需要完整、正确地复制下列代码并添加到一个新类中,编译之后就可以在工具箱中使用这个新控件了(也可以通过代码创建)。如下图:
完整代码(Visual Basic语言,C#和J#程序员请自行转换):
Imports System.ComponentModel
Namespace Controls
''' <summary>
''' 自定义一个带下拉列表的按钮。
''' </summary>
Public Class LieoSplitButton
Inherits Button
'记录鼠标当前悬浮的区域(0-按钮之外,1-文字,2-下拉按钮)
Private MouseHoverArea As Integer
Private s_SplitMenu As ContextMenuStrip
Private s_MustSplitDown As Boolean
Private s_Text As String
''' <summary>
''' 要显示的下拉菜单(若为Nothing,则与普通按钮样式相同)。
''' </summary>
Public Property SplitMenu As ContextMenuStrip
Get
Return s_SplitMenu
End Get
Set(ByVal value As ContextMenuStrip)
s_SplitMenu = value
Me.Refresh()
End Set
End Property
''' <summary>
''' 设置右侧的下拉三角形和竖杠的颜色。
''' </summary>
Public Shadows Property ForeColor As Color
''' <summary>
''' 设置鼠标悬浮在文字和箭头上的高亮颜色。
''' </summary>
Public Property HighLightColor As Color
''' <summary>
''' 指定是否必须在下拉菜单中执行命令。若为 False,则单击按钮时执行第一项。
''' </summary>
<DefaultValue(True)> Public Property MustSplitDown As Boolean
Get
Return s_MustSplitDown
End Get
Set(ByVal value As Boolean)
s_MustSplitDown = value
Me.Refresh()
End Set
End Property
''' <summary>
''' 显示在控件上的文字。
''' </summary>
Public Shadows Property Text As String
Get
Return s_Text
End Get
Set(ByVal value As String)
s_Text = value
Me.Refresh()
End Set
End Property
''' <summary>
''' SplitButton 的构造函数。设置各属性的默认值。
''' </summary>
''' <remarks></remarks>
Public Sub New()
MustSplitDown = True
HighLightColor = Color.Blue
End Sub
''' <summary>
''' 鼠标离开时,将文字和按钮的颜色还原。
''' </summary>
Protected Overrides Sub OnMouseLeave(ByVal e As System.EventArgs)
MyBase.OnMouseLeave(e)
MouseHoverArea = 0 '鼠标移走,还原按钮颜色
End Sub
''' <summary>
''' 设置鼠标的样式。
''' </summary>
Protected Overrides Sub OnMouseMove(ByVal mevent As System.Windows.Forms.MouseEventArgs)
MyBase.OnMouseMove(mevent)
If SplitMenu Is Nothing Then Exit Sub
'如果必须拉出下拉菜单,或者鼠标落在三角形区域,则鼠标变成手型样式
If mevent.X > Size.Width - 25 OrElse MustSplitDown Then
If Me.Cursor <> Cursors.Hand Then Me.Cursor = Cursors.Hand
If MouseHoverArea <> 2 Then
MouseHoverArea = 2
Me.Refresh()
End If
Else
If Me.Cursor = Cursors.Hand Then Me.Cursor = Cursors.Default
If MouseHoverArea <> 1 Then
MouseHoverArea = 1
Me.Refresh()
End If
End If
End Sub
''' <summary>
''' 复写鼠标单击事件。实现显示下拉菜单或直接执行原有代码。
''' </summary>
Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
'决定是否弹出菜单
If SplitMenu Is Nothing Then Exit Sub
'如果必须拉出下拉菜单,或者鼠标落在三角形区域,则弹出菜单
If Me.Cursor = Cursors.Hand Then
SplitMenu.Show(PointToScreen(New Point(0, Me.Size.Height)))
Else
MyBase.OnClick(e)
End If
End Sub
''' <summary>
''' 绘制按钮右边的下拉三角形和分隔符。
''' </summary>
Private Sub PaintGraphics(ByVal e As System.Windows.Forms.PaintEventArgs)
If SplitMenu Is Nothing OrElse Me.Size.Width < 25 OrElse Me.Size.Height < 17 Then Exit Sub
Dim TriPoints(2) As Point
TriPoints(0) = New Point(Size.Width - 20, CInt(Size.Height / 2 - 3))
TriPoints(1) = New Point(TriPoints(0).X + 10, TriPoints(0).Y)
TriPoints(2) = New Point(Size.Width - 15, TriPoints(0).Y + 5)
If Me.Enabled = True Then
If MouseHoverArea = 2 Then
e.Graphics.FillPolygon(New SolidBrush(Me.HighLightColor), TriPoints)
Else
e.Graphics.FillPolygon(New SolidBrush(Me.ForeColor), TriPoints)
End If
Else
e.Graphics.FillPolygon(Brushes.Gray, TriPoints)
End If
If MustSplitDown = False Then
If Me.Enabled = True Then
If MouseHoverArea = 2 Then
e.Graphics.DrawLine(New Pen(Me.HighLightColor), New Point(Size.Width - 23, CInt(Size.Height / 2 - 8)), _
New Point(Size.Width - 23, CInt(Size.Height / 2 + 5)))
Else
e.Graphics.DrawLine(New Pen(Me.ForeColor), New Point(Size.Width - 23, CInt(Size.Height / 2 - 8)), _
New Point(Size.Width - 23, CInt(Size.Height / 2 + 5)))
End If
Else
e.Graphics.DrawLine(Pens.Gray, New Point(Size.Width - 23, CInt(Size.Height / 2 - 8)), _
New Point(Size.Width - 23, CInt(Size.Height / 2 + 5)))
End If
End If
End Sub
''' <summary>
''' 覆盖父类的OnPaint事件。绘制自定义按钮样式。
''' </summary>
''' <param name="pevent"></param>
Protected Overrides Sub OnPaint(ByVal pevent As System.Windows.Forms.PaintEventArgs)
Select Case MouseHoverArea
Case 0, 2
MyBase.ForeColor = Me.ForeColor
Case 1
MyBase.ForeColor = Me.HighLightColor
End Select
If Me.SplitMenu Is Nothing Then
MyBase.Text = Me.Text
Else
MyBase.Text = Me.Text & " "
End If
MyBase.OnPaint(pevent)
PaintGraphics(pevent)
End Sub
End Class
End Namespace