![]() 圖 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_selectedItem As Integer = 3 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 時)。
| 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 If Me.ImageToDraw = UserSuppliedImage Then
Dim img As Image If Not img Is Nothing Then Protected Overridable Sub DrawStandardImage( _ Dim fillBrush As Brush If m_hovering And m_hoverItem > currentPos Then Select Case ImageType |
在該例程中,計算每個圖形的位置(使用 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 窗體的設計表面的能力。但是所需的不只是那些標準的功能,因此,我將向幾個重要域中添加新的事件和代碼(參見下面的示例代碼)。
| Public Event SelectedItemChanged As EventHandler Protected Overridable Sub OnSelectedItemChanged() Public Property SelectedItem() As Integer |
這個新的 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 |
設計繼承
只有將類設計為可從其中繼承時繼承才起作用。因此,或許它有點像強語句。繼承將始終起作用(只要您從中繼承的類沒有標記為 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 |
一個三角形類繼承示例
由于目前為止已做的工作,只需幾分鐘編碼,我就能從我的控件中進行繼承并使用新的圖像類型對它進行擴展。圖 7 顯示了最后結果 ─ 支持單個新圖形類型(三角形)的類。
![]() 圖 7 MTS |
我所需的代碼只是 DrawStandardImage 的重寫和一個新的常量,如下所示。
| Public Class Triangles Inherits Ratings Public Const Triangle = 3 Protected Overrides Sub DrawStandardImage( _ Select Case ImageType If IsHovering AndAlso HoverItem > currentPos Then Dim pts(2) As PointF pts(0).X = (x + (w / 2)) g.FillPolygon(fillBrush, pts) Case Else |
無論是對于 Windows 還是 Web,控件開發都是使代碼片斷可重用的一種極好的方法,但是如果您要允許其他開發人員在您的基礎上進行構建,則您需要謹慎地計劃繼承。
小結
本文完成的產品是一個簡單的等級控件,但如果您使用它作為 DataGrid 中的列,它可能非常有用。

