Visual Basic创建“五星”级控件

  • 来源: 编程中国 作者: 若水   2008-04-23/10:45
  • 本文的目标是 Windows MediaPlayer 中酷炫的“五星”级控件(参见 图 1)。


    图 1“五星”级控件

    该控件看起来非常棒,并且提供了一种很好的图形化查看等级的方法,但根据编辑经验,我发现它特别酷。当鼠标悬停在该栏上时,Windows Media Player 突出显示其中的星来显示您当前浮于其上的栏的值,从而提供了一种很好的图形反馈。在各种 Web 站点(包括 Netflix 和 Amazon)中,您可以发现相同类型的用户界面,并且我想在自己的应用程序中拥有这种功能,因此,我决定创建我自己的控件。我将使用 Windows 窗体控件来模拟这种用户界面元素,同时尝试使它具有足够的自定义能力,以便将其用于各种环境。

    入门

    第一个步骤是创建一个新的类库项目来容纳控件和空的 Windows 应用程序,以作为测试项目。尽管 Windows 控件库项目模板似乎更为合适,并且它可以很好地进行工作,但是在默认情况下,该项目包括用户控件(用户控件一般用于复合控件 ─ 包含一个或多个控件的 Windows 窗体控件),而我需要的只是一个空的类文件。下一步,您必须使当前的、新的空类从 System.Windows.Forms.Control 中继承,这只需在类声明后添加一个单行就可以实现:

    Public Class Ratings
    Inherits System.Windows.Forms.Control End Class

    如果您尝试只使用 IntelliSense? 添加 Inherits 语句,将会遇到一个小问题:使用类库模板来启动您的项目并不会添加对 System.Windows.Forms 程序集的引用,因此,您需要手动添加。此时,可先添加一个对 System.Drawing.dll 的引用,因为最后需要使用自定义绘图控件。 从现在起,我一般遵循以下几个步骤来进行所有的控件开发:

    1.为自定义绘图控件添加标准构造函数,设置该控件所需的所有控件样式,以便正确地绘图并尽可能使其平滑。

    Public Sub New()
    Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
    Me.SetStyle(ControlStyles.DoubleBuffer, True)
    Me.SetStyle(ControlStyles.ResizeRedraw, True)
    Me.SetStyle(ControlStyles.UserPaint, True)
    ... 'add any additional initialization code here
    End Sub

    2.用纸记下配置控件行为和外观可能需要的公共属性清单。

    3.将所有这些属性添加为私有成员变量(我喜欢使用匈牙利表示法在每个变量前面添加前缀“m_”来表示内部变量),并包括适当的默认值,如下 所示。

    Private m_FilledImage As Image
    Private m_EmptyImage As Image
    Private m_HoverImage As Image
    Private m_ImageCount As Integer = 5
    Private m_TopMargin As Integer = 2
    Private m_LeftMargin As Integer = 4
    Private m_BottomMargin As Integer = 2
    Private m_RightMargin As Integer = 4
    Private m_ImageSpacing As Integer = 8
    Private m_ImageToDraw As Integer = 1

    Private m_SelectedColor As Color = Color.Empty
    Private m_HoverColor As Color = Color.Empty
    Private m_EmptyColor As Color = Color.Empty
    Private m_OutlineColor As Color = Color.Empty

    Private m_selectedItem As Integer = 3
    Private m_hoverItem As Integer = 1
    Private m_hovering As Boolean = False

    Private ItemAreas() As Rectangle


    4.将这些变量放入属性过程,虽然其中大部分过程相当简单(获取值,设置值),但是其中有些过程将需要一些附加代码,稍后我将进行讨论。

    5.开始设计和编写自定义绘图代码。

    6.最后,添加新的事件,如单击处理或该特定控件所需的特定事件。

    带有特定默认值的属性例程

    我想要一些自己的属性

    ─ 处理颜色的那些属性

    ─ 反映有关控件的其他属性的默认值(如 ForeColor)以及反映用户系统颜色的其他值。例如,让我们只使用其中的一种颜色 HoverColor 来查看可生成默认值的不同方法。

    第一种方法是显而易见的,只需在变量声明(或者构造函数)中设置默认值:

    Private m_HoverColor As Color = _
    Color.FromKnownColor(KnownColor.Highlight)

    在多数情况下,这就可以很好地进行工作,但是它存在两个问题。第一个问题是,如果用户在该应用程序运行时更改他们的系统颜色,该怎么办?在重新启动该程序后,该控件将反映正确的颜色,但是当时不会。第二个问题是,如果用户要以编程方式将颜色设置为默认颜色,该怎么办?没有实用方法可以清除颜色设置并将其设置为适当的系统颜色。用户自然会正确地将其设置为适当的系统颜色,但是随后又会再次回到第一个问题。 另外一种方法是在用户系统颜色发生更改时设置陷阱,并相应地更改您的属性值:

    Protected Overrides Sub OnSystemColorsChanged( _
    ByVal e As System.EventArgs)
    Me.HoverColor = Color.FromKnownColor(KnownColor.Highlight)
    Me.Invalidate()
    End Sub


    该解决方案并不会真正地解决问题,除非您有办法通过开发人员使用该控件了解该属性是否会设为默认值或设置为特定的颜色。跟踪该信息的系统开销可能不值得花费这么大的精力。作为替代,我决定使用默认的空值,并在属性例程自身中返回适当的默认值,如下 所示。 这样,就解决了到目前为止我所提出的问题,包括系统颜色更改的处理;了解它何时会返回默认值与用户何时对其进行设置;以及允许用户重新将该值设置为默认值(在 myControl.HoverControl = Color.Empty 时)。 #p#分页标题#e#

    Public Property HoverColor() As Color
    Get
    If m_HoverColor.Equals(Color.Empty) Then
    Return Color.FromKnownColor(KnownColor.Highlight)
    Else
    Return m_HoverColor
    End If
    End Get
    Set(ByVal Value As Color)
    If Not Value.Equals(m_HoverColor) Then
    m_HoverColor = Value
    Me.Invalidate()
    End If
    End Set
    End Property

    拟定自定义和标准图像

    在我正在构建的控件中,我决定允许两种主要的图像类别:标准图像和用户提供的图像。虽然最初的控件只支持两种标准图形(圆形和正方形),但是稍后我将讨论一种将自定义图形添加到该列表中的方法。 该控件的所有绘图在 OnPaint 例程中进行处理,我重写了该例程以提供我自己的渲染代码(参见下面的示例代码)。

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
    e.Graphics.Clear(Me.BackColor)

    Dim imageWidth, imageHeight As Integer
    imageWidth = (Me.Width-(LeftMargin + RightMargin + ( _
    Me.m_ImageSpacing * (Me.m_ImageCount-1)))) \ Me.m_ImageCount
    imageHeight = (Me.Height-(TopMargin + BottomMargin))

    Dim start As New Point(Me.LeftMargin, Me.TopMargin)

    For i As Integer = 0 To Me.ImageCount-1
    Me.ItemAreas(i).X = start.X-Me.ImageSpacing \ 2
    Me.ItemAreas(i).Y = start.Y
    Me.ItemAreas(i).Width = imageWidth + Me.ImageSpacing \ 2
    Me.ItemAreas(i).Height = imageHeight

    If Me.ImageToDraw = UserSuppliedImage Then
    DrawUserSuppliedImage(e.Graphics, _
    start.X, start.Y, imageWidth, imageHeight, i)
    Else
    DrawStandardImage(e.Graphics, Me.ImageToDraw, _
    start.X, start.Y, imageWidth, imageHeight, i)
    End If
    start.X += imageWidth + Me.ImageSpacing
    Next
    MyBase.OnPaint(e)
    End Sub


    Protected Overridable Sub DrawUserSuppliedImage( _
    ByVal g As Graphics, _
    ByVal x As Integer, ByVal y As Integer, _
    ByVal w As Integer, ByVal h As Integer, _
    ByVal currentPos As Integer)

    Dim img As Image
    If m_hovering And m_hoverItem > currentPos Then
    img = Me.HoverImage
    ElseIf Not m_hovering And m_selectedItem > currentPos Then
    img = Me.FilledImage
    Else
    img = Me.EmptyImage
    End If

    If Not img Is Nothing Then
    g.DrawImage(img, x, y, w, h)
    Else
    Me.DrawStandardImage(g, Me.Circle, x, y, w, h, currentPos)
    End If
    End Sub

    Protected Overridable Sub DrawStandardImage( _
    ByVal g As Graphics, ByVal ImageType As Integer, _
    ByVal x As Integer, ByVal y As Integer, _
    ByVal w As Integer, ByVal h As Integer, _
    ByVal currentPos As Integer)

    Dim fillBrush As Brush
    Dim outlinePen As Pen = New Pen(Me.OutlineColor, 1)

    If m_hovering And m_hoverItem > currentPos Then
    fillBrush = New SolidBrush(Me.HoverColor)
    ElseIf Not m_hovering And m_selectedItem > currentPos Then
    fillBrush = New SolidBrush(Me.SelectedColor)
    Else
    fillBrush = New SolidBrush(Me.EmptyColor)
    End If

    Select Case ImageType
    Case Me.Square
    g.FillRectangle(fillBrush, x, y, w, h)
    g.DrawRectangle(outlinePen, x, y, w, h)
    Case Me.Circle
    g.FillEllipse(fillBrush, x, y, w, h)
    g.DrawEllipse(outlinePen, x, y, w, h)
    End Select
    End Sub


    在该例程中,计算每个图形的位置(使用 ImageCount 属性来确定应绘制的图形数量),然后调用 DrawStandardImage(绘制圆形或正方形)或 DrawUserSuppliedImage(绘制用户提供的图形)。 这些例程并不是最有效的(例如,我始终重新绘制完整的控件,而不是只将那些受特定更新影响的区域设置为无效),但是在必要时它们会考虑绘制适当的图形(或在标准选项情况下绘制适当的彩色图形)。在控件的其余代码中,每当属性或声明发生更改引起控件外观发生更改时,都会通过调用 Me.Invalidate 触发完整的重新绘图。OnMouseMove 的重写例程是这种类型代码的一个示例:

    Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
    For i As Integer = 0 To Me.ImageCount-1
    If Me.ItemAreas(i).Contains(e.X, e.Y) Then
    Me.m_hoverItem = i + 1
    Me.Invalidate()
    Exit For
    End If
    Next
    MyBase.OnMouseMove(e)
    End Sub

    处理和启动事件

    此时,该控件运行正常,主要是因为从 System.Windows.Forms.Control 中继承了所有出色的功能。这种继承关系为控件提供了一组可用功能,包括“Click”事件和拖动到 Windows 窗体的设计表面的能力。但是所需的不只是那些标准的功能,因此,我将向几个重要域中添加新的事件和代码(参见下面的示例代码)。 #p#分页标题#e#

    Public Event SelectedItemChanged As EventHandler

    Protected Overridable Sub OnSelectedItemChanged()
    RaiseEvent SelectedItemChanged(Me, EventArgs.Empty)
    End Sub

    Public Property SelectedItem() As Integer
    Get
    Return m_selectedItem
    End Get
    Set(ByVal Value As Integer)
    If Value >= 0 And Value <= Me.ImageCount + 1 Then
    m_selectedItem = Value
    OnSelectedItemChanged()
    Else
    Value = 0
    End If
    End Set
    End Property


    这个新的 SelectedItemChanged 事件提供了许多便利,它还具有很好的辅助作用,即改善数据绑定性能。如果 Windows 窗体数据绑定代码发现一个事件,该事件具有遵循 Changed 格式的名称以及具有被定义为 System.EventHandler 的签名,则它将使用该事件作为绑定属性发生更改的通知。与轮询任何更改的属性相比,监控该事件的工作量要小得多,因此,最后将获得更有效的数据绑定。 我需要向控件中添加的其他例程只有 OnMouseEnter 和 OnMouseLeave 例程的重写,以确保用户在其上悬停时我正确地显示控件。

    如下面的代码所示,我还需要重写 OnClick 例程,这样,在用户选取新的等级值时,我可以正确地更新当前选定的项目。 此时,虽然我可以添加许多“装饰品”,例如指定工具箱位图以及对我的属性进行分类的属性,但是该控件基本上已经完成,并且可以很好地工作。尽管如此,下一个窍门是允许其他开发人员对我的工作进行扩展,以便为其他图形提供支持。

    Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
    Dim pt As Point = Me.PointToClient(Me.MousePosition)

    For i As Integer = 0 To Me.ImageCount-1
    If Me.ItemAreas(i).Contains(pt) Then
    Me.m_hoverItem = i + 1
    Me.SelectedItem = i + 1
    Me.Invalidate()
    Exit For
    End If
    Next
    MyBase.OnClick(e)
    End Sub


    设计继承

    只有将类设计为可从其中继承时继承才起作用。因此,或许它有点像强语句。继承将始终起作用(只要您从中继承的类没有标记为 NotInheritable),但是只有在基类设计过程中考虑到将来的继承,它才有可能在以后为其他开发人员添加所需功能提供便利。要设计易于从中继承的类,第一个步骤是确定其他开发人员将如何对其进行扩展。虽然您不能预测其他开发人员有可能做的一切事情,但是,您肯定可以推测他们可能进行哪些最明显的修改。记住这一点,您现在可以查看用于组织问题、辅助功能以及易用性的代码。

    注意:您是否将代码分成各种功能以最好地封装多个域(其中有人要对该类进行扩展),或者,其他开发人员是否只为了添加一些新功能,而必须重写许多不相关的代码。而且要注意,您是否已经适当地设置了有关变量和例程的访问修饰符 (Public、Private、Protected)。记住您的目标是尽可能使用户的体验完美无缺。 虽然在将类设计为参与继承时还需要注意其他一些事项,但是在查看这个特殊示例时已经考虑到这些问题。让我们有条不紊地来处理这些问题。我将讨论已经对“base”类(等级)所做的更改,以使其易于扩展。

    代码组织

    为了最有效地组织代码(尽管可能已经采用某种方式对代码进行了组织,以使其清晰明了),我还没有将图像和图形绘制代码放入 OnPaint 中。通过将它们作为自身的例程,开发人员可以重写其中一个例程,而无需担心 OnPaint 中出现的所有定位和图形安装项。而且,我还谨慎地将那两个绘图例程(以及该类中的大多数例程)标记为 Overridable,因为它们看起来可能需要进行扩展。

    辅助功能

    要使我的代码有些辅助功能,我将这两个绘图例程设为 Protected(而不是 Private),这样,从我的类中继承的类就可以使用它们,但仍然从公共接口将它们隐藏起来。我还将几个附加例程(包括 OnSelectedItemChanged)标记为 Protected,这样,必要时子类就可以调用那些例程。

    易用性

    在这三个注意事项中,最模糊的就是易用性,它是指使类的扩展版本可以与基类一样易于使用。当然,您不能控制继承开发人员将对您的类所进行的操作,因此您不能保证最后它一定能易于使用,但是您可以尝试改进几率。就我的类来说,我最初创建 ImageType 作为 Enum(包括 UserDefined、Square 和 Circle),这产生如下代码:

    sr.ImageToDraw = ImageType.Circle

    在我尝试将其设想为一个继承环境(其中子类已经添加了一个新的 ImageType)时,就会存在一个问题。不能对枚举进行扩展,因此,结果为:

    sr.ImageToDraw = 3 'some number not in our original enum

    按照强类型,这可能存在问题(尽管您可能使其工作,因为 Enum 一般是幕后的 Int32 类型),而且它不是很合适。要解决这个问题,我丢弃枚举并定义自己的 ImageType 作为有关控件类的公共约束,代码如下:

    sr.ImageToDraw = Ratings.Circle

    并且就带有新图形类型的子类来说,代码如下:

    sr.ImageToDraw = myNewClass.NewShape
    #p#分页标题#e#
    一个三角形类继承示例

    由于目前为止已做的工作,只需几分钟编码,我就能从我的控件中进行继承并使用新的图像类型对它进行扩展。图 7 显示了最后结果 ─ 支持单个新图形类型(三角形)的类。


    图 7 MTS

    我所需的代码只是 DrawStandardImage 的重写和一个新的常量,如下所示。

    Public Class Triangles
    Inherits Ratings

    Public Const Triangle = 3

    Protected Overrides Sub DrawStandardImage( _
    ByVal g As System.Drawing.Graphics, _
    ByVal ImageType As Integer, _
    ByVal x As Integer, ByVal y As Integer, _
    ByVal w As Integer, ByVal h As Integer, _
    ByVal currentPos As Integer)

    Select Case ImageType
    Case Triangles.Triangle
    Dim fillBrush As Brush
    Dim outlinePen As Pen = New Pen(Me.OutlineColor, 1)

    If IsHovering AndAlso HoverItem > currentPos Then
    fillBrush = New SolidBrush(Me.HoverColor)
    ElseIf Not IsHovering AndAlso _
    SelectedItem > currentPos Then
    fillBrush = New SolidBrush(Me.SelectedColor)
    Else
    fillBrush = New SolidBrush(Me.EmptyColor)
    End If

    Dim pts(2) As PointF

    pts(0).X = (x + (w / 2))
    pts(0).Y = y
    pts(1).X = x + w
    pts(1).Y = y + h
    pts(2).X = x
    pts(2).Y = y + h

    g.FillPolygon(fillBrush, pts)
    g.DrawPolygon(outlinePen, pts)

    Case Else
    MyBase.DrawStandardImage(g, ImageType, _
    x, y, w, h, currentPos)
    End Select
    End Sub
    End Class


    无论是对于 Windows 还是 Web,控件开发都是使代码片断可重用的一种极好的方法,但是如果您要允许其他开发人员在您的基础上进行构建,则您需要谨慎地计划继承。

    小结

    本文完成的产品是一个简单的等级控件,但如果您使用它作为 DataGrid 中的列,它可能非常有用。

     

    上一页  [1] [2] [3] 


    评论 {{userinfo.comments}}

    {{money}}

    {{question.question}}

    A {{question.A}}
    B {{question.B}}
    C {{question.C}}
    D {{question.D}}
    提交

    驱动号 更多