.NET中扩充控件功能的实现方法——制作一个SplitButton

作者: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
✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com