我從來沒有真正想過要當一名 C++ 程序員
因為我太懶了
不能那麼辛苦地工作
但我必須承認
我過去常常嫉妒那些 C++ 程序員
嫉妒他們編寫可視控件的能力
Visual Basic® 及其早期版本中的控件僅限於復合控件(由其他控件組成的控件)這種控件稱為 UserControl在 Visual Basic 中編寫能夠在屏幕上呈現其特有可視外觀的控件幾乎是不可能的
現在好了可以使用功能強大的 Visual 編寫各種類型的可視控件了!不僅可以編寫復合的 UserControl還能繼承現有的控件(如 TextBox)並擴展其新功能更重要的是還可以從頭編寫能夠呈現其特有界面的可視控件
在本文中我將從頭創建一個完整的可視控件以說明 Visual Basic NET 的後一種功能該控件是一個紅綠燈 一個包含三個圓(分別代表紅黃綠三個燈)的矩形圖 顯示各個燈亮時該控件的外觀控件的背景顏色設置為系統顏色 ControlDark
圖 帶有三個 TrafficLight 控件的窗體每個控件亮不同的燈
我們稱它為 TrafficLight 控件它可以通過代碼或讓用戶單擊燈來改變亮起的燈
因為 TrafficLight 是一個可視的 Windows 窗體控件它將繼承 S ystemWindowsForms 命名空間中的 Control 類這樣它將具有很多預定義的屬性方法和事件包括控制其外觀的屬性如 ForeColorBackColorSize 和 Location還包括事件如 MouseOver 和 Click您可以查看 NET 文檔獲得 Control 類成員的完整列表
紅綠燈也需要具有特殊的屬性和事件如下所示
Status 屬性 |
確定亮起哪種顏色的燈
必須為以下三個枚舉值之一
StatusRed
紅燈亮
StatusYellow
黃燈亮
StatusGreen
綠燈亮
BorderWidth 屬性 |
紅綠燈周圍邊框的寬度
StatusChanged 事件 |
當通過代碼或由用戶單擊不同的燈改變 Status 屬性的值時
觸發該事件
由於這些成員不屬於 Control 基類所以我們需要包括完整的代碼以處理它們我們還需要繪制邊框和三個相應顏色的燈的代碼以便在屏幕上繪制紅綠燈最後我們需要處理用戶單擊圓以更改亮起燈的操作並在更改亮起的燈時更改 Status 屬性
為了使本示例盡可能接近實際應用環境我們還將包括能夠確保在 Visual Studio® NET IDE 中更好地使用控件的代碼我們為工具箱設置適當的圖標並包括能夠使屬性更好地與各屬性窗口集成的邏輯
現在讓我們開始吧
第 步創建類型正確的項目
要創建一個保存 Windows 窗體控件的庫需要在 Visual 中啟動一個新項目選擇 Windows Control Library(Windows 控件庫)項目類型然後將項目命名為 MyControls
所創建的項目實際上可以保存多個 Windows 窗體控件每個控件都屬於其各自的類但我們只需在其中創建一個控件
第 步更改基類
在控件庫中創建的類自動命名為 UserControl默認情況下從 UserControl 類繼承如果我們要創建復合控件那非常容易只需將其他控件從工具箱中拖到設計表面上即可
但是由於我們要從頭創建自己的控件因此需要做一些更改將控件類的名稱從 UserControl 更改為 TrafficLight然後將以下行
Inherits SystemWindowsFormsUserControl
更改為
Inherits SystemWindowsFormsControl
這樣使最一般的 Control 類成為基類您會發現不再顯示可視設計表面而是替換為組件設計表面
為保持代碼的一致性也要將代碼文件名從 UserControlVB 更改為 TrafficLightvb可以在 Solution Explorer(解決方案資源管理器)中進行更改右鍵單擊代碼文件的名稱並選擇 Rename(重命名)
還需要在類模塊的頂部添加幾行代碼將 Option Strict 設置為 On並導入包含我們將來要用到的某些屬性的命名空間下面是要放到代碼最上面的兩行 Option Strict On
Imports SystemComponentModel
第 步實現屬性和事件
要實現 Status 屬性首先要為可能的屬性值創建枚舉將以下幾行插入以 Inherits 開始的行下面
Public Enum TrafficLightStatus
statusRed =
statusYellow =
statusGreen =
End Enum
此枚舉是公開的也就是說使用該控件的窗體可以訪問它
在這些行下面添加以下三行
Dim mStatus As TrafficLightStatus = TrafficLightStatusstatusGreen
Dim msngBorderWidth As Single = !
Public Event StatusChanged(ByVal NewStatus As TrafficLightStatus)
前兩行中的兩個變量可用於存儲 Status 和 BorderWidth 屬性的屬性值還為這些屬性設置了默認值保存 BorderWidth 的變量必須為 Single 類型因為它是繪制邊框所用的圖形語句需要的類型默認值中的驚歎號也表明它是 Single 類型此集合中的最後一行聲明了 StatusChanged 事件
現在我們為 BorderWidth 屬性編寫代碼在標記為 Windows Form Designer Generated Code(Windows 窗體設計器生成的代碼)的代碼區域下插入以下行
<DefaultValue(!) _
Description(紅綠燈周圍邊框的寬度)> _
Public Property BorderWidth() As Single
Get
Return msngBorderWidth
End Get
Set(ByVal Value As Single)
If msngBorderWidth <> Value Then
msngBorderWidth = Value
MeInvalidate()
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)
MeInvalidate()
End If
End Set
End Property
看起來與 BorderWidth 屬性的實現類似只有一點不同當 Status 屬性發生改變時除了強制重新繪制控件外還會觸發 StatusChanged 事件
要在 Properties(屬性)窗口中處理屬性的自動重置我們需要使用一種特殊的方法由於我們的屬性命名為 Status因此必須將重置方法命名為 ResetStatus重置方法只是恢復屬性的默認值以下是其代碼
Public
Sub ResetStatus()
MeStatus = TrafficLightStatusstatusGreen
End Sub
為了提示設計器何時需要包括一行代碼以便設置 Status 屬性我們需要包括一個名為 ShouldSerializeStatus 的方法當屬性需要一行代碼時此方法返回布爾值 True否則則返回 False以下是其代碼
Public Function ShouldSerializeStatus() As Boolean
If mStatus = TrafficLightStatusstatusGreen Then
Return False
Else
Return True
End If
End Function
第 步繪制控件的外觀
要使控件具有一個可視的外觀我們需要在 Paint 事件中放置邏輯然後每次控件需要刷新其可視外觀時就會運行該邏輯
Windows 窗體中的 Paint 邏輯使用 中 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 _
SystemWindowsFormsPaintEventArgs)
MyBaseOnPaint(pe)
Dim grfGraphics As SystemDrawingGraphics
grfGraphics = peGraphics
首先繪制三個代表燈的圓
一個亮起其余兩個熄滅
DrawLight(TrafficLightStatusstatusGreen grfGraphics)
DrawLight(TrafficLightStatusstatusYellow grfGraphics)
DrawLight(TrafficLightStatusstatusRed grfGraphics)
現在繪制紅綠燈周圍的輪廓
用畫筆繪制輪廓將它塗成黑色
Dim penDrawingPen As New _
SystemDrawingPen(SystemDrawingColorBlack msngBorderWidth)
在控件上繪制紅綠燈的輪廓
首先定義要繪制的矩形
Dim rectBorder As SystemDrawingRectangle
rectBorderX =
rectBorderY =
rectBorderHeight = MeHeight
rectBorderWidth = MeWidth
grfGraphicsDrawRectangle(penDrawingPen rectBorder)
釋放圖形對象
penDrawingPenDispose()
grfGraphicsDispose()
End Sub
首先使用基類繪制它通常使用控件的背景顏色繪制背景然後從事件參數中獲取控件的 Graphics 對象
接下來用一個函數畫出三個圓有關該函數的內容稍後介紹請注意我們必須向該函數傳遞一個 Graphics 對象的引用同時還要指示要畫的圓(紅黃綠)
然後是繪制輪廓的代碼聲明一個具有適當位置和大小的矩形然後傳遞給 Graphics 對象的 DrawRectangle 方法
最後圖形對象激活其 Dispose 方法使用 GDI+ 時最好在完成圖形對象後立即釋放它們這有助於清除操作系統繪圖時所用的資源如果要在 Windows® 或 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(MeSizeWidth * )
nCircleDiameter = CInt(MeSizeWidth * )
Select Case LightToDraw
Case TrafficLightStatusstatusRed
If LightToDraw = MeStatus Then
nCircleColor = ColorOrangeRed
Else
nCircleColor = ColorMaroon
End If
nCircleY = CInt(MeSizeHeight * )
Case TrafficLightStatusstatusYellow
If LightToDraw = MeStatus Then
nCircleColor = ColorYellow
Else
nCircleColor = ColorTan
End If
nCircleY = CInt(MeSizeHeight * )
Case TrafficLightStatusstatusGreen
If LightToDraw = MeStatus Then
nCircleColor = ColorLimeGreen
Else
nCircleColor = ColorForestGreen
End If
nCircleY = CInt(MeSizeHeight * )
End Select
Dim bshBrush As SystemDrawingBrush
If LightToDraw = MeStatus Then
bshBrush = New SolidBrush(nCircleColor)
Else
bshBrush = New SolidBrush(ColorFromArgb( nCircleColor))
End If
繪制代表紅綠燈的圓
grfGraphicsFillEllipse(bshBrush nCircleX nCircleY nCircleDiameter nCircleDiameter)
釋放筆刷
bshBrushDispose()
End Sub
這是整個控件中唯一的一個復雜圖形在 GDI+ 中在要繪制橢圓的矩形中指定左上角的 X 坐標和 Y 坐標然後指定矩形的高度和寬度即可繪制一個橢圓我們分別將 X 坐標和 Y 坐標稱為 nCircleX 和 nCircleY因為我們要繪制一個圓因此矩形的高度等於寬度用變量 nCircleDiameter 來控制該值
將 nCircleX 設置為剛好放到控件內(控件的寬度乘以 )nCircleY 取決於要繪制哪個燈可以設置成靠近控件的頂部(紅燈)大約向下三分之一(黃燈)或大約向下三分之二(綠燈)直徑 nCircleDiameter 設置為等於控件寬度的 %
要繪制實心橢圓還需完成一件事即確定要使用的顏色顏色取決於正在繪制哪個燈以及正在繪制的燈是否亮起亮起的燈的顏色要比熄滅的燈的顏色亮
創建繪圖要使用的筆刷時需要使用這些顏色如果正在繪制的燈是亮起的即使用該顏色如果繪制的燈是熄滅的則要使用不同的方法實例化筆刷下面是熄滅的燈所使用筆刷的代碼行
bshBrush = New SolidBrush(ColorFromArgb( nCircleColor))
這並不是 NET 中較好的方法名但 FromArgB 方法的作用是創建筆刷並通過將筆刷與背景顏色相結合來淡化顏色第一個參數使用的數字介於 至 之間數字越小背景顏色滲透越深我們使用的值為 它將大大降低處於熄滅狀態的燈的顏色您可以嘗試對該參數使用不同的值(或將它設置成可設置屬性)以獲得不同的效果
最後Graphics 對象的 DrawEllipse 方法繪制出該圓函數結束記住該函數需要調用三次以繪制三個不同的圓
第 步使控件響應用戶
要允許用戶更改燈的顏色必須檢測到用戶的鼠標單擊操作有經驗的 Visual Basic 開發人員都知道可以使用多種方法實現這一目的我們使用最簡單的一種方法即檢測 MouseUp 事件下面是檢測用戶單擊並更改 Status 屬性以與之匹配的代碼
Private Sub TrafficLight_MouseUp(ByVal sender As Object _
ByVal e As SystemWindowsFormsMouseEventArgs) _
Handles MyBaseMouseUp
Dim nMidPointX As Integer = CInt(MeSizeWidth * )
Dim nCircleRadius As Integer = nMidPointX
If Distance(eX eY nMidPointX CInt(MeSizeHeight / )) _
< nCircleRadius Then
MeStatus = TrafficLightStatusstatusRed
Exit Sub
End If
If Distance(eX eY nMidPointX CInt(MeSizeHeight / )) _
< nCircleRadius Then
MeStatus = TrafficLightStatusstatusYellow
Exit Sub
End If
If Distance(eX eY nMidPointX CInt(( * MeSizeHeight) / )) _
< nCircleRadius Then
MeStatus = TrafficLightStatusstatusGreen
End If
End Sub
Private Function Distance(ByVal X As Integer _
ByVal Y As Integer _
ByVal X As Integer _
ByVal y As Integer) As Integer
Return CInt(SystemMathSqrt((X X) ^ + (Y y) ^ ))
End Function
事件處理非常簡單檢查鼠標單擊的位置和每個圓心之間的距離(請注意圓心分別位於控件下方 // 和 / 的位置如果不太明白可以在紙上畫出來看看)如果計算出的距離小於圓的半徑則更改 Status 屬性
距離由 Distance 函數使用您可能在代數課中學過的公式計算請注意平方根函數是從 SystemMath 命名空間中獲得的數學函數通常都保存在該命名空間中
第 步清理
為了使控件順利地運作我們還需要執行一些其他操作例如大小改變時需要重新繪制控件而且為了不改變控件的比例我們需要檢測影響大小的屬性發生更改的時間然後強制寬度等於高度的三分之一下面是完成這兩項任務的事件處理程序
Private Sub TrafficLight_Resize(ByVal sender As Object _
ByVal e As SystemEventArgs) Handles MyBaseResize
MeInvalidate()
End Sub
Private Sub TrafficLight_Layout(ByVal sender As Object _
ByVal e As SystemWindowsFormsLayoutEventArgs) _
Handles MyBaseLayout
Select Case eAffectedProperty
Case Bounds
MeWidth = CInt(MeHeight * )
Case Else
不執行任何操作
End Select
End Sub
最後設置控件在工具箱中使用的圖標控件已經有一個看似齒輪的默認圖標但是我們要使用 Visual 附帶的紅綠燈圖標
控件的工具箱圖標是由名為 ToolboxBitmap 的類中的特性設置的在以 Public Class 開始的行上面插入以下行
<ToolboxBitmap(C:\Program Files\Microsoft Visual Studio
NET\Common\Graphics\icons\Traffic\TRFFCICO)> _
注意所有內容都應在一行中為了便於閱讀我們在 Studio 後放置了一個回車粘貼該代碼時要確保它們位於一行中Studio 和 NET 之間只需一個空格並刪除回車如果您已經將 Visual Studio NET 安裝到其默認位置那麼上述代碼將用 Visual Studio 目錄中的圖標設置該特性如果您沒有將 Visual Studio NET 安裝到其默認位置則需要相應地更改圖標的路徑名
第 步生成和測試控件
現在 TrafficLight 控件的設計就完成了選擇 Build | Build MyControls(生成 | 生成 MyControls)以創建最終的控件庫
要測試控件我們需要一個 Windows 窗體項目您可以在其他解決方案中執行此操作但在開發控件所用的解決方案中執行會更容易從菜單中選擇 File | Add Project | New Project(文件 | 添加項目 | 新項目)選擇 Windows Application(Windows 應用程序)項目類型將項目命名為 TestTrafficLight單擊 OK(確定)啟動測試所需的 Windows 應用程序
必須先將 TrafficLight 控件放到工具箱中才能將其拖放到測試應用程序的空白窗體 中右鍵單擊工具箱中的 Windows 窗體選項卡然後選擇 Customize Toolbox(自定義工具箱)選擇 NET Framework Components(NET Framework 組件)選項卡然後單擊 Browse(浏覽)按鈕浏覽到您的 MyControls 項目所在的位置然後轉到該項目的 /bin 目錄選擇 MyControlsdll 組件並單擊 OK(確定)現在該對話框應如圖 所示
圖 在 Customize Toolbox(自定義工具箱)對話框中TrafficLight 控件被選中
您可以看到 TrafficLight 控件旁邊有一個復選標記單擊 OK(確定)按鈕在工具箱的 Windows Forms(Windows 窗體)選項卡上TrafficLight 控件將出現在控件列表的底部圖 顯示了底部為 TrafficLight 控件的工具箱
圖 工具箱底部的 TrafficLight 控件
現在您可以將 TrafficLight 控件拖放到 TestTrafficLight 的空白窗體 中默認情況下它被命名為 TrafficLight您可以調整控件的大小重新設置控件的屬性包括 Status 屬性該屬性有一個下拉菜單菜單中包含該屬性的三個可能的值請注意調整控件的大小或更改其屬性時控件將在設計器中自動刷新
要恢復屬性的默認值請將 Status 屬性更改為 statusRed然後右鍵單擊 Properties(屬性)窗口中的 Status(狀態)屬性並選擇 Reset(重置)如圖 所示該屬性將更改回 statusGreen如果將 BorderWidth 屬性設置為 之外的其他值也可以使用同樣的方法恢復其默認值
圖 Properties(屬性)窗口中 Status(狀態)屬性的 Reset(重置)選項請注意窗口底部有關 Status(狀態)屬性的說明
如果需要還可以為控件插入 StatusChanged 事件然後可以使用該事件中的以下代碼行查看更改後的狀態
MsgBox(新狀態為 & NewStatusToString)
要在操作中測試該控件您需要啟動 TestTrafficLight 項目此時它還不是該解決方案的啟動項目因此您需要解決它在 Solution Explorer(解決方案資源管理器)中右鍵單擊 Solution(解決方案)名稱 Solution Explorer(解決方案資源管理器)中的第一行選擇 Properties(屬性)然後將 Single Startup Project(單啟動項目)設置從 MyControls 更改為 TestTrafficLight然後單擊 OK(確定)
按 F 鍵啟動該項目將顯示帶有 TrafficLight 控件的窗體測試控件按下不同的燈查看它們是否亮起您還可以測試 BorderWidth 屬性嘗試在代碼中設置燈的 Status 屬性
小結
盡管 TrafficLight 是一個簡單的控件(雖然曾有開發人員要把它用到真實的項目中)但它卻顯示了開發復雜控件所需要的所有原理包括
在控件中添加屬性
使用默認值和說明使屬性與 Visual Studio IDE 協調
在 Paint 事件中插入邏輯以繪制控件
在繪圖邏輯中使用 GDI+
為控件設置位圖以便在工具箱中顯示
創建復雜控件的關鍵在於熟悉 GDI+ 的繪圖能力如果理解了 TrafficLight 繪制邊框和彩色圓的原理那麼您就有了一個好的起點關鍵是有了 Visual Basic NET即使象我這麼懶惰的程序員也能創建高級的 Windows 窗體
From:http://tw.wingwit.com/Article/program/net/201311/11983.html