摘要:本月 Billy Hollis 將向您介紹如何從頭創建可呈現其特有界面的可視控件。
我從來沒有真正想過要當一名 C++ 程序員,因為我太懶了,不能那么辛苦地工作。但我必須承認,我過去常常嫉妒那些 C++ 程序員,嫉妒他們編寫可視控件的能力。
Visual Basic® 6.0 及其早期版本中的控件僅限于“復合”控件(由其他控件組成的控件),這種控件稱為 UserControl。在 Visual Basic 6.0 中編寫能夠在屏幕上呈現其特有可視外觀的控件幾乎是不可能的。
現在好了,可以使用功能強大的 Visual Basic .NET 編寫各種類型的可視控件了!不僅可以編寫復合的 UserControl,還能繼承現有的控件(如 TextBox)并擴展其新功能。更重要的是,還可以從頭編寫能夠呈現其特有界面的可視控件。
在本文中,我將從頭創建一個完整的可視控件,以說明 Visual Basic .NET 的后一種功能。該控件是一個“紅綠燈”- 一個包含三個圓(分別代表紅、黃、綠三個燈)的矩形。圖 1 顯示各個燈亮時該控件的外觀,控件的背景顏色設置為系統顏色 ControlDark。
圖 1:帶有三個 TrafficLight 控件的窗體,每個控件亮不同的燈。
我們稱它為 TrafficLight 控件,它可以通過代碼或讓用戶單擊燈來改變亮起的燈。
因為 TrafficLight 是一個可視的 Windows 窗體控件,它將繼承 System.Windows.Forms 命名空間中的 Control 類。這樣,它將具有很多預定義的屬性、方法和事件,包括控制其外觀的屬性,如 ForeColor、BackColor、Size 和 Location;還包括事件,如 MouseOver 和 Click。您可以查看 .NET 文檔,獲得 Control 類成員的完整列表。
紅綠燈也需要具有特殊的屬性和事件,如下所示:
Status 屬性 | 確定亮起哪種顏色的燈。必須為以下三個枚舉值之一:
|
---|---|
BorderWidth 屬性 | 紅綠燈周圍邊框的寬度。 |
StatusChanged 事件 | 當通過代碼或由用戶單擊不同的燈改變 Status 屬性的值時,觸發該事件。 |
由于這些成員不屬于 Control 基類,所以我們需要包括完整的代碼以處理它們。我們還需要繪制邊框和三個相應顏色的燈的代碼,以便在屏幕上繪制紅綠燈。最后,我們需要處理用戶單擊圓以更改亮起燈的操作,并在更改亮起的燈時更改 Status 屬性。
為了使本示例盡可能接近實際應用環境,我們還將包括能夠確保在 Visual Studio® .NET IDE 中更好地使用控件的代碼。我們為工具箱設置適當的圖標,并包括能夠使屬性更好地與各屬性窗口集成的邏輯。
現在讓我們開始吧。
第 1 步:創建類型正確的項目
要創建一個保存 Windows 窗體控件的庫,需要在 Visual Basic.NET 中啟動一個新項目,選擇 Windows Control Library(Windows 控件庫)項目類型,然后將項目命名為 MyControls。
所創建的項目實際上可以保存多個 Windows 窗體控件,每個控件都屬于其各自的類,但我們只需在其中創建一個控件。
第 2 步:更改基類
在控件庫中創建的類自動命名為 UserControl1,默認情況下,從 UserControl 類繼承。如果我們要創建復合控件,那非常容易,只需將其他控件從工具箱中拖到設計表面上即可。
但是,由于我們要從頭創建自己的控件,因此需要做一些更改。將控件類的名稱從 UserControl1 更改為 TrafficLight。然后,將以下行:
Inherits System.Windows.Forms.UserControl
更改為:
Inherits System.Windows.Forms.Control
這樣,使最一般的 Control 類成為基類。您會發現,不再顯示可視設計表面,而是替換為組件設計表面。
為保持代碼的一致性,也要將代碼文件名從 UserControl1.vb 更改為 TrafficLight.vb?梢栽 Solution Explorer(解決方案資源管理器)中進行更改:右鍵單擊代碼文件的名稱,并選擇 Rename(重命名)。
還需要在類模塊的頂部添加幾行代碼。將 Option Strict 設置為 On,并導入包含我們將來要用到的某些屬性的命名空間。下面是要放到代碼最上面的兩行:
Option Strict On Imports System.ComponentModel
第 3 步:實現屬性和事件
要實現 Status 屬性,首先要為可能的屬性值創建枚舉。將以下幾行插入以 Inherits
開始的行下面:
Public Enum TrafficLightStatus statusRed = 1 statusYellow = 2 statusGreen = 3 End Enum
此枚舉是公開的,也就是說使用該控件的窗體可以訪問它。
在這些行下面添加以下三行:
Dim mStatus As TrafficLightStatus = TrafficLightStatus.statusGreen Dim msngBorderWidth As Single = 1.0! Public Event StatusChanged(ByVal NewStatus As TrafficLightStatus)
前兩行中的兩個變量可用于存儲 Status 和 BorderWidth 屬性的屬性值,還為這些屬性設置了默認值。保存 BorderWidth 的變量必須為 Single 類型,因為它是繪制邊框所用的圖形語句需要的類型。默認值中的驚嘆號也表明它是 Single 類型。此集合中的最后一行聲明了 StatusChanged 事件。
現在,我們為 BorderWidth 屬性編寫代碼。在標記為 Windows Form Designer Generated Code
(Windows 窗體設計器生成的代碼)的代碼區域下插入以下行:
<DefaultValue(1.0!), _ Description("紅綠燈周圍邊框的寬度")> _ Public Property BorderWidth() As Single Get Return msngBorderWidth End Get Set(ByVal Value As Single) If msngBorderWidth <> Value Then msngBorderWidth = Value Me.Invalidate() End If End Set End Property
前兩行包括使該屬性更好地使用 IDE 的屬性。DefaultValue 特性允許在 Properties(屬性)窗口中將屬性值重置為默認值(操作步驟稍后介紹)。Description 特性提供選中該屬性時在 Properties(屬性)窗口底部顯示的文本。
DefaultValue 特性還有一個技巧。如果將 TrafficLight 控件放到窗體上,并保留 BorderWidth 屬性的默認值,那么窗體設計器將不生成設置屬性值的代碼行。這使它與其他 Windows 窗體控件沒有什么區別。如果您查看典型控件(如 TextBox)的設計器生成的代碼,您會發現只包括設置為非默認值的屬性的代碼行。我們賦予 TrafficLight 控件同樣的能力。
Property Get 簡單明了。Property Set 子句包括可視控件屬性中常見的邏輯。設置屬性時,重要的是在新屬性值更改控件的外觀時要能夠重新繪制控件。因此,Set 子句負責確定傳遞的新值是否與屬性中現有的值不相同。如果相同,則不執行操作。如果不同,則接受新值,然后訪問控件的 Invalidate 方法。此方法表明,控件的可視區域已過期,控件需要重新繪制。
Status 屬性的處理有些不同,因為它是枚舉值。DefaultValue 特性沒有為枚舉屬性提供自動重置能力。在這種情況下,DefaultValue 也無法告訴設計器何時停止設置屬性值的代碼。因此,Status 屬性的實現中不需要 DefaultValue 特性。下面是 Status 屬性的代碼:
<Description("紅綠燈的狀態(顏色)")> _ Public Property Status() As TrafficLightStatus Get Status = mStatus End Get Set(ByVal Value As TrafficLightStatus) If mStatus <> Value Then mStatus = Value RaiseEvent StatusChanged(mStatus) Me.Invalidate() End If End Set End Property
看起來與 BorderWidth 屬性的實現類似,只有一點不同:當 Status 屬性發生改變時,除了強制重新繪制控件外,還會觸發 StatusChanged 事件。
要在 Properties(屬性)窗口中處理屬性的自動重置,我們需要使用一種特殊的方法。由于我們的屬性命名為 Status,因此必須將重置方法命名為 ResetStatus。重置方法只是恢復屬性的默認值。以下是其代碼:
Public Sub ResetStatus() Me.Status = TrafficLightStatus.statusGreen End Sub
為了提示設計器何時需要包括一行代碼以便設置 Status 屬性,我們需要包括一個名為 ShouldSerializeStatus 的方法。當屬性需要一行代碼時,此方法返回布爾值 True,否則,則返回 False。以下是其代碼:
Public Function ShouldSerializeStatus() As Boolean If mStatus = TrafficLightStatus.statusGreen Then Return False Else Return True End If End Function
第 4 步:繪制控件的外觀
要使控件具有一個可視的外觀,我們需要在 Paint 事件中放置邏輯。然后,每次控件需要刷新其可視外觀時,就會運行該邏輯。
Windows 窗體中的 Paint 邏輯使用 .NET 中 GDI+ 部分中的類。這些類基本上包括了 Windows API 圖形功能。由于適合 .NET,所以比 API 更易于使用。但是,有關它們的工作原理,需要理解以下幾點。
在 Windows API 中,圖形操作需要一個窗口句柄,有時稱為 hWnd。在 GDI+ 中,它由 Graphics 對象取代,該對象不僅代表了繪圖區域,還提供在該區域執行的操作(方法)。
例如,Graphics 對象具有以下方法,可用來繪制各種屏幕元素:
- DrawCurve
- DrawEllipse
- DrawLine
- DrawPolygon
- DrawRectangle
- DrawString
- FillEllipse
- FillPolygon
這些都是很容易理解的,只是可用方法的示例。一些更復雜的方法還允許旋轉對象。我們將使用 DrawRectangle 方法繪制邊框,使用 FillEllipse 方法繪制彩色的圓。
大多數繪圖方法都要求使用 Pen 或 Brush 對象。Pen 對象用于繪制直線并確定直線的顏色和粗細。Brush 對象用于填充區域、確定填充區域所使用的顏色,以及一些特殊效果(例如,用位圖填充區域)。我們將使用特殊的 Brush 效果使當前沒有亮起的燈的顏色變暗。
下面是處理控件的 Paint 事件的代碼:
Protected Overrides Sub OnPaint(ByVal pe As _ System.Windows.Forms.PaintEventArgs) MyBase.OnPaint(pe) Dim grfGraphics As System.Drawing.Graphics grfGraphics = pe.Graphics ' 首先繪制三個代表燈的圓。 ' 一個亮起,其余兩個熄滅。 DrawLight(TrafficLightStatus.statusGreen, grfGraphics) DrawLight(TrafficLightStatus.statusYellow, grfGraphics) DrawLight(TrafficLightStatus.statusRed, grfGraphics) ' 現在繪制紅綠燈周圍的輪廓 ' 用畫筆繪制輪廓,將它涂成黑色。 Dim penDrawingPen As New _ System.Drawing.Pen(System.Drawing.Color.Black, msngBorderWidth) ' 在控件上繪制紅綠燈的輪廓。 ' 首先定義要繪制的矩形。 Dim rectBorder As System.Drawing.Rectangle rectBorder.X = 1 rectBorder.Y = 1 rectBorder.Height = Me.Height - 2 rectBorder.Width = Me.Width - 2 grfGraphics.DrawRectangle(penDrawingPen, rectBorder) ' 釋放圖形對象 penDrawingPen.Dispose() grfGraphics.Dispose() End Sub
首先使用基類繪制,它通常使用控件的背景顏色繪制背景。然后,從事件參數中獲取控件的 Graphics 對象。
接下來,用一個函數畫出三個圓。有關該函數的內容稍后介紹。請注意,我們必須向該函數傳遞一個 Graphics 對象的引用,同時還要指示要畫的圓(紅、黃、綠)。
然后是繪制輪廓的代碼。聲明一個具有適當位置和大小的矩形,然后傳遞給 Graphics 對象的 DrawRectangle 方法。
最后,圖形對象激活其 Dispose 方法。使用 GDI+ 時,最好在完成圖形對象后立即釋放它們。這有助于清除操作系統繪圖時所用的資源。如果要在 Windows® 98 或 Windows Me 中使用控件,管理圖形資源就更加重要,因為這些操作系統處理這種資源的能力較差。
下面是繪制圓的函數:
Private Sub DrawLight(ByVal LightToDraw As TrafficLightStatus, _ ByVal grfGraphics As Graphics) Dim nCircleX As Integer Dim nCircleY As Integer Dim nCircleDiameter As Integer Dim nCircleColor As Color ' 找到所有圓的 X 坐標和直徑 nCircleX = CInt(Me.Size.Width * 0.02) nCircleDiameter = CInt(Me.Size.Width * 0.96) Select Case LightToDraw Case TrafficLightStatus.statusRed If LightToDraw = Me.Status Then nCircleColor = Color.OrangeRed Else nCircleColor = Color.Maroon End If nCircleY = CInt(Me.Size.Height * 0.01) Case TrafficLightStatus.statusYellow If LightToDraw = Me.Status Then nCircleColor = Color.Yellow Else nCircleColor = Color.Tan End If nCircleY = CInt(Me.Size.Height * 0.34) Case TrafficLightStatus.statusGreen If LightToDraw = Me.Status Then nCircleColor = Color.LimeGreen Else nCircleColor = Color.ForestGreen End If nCircleY = CInt(Me.Size.Height * 0.67) End Select Dim bshBrush As System.Drawing.Brush If LightToDraw = Me.Status Then bshBrush = New SolidBrush(nCircleColor) Else bshBrush = New SolidBrush(Color.FromArgb(60, nCircleColor)) End If ' 繪制代表紅綠燈的圓 grfGraphics.FillEllipse(bshBrush, nCircleX, nCircleY, nCircleDiameter, nCircleDiameter) ' 釋放筆刷 bshBrush.Dispose() End Sub
這是整個控件中唯一的一個復雜圖形。在 GDI+ 中,在要繪制橢圓的矩形中指定左上角的 X 坐標和 Y 坐標,然后指定矩形的高度和寬度即可繪制一個橢圓。我們分別將 X 坐標和 Y 坐標稱為 nCircleX 和 nCircleY。因為我們要繪制一個圓,因此矩形的高度等于寬度,用變量 nCircleDiameter 來控制該值。
將 nCircleX 設置為剛好放到控件內(控件的寬度乘以 0.02)。nCircleY 取決于要繪制哪個燈,可以設置成靠近控件的頂部(紅燈)、大約向下三分之一(黃燈)或大約向下三分之二(綠燈)。直徑 nCircleDiameter 設置為等于控件寬度的 96%。
要繪制實心橢圓,還需完成一件事,即確定要使用的顏色。顏色取決于正在繪制哪個燈以及正在繪制的燈是否亮起。亮起的燈的顏色要比熄滅的燈的顏色亮。
創建繪圖要使用的筆刷時需要使用這些顏色。如果正在繪制的燈是亮起的,即使用該顏色。如果繪制的燈是熄滅的,則要使用不同的方法實例化筆刷。下面是熄滅的燈所使用筆刷的代碼行:
bshBrush = New SolidBrush(Color.FromArgb(60, nCircleColor))
這并不是 .NET 中較好的方法名,但 FromArgB 方法的作用是創建筆刷,并通過將筆刷與背景顏色相結合來淡化顏色。第一個參數使用的數字介于 0 至 255 之間,數字越小,背景顏色滲透越深。我們使用的值為 60,它將大大降低處于熄滅狀態的燈的顏色。您可以嘗試對該參數使用不同的值(或將它設置成可設置屬性),以獲得不同的效果。
最后,Graphics 對象的 DrawEllipse 方法繪制出該圓,函數結束。記住,該函數需要調用三次以繪制三個不同的圓。
第 5 步:使控件響應用戶
要允許用戶更改燈的顏色,必須檢測到用戶的鼠標單擊操作。有經驗的 Visual Basic 開發人員都知道,可以使用多種方法實現這一目的。我們使用最簡單的一種方法,即檢測 MouseUp 事件。下面是檢測用戶單擊并更改 Status 屬性以與之匹配的代碼:
Private Sub TrafficLight_MouseUp(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles MyBase.MouseUp Dim nMidPointX As Integer = CInt(Me.Size.Width * 0.5) Dim nCircleRadius As Integer = nMidPointX If Distance(e.X, e.Y, nMidPointX, CInt(Me.Size.Height / 6)) _ < nCircleRadius Then Me.Status = TrafficLightStatus.statusRed Exit Sub End If If Distance(e.X, e.Y, nMidPointX, CInt(Me.Size.Height / 2)) _ < nCircleRadius Then Me.Status = TrafficLightStatus.statusYellow Exit Sub End If If Distance(e.X, e.Y, nMidPointX, CInt((5 * Me.Size.Height) / 6)) _ < nCircleRadius Then Me.Status = TrafficLightStatus.statusGreen End If End Sub Private Function Distance(ByVal X1 As Integer, _ ByVal Y1 As Integer, _ ByVal X2 As Integer, _ ByVal y2 As Integer) As Integer Return CInt(System.Math.Sqrt((X1 - X2) ^ 2 + (Y1 - y2) ^ 2)) End Function
事件處理非常簡單。檢查鼠標單擊的位置和每個圓心之間的距離。(請注意,圓心分別位于控件下方 1/6、1/2 和 5/6 的位置。如果不太明白,可以在紙上畫出來看看。)如果計算出的距離小于圓的半徑,則更改 Status 屬性。
距離由 Distance 函數使用您可能在代數課中學過的公式計算。請注意,平方根函數是從 System.Math 命名空間中獲得的,數學函數通常都保存在該命名空間中。
第 6 步:清理
為了使控件順利地運作,我們還需要執行一些其他操作。例如,大小改變時需要重新繪制控件。而且,為了不改變控件的比例,我們需要檢測影響大小的屬性發生更改的時間,然后強制寬度等于高度的三分之一。下面是完成這兩項任務的事件處理程序:
Private Sub TrafficLight_Resize(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Resize Me.Invalidate() End Sub Private Sub TrafficLight_Layout(ByVal sender As Object, _ ByVal e As System.Windows.Forms.LayoutEventArgs) _ Handles MyBase.Layout Select Case e.AffectedProperty Case "Bounds" Me.Width = CInt(Me.Height * 0.3333) Case Else ' 不執行任何操作 End Select End Sub
最后,設置控件在工具箱中使用的圖標?丶呀浻幸粋看似齒輪的默認圖標,但是我們要使用 Visual Studio .NET 附帶的紅綠燈圖標。
控件的工具箱圖標是由名為 ToolboxBitmap 的類中的特性設置的。在以 Public Class
開始的行上面插入以下行:
<ToolboxBitmap("C:\Program Files\Microsoft Visual Studio .NET\Common7\Graphics\icons\Traffic\TRFFC09.ICO")> _
注意:所有內容都應在一行中。為了便于閱讀,我們在Studio
后放置了一個回車。粘貼該代碼時,要確保它們位于一行中,Studio
和.NET
之間只需一個空格,并刪除回車。如果您已經將 Visual Studio .NET 安裝到其默認位置,那么上述代碼將用 Visual Studio 目錄中的圖標設置該特性。如果您沒有將 Visual Studio .NET 安裝到其默認位置,則需要相應地更改圖標的路徑名。
第 7 步:生成和測試控件
現在 TrafficLight 控件的設計就完成了。選擇 Build | Build MyControls(生成 | 生成 MyControls),以創建最終的控件庫。
要測試控件,我們需要一個 Windows 窗體項目。您可以在其他解決方案中執行此操作,但在開發控件所用的解決方案中執行會更容易。從菜單中選擇 File | Add Project | New Project(文件 | 添加項目 | 新項目)。選擇 Windows Application(Windows 應用程序)項目類型,將項目命名為 TestTrafficLight。單擊 OK(確定),啟動測試所需的 Windows 應用程序。
必須先將 TrafficLight 控件放到工具箱中,才能將其拖放到測試應用程序的空白窗體 1 中。右鍵單擊工具箱中的 Windows 窗體選項卡,然后選擇 Customize Toolbox(自定義工具箱)。選擇 .NET Framework Components(.NET Framework 組件)選項卡,然后單擊 Browse(瀏覽)按鈕。瀏覽到您的 MyControls 項目所在的位置,然后轉到該項目的 /bin 目錄。選擇 MyControls.dll 組件并單擊 OK(確定),F在,該對話框應如圖 2 所示。
圖 2:在 Customize Toolbox(自定義工具箱)對話框中,TrafficLight 控件被選中。
您可以看到 TrafficLight 控件旁邊有一個復選標記。單擊 OK(確定)按鈕,在工具箱的 Windows Forms(Windows 窗體)選項卡上,TrafficLight 控件將出現在控件列表的底部。圖 3 顯示了底部為 TrafficLight 控件的工具箱。
圖 3:工具箱底部的 TrafficLight 控件
現在,您可以將 TrafficLight 控件拖放到 TestTrafficLight 的空白窗體 1 中。默認情況下,它被命名為 TrafficLight1。您可以調整控件的大小,重新設置控件的屬性,包括 Status 屬性,該屬性有一個下拉菜單,菜單中包含該屬性的三個可能的值。請注意,調整控件的大小或更改其屬性時,控件將在設計器中自動刷新。
要恢復屬性的默認值,請將 Status 屬性更改為 statusRed。然后,右鍵單擊 Properties(屬性)窗口中的 Status(狀態)屬性,并選擇 Reset(重置),如圖 4 所示。該屬性將更改回 statusGreen。如果將 BorderWidth 屬性設置為 1 之外的其他值,也可以使用同樣的方法恢復其默認值。
圖 4:Properties(屬性)窗口中 Status(狀態)屬性的 Reset(重置)選項。請注意窗口底部有關 Status(狀態)屬性的說明。
如果需要,還可以為控件插入 StatusChanged 事件。然后,可以使用該事件中的以下代碼行查看更改后的狀態:
MsgBox("新狀態為 " & NewStatus.ToString)
要在操作中測試該控件,您需要啟動 TestTrafficLight 項目。此時,它還不是該解決方案的啟動項目,因此您需要解決它。在 Solution Explorer(解決方案資源管理器)中,右鍵單擊 Solution(解決方案)名稱 - Solution Explorer(解決方案資源管理器)中的第一行。選擇 Properties(屬性),然后將 Single Startup Project(單啟動項目)設置從 MyControls 更改為 TestTrafficLight,然后單擊 OK(確定)。
按 F5 鍵啟動該項目。將顯示帶有 TrafficLight 控件的窗體。測試控件:按下不同的燈,查看它們是否亮起。您還可以測試 BorderWidth 屬性,嘗試在代碼中設置燈的 Status 屬性。
小結
盡管 TrafficLight 是一個簡單的控件(雖然曾有開發人員要把它用到真實的項目中),但它卻顯示了開發復雜控件所需要的所有原理,包括:
- 在控件中添加屬性。
- 使用默認值和說明,使屬性與 Visual Studio IDE 協調。
- 在 Paint 事件中插入邏輯以繪制控件。
- 在繪圖邏輯中使用 GDI+。
- 為控件設置位圖,以便在工具箱中顯示。
創建復雜控件的關鍵在于熟悉 GDI+ 的繪圖能力。如果理解了 TrafficLight 繪制邊框和彩色圓的原理,那么您就有了一個好的起點。關鍵是,有了 Visual Basic .NET,即使象我這么懶惰的程序員也能創建高級的 Windows 窗體。
文章來源于領測軟件測試網 http://www.kjueaiud.com/