逐步解說:建立利用 Visual Studio 設計階段功能的 Windows Form 控制項
自訂控制項的設計階段經驗,可以透過撰寫關聯的自訂設計工具來加強。
這個逐步解說會說明如何建立自訂控制項的自訂設計工具。您將會實作 MarqueeControl
型別和關聯的設計工具類別 (稱為 MarqueeControlRootDesigner
)。
MarqueeControl
型別會實作類似於劇院跑馬燈的顯示,具有動畫燈和閃爍的文字。
這個控制項的設計工具會和設計環境互動,以提供自訂設計階段經驗。有了自訂設計工具,您可以組合具有動畫燈和閃爍文字多種組合的自訂 MarqueeControl
實作。您可以在表單上使用組合的控制項,就像使用任何其他 Windows Form 控制項一樣。
逐步解說將說明的工作包括:
建立專案
建立控制項程式庫專案
參考自訂控制項專案
定義自訂控制項及其自訂設計工具
建立自訂控制項的執行個體
設定專案進行設計階段偵錯
實作自訂控制項
建立自訂控制項的子控制項
建立 MarqueeBorder 子控制項
建立自訂設計工具以遮蔽和篩選屬性
處理元件變更
加入設計工具動詞命令至自訂設計工具
建立自訂 UITypeEditor
測試設計工具中的自訂控制項
當完成時,您的自訂控制項看起來會如同下列所示:
如需完整的程式碼清單,請參閱 HOW TO:建立採用設計階段功能的 Windows Form 控制項。
注意事項 |
---|
根據您目前使用的設定或版本,您所看到的對話方塊與功能表指令可能會與 [說明] 中描述的不同。如果要變更設定,請從 [工具] 功能表中選擇 [匯入和匯出設定]。如需詳細資訊,請參閱 Visual Studio 設定。 |
必要條件
若要完成這個逐步解說,您必須要有:
- 擁有足夠的使用權限,可以在安裝 Visual Studio 的電腦上建立並執行 Windows Form 應用程式專案。
建立專案
首先建立應用程式物件。您將會使用這個專案來建置裝載自訂控制項的應用程式。
若要建立專案
- 建立名為 "MarqueeControlTest" 的 Windows 應用程式專案。如需詳細資訊,請參閱 HOW TO:建立 Windows 應用程式專案。
建立控制項程式庫專案
下一步就是建立控制項程式庫專案。您將建立新的自訂控制項及其對應的自訂設計工具。
若要建立控制項程式庫專案
加入 Windows 控制項程式庫專案至方案。如需詳細資訊,請參閱加入新的專案對話方塊。將專案命名為 "MarqueeControlLibrary"。
使用 [方案總管],刪除名為 "UserControl1.cs" 或 "UserControl1.vb" (依據您選擇的語言而定) 的原始程式檔 (Source File),藉此刪除專案的預設控制項。如需詳細資訊,請參閱 HOW TO:移除、刪除和排除項目。
將新的 UserControl 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "MarqueeControl" 給新的原始程式檔。
使用 [方案總管],在 MarqueeControlLibrary 專案中建立新資料夾。如需詳細資訊,請參閱 HOW TO:加入新專案項目。將新資料夾命名為 "Design"。
以滑鼠右鍵按一下 [Design] 資料夾,並加入新類別。提供主檔名 "MarqueeControlRootDesigner" 給新的原始程式檔。
您將會需要使用 System.Design 組件的型別,所以請加入此參考至 MarqueeControlTest 專案。如需詳細資訊,請參閱 HOW TO:在 Visual Studio 中新增或移除參考 (C#、J#)。
參考自訂控制項專案
您將使用 MarqueeControlTest 專案以測試自訂控制項。在您加入專案參考至 MarqueeControlLibrary 組件時,測試專案會留意自訂控制項。
若要參考自訂控制項專案
- 在 MarqueeControlTest 專案中,加入專案參考至 MarqueeControlLibrary 組件。請確認有使用 [加入參考] 對話方塊中的 [屬性] 索引標籤,而非直接參考 MarqueeControlLibrary 組件。
定義自訂控制項及其自訂設計介面
您的自訂控制項將衍生自 UserControl 類別。這能讓您的控制項包含其他控制項,且提供您的控制項許多預設功能。
您的自訂控制項將會有關聯的自訂設計工具。這能讓您建立特別為自訂控制項量身訂做的獨特設計經驗。
您可透過使用 DesignerAttribute 類別,將控制項與設計工具相關聯。因為您是開發自訂控制項的整個設計階段行為,所以自訂控制項將會實作 IRootDesigner 介面。
若要定義自訂控制項及其自訂設計工具
在 [程式碼編輯器] 中,開啟
MarqueeControl
來源檔。在檔案頂端匯入下列命名空間:Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Drawing Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Windows.Forms; using System.Windows.Forms.Design;
請將 DesignerAttribute 屬性加入至
MarqueeControl
類別宣告 (Class Declaration)。這會將自訂控制項與設計工具相關聯。<Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _ GetType(IRootDesigner))> _ Public Class MarqueeControl Inherits UserControl
[Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )] public class MarqueeControl : UserControl {
在 [程式碼編輯器] 中,開啟
MarqueeControlRootDesigner
來源檔。在檔案頂端匯入下列命名空間:Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing.Design Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing.Design; using System.Windows.Forms; using System.Windows.Forms.Design;
將
MarqueeControlRootDesigner
的宣告變更為繼承自 DocumentDesigner 類別。套用 ToolboxItemFilterAttribute 以指定與 [工具箱] 進行的設計工具互動。注意:
MarqueeControlRootDesigner
類別的定義放在名為 "MarqueeControlLibrary.Design" 的命名空間中。這個宣告會將設計工具放在為設計相關型別所保留的特殊命名空間中。Namespace MarqueeControlLibrary.Design <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _ Public Class MarqueeControlRootDesigner Inherits DocumentDesigner
namespace MarqueeControlLibrary.Design { [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] public class MarqueeControlRootDesigner : DocumentDesigner {
定義
MarqueeControlRootDesigner
類別的建構函式。在建構函式主體中插入 WriteLine 陳述式。這對於偵錯用途將會非常有用。Public Sub New() Trace.WriteLine("MarqueeControlRootDesigner ctor") End Sub
public MarqueeControlRootDesigner() { Trace.WriteLine("MarqueeControlRootDesigner ctor"); }
建立自訂控制項的執行個體
若要觀察控制項的自訂設計階段行為,您要將控制項的執行個體放置在 MarqueeControlTest 專案中的表單上。
若要建立自訂控制項的執行個體
將新的 UserControl 項目加入至 MarqueeControlTest 專案。提供主檔名 "DemoMarqueeControl" 給新的原始程式檔。
在 [程式碼編輯器] 中,開啟
DemoMarqueeControl
檔案。在檔案的頂端匯入 MarqueeControlLibrary 命名空間:
Imports MarqueeControlLibrary
using MarqueeControlLibrary;
設定專案進行設計階段偵錯
您在開發自訂設計階段經驗時,將需要偵錯控制項和元件。有一種簡單的方法可以設定您的專案以允許設計階段時的偵錯。如需詳細資訊,請參閱逐步解說:在設計階段偵錯自訂的 Windows Form 控制項。
若要設定設計階段偵錯的專案
以滑鼠右鍵按一下 MarqueeControlLibrary 專案,然後選取 [屬性]。
在 [MarqueeControlLibrary 屬性頁] 對話方塊中選取 [組態屬性] 頁。
在 [起始動作] 區段中選取 [起始外部程式]。您將會偵錯個別的 Visual Studio 執行個體,所以請按一下省略 () 按鈕,以瀏覽 Visual Studio IDE。可執行檔的名稱為 devenv.exe,如果您安裝至預設位置,其路徑就會是 "C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe"。
按一下 [確定] 以關閉對話方塊。
以滑鼠右鍵按一下 MarqueeControlLibrary 專案,然後選取 [設定為啟始專案],以啟用這個偵錯設定。
檢查點
您現在已準備好偵錯自訂控制項的設計階段行為。一旦您決定偵錯環境已設定正確,就可測試自訂控制項和自訂設計介面之間的關聯。
若要測試偵錯環境和設計工具關聯
請開啟 [程式碼編輯器] 中的
MarqueeControlRootDesigner
,並在 WriteLine 陳述式上放置中斷點。按 F5 以啟動偵錯工作階段。請注意,會建立新的 Visual Studio 執行個體。
在新的 Visual Studio 執行個體中開啟 "MarqueeControlTest" 方案。您可以輕易地找到方案,方法是從 [檔案] 功能表中選取 [最近使用的專案]。"MarqueeControlTest.sln" 方案檔將會列為最近使用的檔案。
在設計工具中開啟
DemoMarqueeControl
。請注意,Visual Studio 的偵錯執行個體需要焦點,且執行會停止於中斷點。按下 F5 以繼續偵錯工作階段。
此時一切都已就緒,您可開發和偵錯自訂控制項及其關聯的自訂設計工具。這個逐步解說剩下的內容,將會集中在控制項及設計工具的實作功能詳細資料。
實作自訂控制項
MarqueeControl
是具有少量自訂的 UserControl。它會公開 (Expose) 兩種方法:Start
(啟動跑馬燈動畫) 以及 Stop
(停止動畫)。因為 MarqueeControl
包含實作 IMarqueeWidget
介面的子控制項,所以 Start
和 Stop
會在實作 IMarqueeWidget
的子控制項上,列舉每個子控制項並分別呼叫 StartMarquee
和 StopMarquee
方法。
MarqueeBorder
和 MarqueeText
控制項的外觀是取決於配置,所以 MarqueeControl
會在這種型別的子控制項上,覆寫 OnLayout 方法並呼叫 PerformLayout。
這是 MarqueeControl
自訂的範圍。執行階段功能是由 MarqueeBorder
和 MarqueeText
控制項實作,而設計階段功能則是由 MarqueeBorderDesigner
和 MarqueeControlRootDesigner
類別實作。
若要實作自訂控制項
在 [程式碼編輯器] 中,開啟
MarqueeControl
來源檔。實作Start
和Stop
方法。Public Sub Start() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so ' find each IMarqueeWidget child and call its ' StartMarquee method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl End Sub Public Sub [Stop]() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl End Sub
public void Start() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so // find each IMarqueeWidget child and call its // StartMarquee method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } } public void Stop() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } }
覆寫 OnLayout 方法。
Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs) MyBase.OnLayout(levent) ' Repaint all IMarqueeWidget children if the layout ' has changed. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) cntrl.PerformLayout() End If Next cntrl End Sub
protected override void OnLayout(LayoutEventArgs levent) { base.OnLayout (levent); // Repaint all IMarqueeWidget children if the layout // has changed. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { Control control = cntrl as Control; control.PerformLayout(); } } }
建立自訂控制項的子控制項
MarqueeControl
將會裝載兩種子控制項:MarqueeBorder
控制項和 MarqueeText
控制項。
MarqueeBorder
:此控制項會在邊緣周圍繪製「燈號」(Lights) 的框線。燈號會依序閃爍,所以看起來像是沿著邊框移動。燈號閃爍的速度是由稱為UpdatePeriod
的屬性控制。數種其他的自訂屬性會決定控制項外觀的其他方面。稱為StartMarquee
和StopMarquee
的兩種方法會控制動畫啟動和停止的時機。MarqueeText
:此控制項會繪製閃爍的字串。和MarqueeBorder
控制項一樣,文字閃爍的速度是由UpdatePeriod
屬性所控制。MarqueeText
控制項和MarqueeBorder
控制項相同,也有StartMarquee
和StopMarquee
方法。
在執行階段,MarqueeControlRootDesigner
允許這兩種控制項型別以任何組合方式加入至 MarqueeControl
。
兩個控制項的通用功能會納入稱為 IMarqueeWidget
的介面中。這可讓 MarqueeControl
探索任何跑馬燈相關的子控制項,並給予它們特殊處理。
若要實作週期性動畫功能,您可從 System.ComponentModel 命名空間使用 BackgroundWorker 物件。您可以使用 Timer 物件,但是有許多 IMarqueeWidget
物件時,單一的 UI 執行緒可能就無法跟上動畫的速度。
若要建立自訂控制項的子控制項
將新的類別項目加入至 MarqueeControlLibrary 專案。提供主檔名 "IMarqueeWidget" 給新的原始程式檔。
開啟 [程式碼編輯器] 的
IMarqueeWidget
原始程式檔,並將 class 的宣告變更為 interface:' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget
// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget {
加入下列程式碼至
IMarqueeWidget
介面,以公開管理跑馬燈動畫的兩種方法和一個屬性:' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget ' This method starts the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StartMarquee on all ' its IMarqueeWidget child controls. Sub StartMarquee() ' This method stops the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StopMarquee on all ' its IMarqueeWidget child controls. Sub StopMarquee() ' This method specifies the refresh rate for the animation, ' in milliseconds. Property UpdatePeriod() As Integer End Interface
// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget { // This method starts the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StartMarquee on all // its IMarqueeWidget child controls. void StartMarquee(); // This method stops the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StopMarquee on all // its IMarqueeWidget child controls. void StopMarquee(); // This method specifies the refresh rate for the animation, // in milliseconds. int UpdatePeriod { get; set; } }
將新的 [自訂控制項] 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "MarqueeText" 給新的原始程式檔。
從 [工具箱] 將 BackgroundWorker 元件拖曳至
MarqueeText
控制項。此元件允許MarqueeText
控制項非同步自我更新。在 [屬性] 視窗中,將 BackgroundWorker 元件的 WorkerReportsProgess 和 WorkerSupportsCancellation 屬性設定為 true。這些設定能讓 BackgroundWorker 元件週期性地引發 ProgressChanged 事件,並取消非同步更新。如需詳細資訊,請參閱 BackgroundWorker 元件。
在 [程式碼編輯器] 中,開啟
MarqueeText
來源檔。在檔案頂端匯入下列命名空間:Imports System Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
將
MarqueeText
的宣告變更為繼承自 Label 以及實作IMarqueeWidget
介面:<ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeText Inherits Label Implements IMarqueeWidget
[ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] public partial class MarqueeText : Label, IMarqueeWidget {
實作對應至公開屬性的執行個體變數,並在建構函式中初始化它們。
isLit
欄位會決定是否要用LightColor
屬性提供的色彩繪製文字。' When isLit is true, the text is painted in the light color; ' When isLit is false, the text is painted in the dark color. ' This value changes whenever the BackgroundWorker component ' raises the ProgressChanged event. Private isLit As Boolean = True ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightColorValue As Color Private darkColorValue As Color ' These brushes are used to paint the light and dark ' colors of the text. Private lightBrush As Brush Private darkBrush As Brush ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) End Sub 'New
// When isLit is true, the text is painted in the light color; // When isLit is false, the text is painted in the dark color. // This value changes whenever the BackgroundWorker component // raises the ProgressChanged event. private bool isLit = true; // These fields back the public properties. private int updatePeriodValue = 50; private Color lightColorValue; private Color darkColorValue; // These brushes are used to paint the light and dark // colors of the text. private Brush lightBrush; private Brush darkBrush; // This component updates the control asynchronously. private BackgroundWorker backgroundWorker1; public MarqueeText() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); }
實作
IMarqueeWidget
介面。StartMarquee
和StopMarquee
方法會叫用 BackgroundWorker 元件的 RunWorkerAsync 和 CancelAsync 方法,以啟動和停止動畫。Category 和 Browsable 屬性 (Attribute) 會套用至
UpdatePeriod
屬性 (Property),使它出現在 [屬性] 視窗的自訂區段 (此區段名為 "Marquee") 中。Public Overridable Sub StartMarquee() _ Implements IMarqueeWidget.StartMarquee ' Start the updating thread and pass it the UpdatePeriod. Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod) End Sub Public Overridable Sub StopMarquee() _ Implements IMarqueeWidget.StopMarquee ' Stop the updating thread. Me.backgroundWorker1.CancelAsync() End Sub <Category("Marquee"), Browsable(True)> _ Public Property UpdatePeriod() As Integer _ Implements IMarqueeWidget.UpdatePeriod Get Return Me.updatePeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.updatePeriodValue = Value Else Throw New ArgumentOutOfRangeException("UpdatePeriod", "must be > 0") End If End Set End Property
public virtual void StartMarquee() { // Start the updating thread and pass it the UpdatePeriod. this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod); } public virtual void StopMarquee() { // Stop the updating thread. this.backgroundWorker1.CancelAsync(); } [Category("Marquee")] [Browsable(true)] public int UpdatePeriod { get { return this.updatePeriodValue; } set { if (value > 0) { this.updatePeriodValue = value; } else { throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0"); } } }
實作屬性存取子。您將公開兩個屬性給用戶端:
LightColor
和DarkColor
。Category 和 Browsable 屬性 (Attribute) 會套用至這些屬性 (Property),使它出現在 [屬性] 視窗的自訂區段 (此區段名為 "Marquee") 中。<Category("Marquee"), Browsable(True)> _ Public Property LightColor() As Color Get Return Me.lightColorValue End Get Set(ByVal Value As Color) ' The LightColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then Me.lightColorValue = Value Me.lightBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property DarkColor() As Color Get Return Me.darkColorValue End Get Set(ByVal Value As Color) ' The DarkColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then Me.darkColorValue = Value Me.darkBrush = New SolidBrush(Value) End If End Set End Property
[Category("Marquee")] [Browsable(true)] public Color LightColor { get { return this.lightColorValue; } set { // The LightColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.lightColorValue.ToArgb() != value.ToArgb()) { this.lightColorValue = value; this.lightBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public Color DarkColor { get { return this.darkColorValue; } set { // The DarkColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.darkColorValue.ToArgb() != value.ToArgb()) { this.darkColorValue = value; this.darkBrush = new SolidBrush(value); } } }
實作 BackgroundWorker 元件的 DoWork 和 ProgressChanged 事件的處理常式。
DoWork 事件處理常式會休眠
UpdatePeriod
所指定的毫秒數,然後引發 ProgressChanged 事件,直到程式碼藉由呼叫 CancelAsync 來停止動畫。ProgressChanged 事件處理常式會切換亮和暗的狀態,以提供閃爍的外觀。
' This method is called in the worker thread's context, ' so it must not make any calls into the MarqueeText control. ' Instead, it communicates to the control using the ' ProgressChanged event. ' ' The only work done in this event handler is ' to sleep for the number of milliseconds specified ' by UpdatePeriod, then raise the ProgressChanged event. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles backgroundWorker1.DoWork Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) ' This event handler will run until the client cancels ' the background task by calling CancelAsync. While Not worker.CancellationPending ' The Argument property of the DoWorkEventArgs ' object holds the value of UpdatePeriod, which ' was passed as the argument to the RunWorkerAsync ' method. Thread.Sleep(Fix(e.Argument)) ' The DoWork eventhandler does not actually report ' progress; the ReportProgress event is used to ' periodically alert the control to update its state. worker.ReportProgress(0) End While End Sub ' The ProgressChanged event is raised by the DoWork method. ' This event handler does work that is internal to the ' control. In this case, the text is toggled between its ' light and dark state, and the control is told to ' repaint itself. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.isLit = Not Me.isLit Me.Refresh() End Sub
// This method is called in the worker thread's context, // so it must not make any calls into the MarqueeText control. // Instead, it communicates to the control using the // ProgressChanged event. // // The only work done in this event handler is // to sleep for the number of milliseconds specified // by UpdatePeriod, then raise the ProgressChanged event. private void backgroundWorker1_DoWork( object sender, System.ComponentModel.DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; // This event handler will run until the client cancels // the background task by calling CancelAsync. while (!worker.CancellationPending) { // The Argument property of the DoWorkEventArgs // object holds the value of UpdatePeriod, which // was passed as the argument to the RunWorkerAsync // method. Thread.Sleep((int)e.Argument); // The DoWork eventhandler does not actually report // progress; the ReportProgress event is used to // periodically alert the control to update its state. worker.ReportProgress(0); } } // The ProgressChanged event is raised by the DoWork method. // This event handler does work that is internal to the // control. In this case, the text is toggled between its // light and dark state, and the control is told to // repaint itself. private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e) { this.isLit = !this.isLit; this.Refresh(); }
覆寫 OnPaint 方法以啟用動畫。
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) ' The text is painted in the light or dark color, ' depending on the current value of isLit. Me.ForeColor = IIf(Me.isLit, Me.lightColorValue, Me.darkColorValue) MyBase.OnPaint(e) End Sub
protected override void OnPaint(PaintEventArgs e) { // The text is painted in the light or dark color, // depending on the current value of isLit. this.ForeColor = this.isLit ? this.lightColorValue : this.darkColorValue; base.OnPaint(e); }
按 F6 建置方案。
建立 MarqueeBorder 子控制項
MarqueeBorder
控制項是比 MarqueeText
控制項略微複雜一點的控制項。它具有較多的屬性,而在 OnPaint 方法中的動畫更為複雜。基本上,它和 MarqueeText
控制項十分類似。
因為 MarqueeBorder
控制項能擁有子控制項,所以需要留意 Layout 事件。
若要建立 MarqueeBorder 控制項
將新的 [自訂控制項] 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "MarqueeBorder" 給新的原始程式檔。
從 [工具箱] 將 BackgroundWorker 元件拖曳至
MarqueeBorder
控制項。這個元件允許MarqueeBorder
控制項非同步自我更新。在 [屬性] 視窗中,將 BackgroundWorker 元件的 WorkerReportsProgess 和 WorkerSupportsCancellation 屬性設定為 true。這些設定能讓 BackgroundWorker 元件週期性地引發 ProgressChanged 事件,並取消非同步更新。如需詳細資訊,請參閱 BackgroundWorker 元件。
在 [屬性] 視窗中,按一下 [事件] 按鈕。附加 DoWork 和 ProgressChanged 事件的處理常式。
在 [程式碼編輯器] 中,開啟
MarqueeBorder
來源檔。在檔案頂端匯入下列命名空間:Imports System Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Drawing.Design Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Drawing.Design; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
變更
MarqueeBorder
的宣告為繼承自 Panel,並實作IMarqueeWidget
介面。<Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeBorder Inherits Panel Implements IMarqueeWidget
[Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] public partial class MarqueeBorder : Panel, IMarqueeWidget {
宣告管理
MarqueeBorder
控制項狀態的兩個列舉型別:MarqueeSpinDirection
(決定燈號在框線周圍「旋轉」(Spin) 的方向) 以及MarqueeLightShape
(會決定燈號的形狀 (方形或圓形))。將這些宣告放置在MarqueeBorder
類別宣告之前。' This defines the possible values for the MarqueeBorder ' control's SpinDirection property. Public Enum MarqueeSpinDirection CW CCW End Enum ' This defines the possible values for the MarqueeBorder ' control's LightShape property. Public Enum MarqueeLightShape Square Circle End Enum
// This defines the possible values for the MarqueeBorder // control's SpinDirection property. public enum MarqueeSpinDirection { CW, CCW } // This defines the possible values for the MarqueeBorder // control's LightShape property. public enum MarqueeLightShape { Square, Circle }
宣告對應至公開屬性的執行個體變數,並在建構函式中初始化它們。
Public Shared MaxLightSize As Integer = 10 ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightSizeValue As Integer = 5 Private lightPeriodValue As Integer = 3 Private lightSpacingValue As Integer = 1 Private lightColorValue As Color Private darkColorValue As Color Private spinDirectionValue As MarqueeSpinDirection = MarqueeSpinDirection.CW Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square ' These brushes are used to paint the light and dark ' colors of the marquee lights. Private lightBrush As Brush Private darkBrush As Brush ' This field tracks the progress of the "first" light as it ' "travels" around the marquee border. Private currentOffset As Integer = 0 ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As System.ComponentModel.BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) ' The MarqueeBorder control manages its own padding, ' because it requires that any contained controls do ' not overlap any of the marquee lights. Dim pad As Integer = 2 * (Me.lightSizeValue + Me.lightSpacingValue) Me.Padding = New Padding(pad, pad, pad, pad) SetStyle(ControlStyles.OptimizedDoubleBuffer, True) End Sub
public static int MaxLightSize = 10; // These fields back the public properties. private int updatePeriodValue = 50; private int lightSizeValue = 5; private int lightPeriodValue = 3; private int lightSpacingValue = 1; private Color lightColorValue; private Color darkColorValue; private MarqueeSpinDirection spinDirectionValue = MarqueeSpinDirection.CW; private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square; // These brushes are used to paint the light and dark // colors of the marquee lights. private Brush lightBrush; private Brush darkBrush; // This field tracks the progress of the "first" light as it // "travels" around the marquee border. private int currentOffset = 0; // This component updates the control asynchronously. private System.ComponentModel.BackgroundWorker backgroundWorker1; public MarqueeBorder() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); // The MarqueeBorder control manages its own padding, // because it requires that any contained controls do // not overlap any of the marquee lights. int pad = 2 * (this.lightSizeValue + this.lightSpacingValue); this.Padding = new Padding(pad, pad, pad, pad); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); }
實作
IMarqueeWidget
介面。StartMarquee
和StopMarquee
方法會叫用 BackgroundWorker 元件的 RunWorkerAsync 和 CancelAsync 方法,以啟動和停止動畫。因為
MarqueeBorder
控制項可以包含子控制項,StartMarquee
方法會列舉所有子控制項,並在實作IMarqueeWidget
的這些子控制項上呼叫StartMarquee
。StopMarquee
方法具有類似的實作。Public Overridable Sub StartMarquee() _ Implements IMarqueeWidget.StartMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StartMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl ' Start the updating thread and pass it the UpdatePeriod. Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod) End Sub Public Overridable Sub StopMarquee() _ Implements IMarqueeWidget.StopMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl ' Stop the updating thread. Me.backgroundWorker1.CancelAsync() End Sub <Category("Marquee"), Browsable(True)> _ Public Overridable Property UpdatePeriod() As Integer _ Implements IMarqueeWidget.UpdatePeriod Get Return Me.updatePeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.updatePeriodValue = Value Else Throw New ArgumentOutOfRangeException("UpdatePeriod", _ "must be > 0") End If End Set End Property
public virtual void StartMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StartMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } // Start the updating thread and pass it the UpdatePeriod. this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod); } public virtual void StopMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } // Stop the updating thread. this.backgroundWorker1.CancelAsync(); } [Category("Marquee")] [Browsable(true)] public virtual int UpdatePeriod { get { return this.updatePeriodValue; } set { if (value > 0) { this.updatePeriodValue = value; } else { throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0"); } } }
實作屬性存取子。
MarqueeBorder
控制項具有控制外觀的數種屬性。<Category("Marquee"), Browsable(True)> _ Public Property LightSize() As Integer Get Return Me.lightSizeValue End Get Set(ByVal Value As Integer) If Value > 0 AndAlso Value <= MaxLightSize Then Me.lightSizeValue = Value Me.DockPadding.All = 2 * Value Else Throw New ArgumentOutOfRangeException("LightSize", _ "must be > 0 and < MaxLightSize") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightPeriod() As Integer Get Return Me.lightPeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.lightPeriodValue = Value Else Throw New ArgumentOutOfRangeException("LightPeriod", _ "must be > 0 ") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightColor() As Color Get Return Me.lightColorValue End Get Set(ByVal Value As Color) ' The LightColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then Me.lightColorValue = Value Me.lightBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property DarkColor() As Color Get Return Me.darkColorValue End Get Set(ByVal Value As Color) ' The DarkColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then Me.darkColorValue = Value Me.darkBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightSpacing() As Integer Get Return Me.lightSpacingValue End Get Set(ByVal Value As Integer) If Value >= 0 Then Me.lightSpacingValue = Value Else Throw New ArgumentOutOfRangeException("LightSpacing", _ "must be >= 0") End If End Set End Property <Category("Marquee"), Browsable(True), _ EditorAttribute(GetType(LightShapeEditor), _ GetType(System.Drawing.Design.UITypeEditor))> _ Public Property LightShape() As MarqueeLightShape Get Return Me.lightShapeValue End Get Set(ByVal Value As MarqueeLightShape) Me.lightShapeValue = Value End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property SpinDirection() As MarqueeSpinDirection Get Return Me.spinDirectionValue End Get Set(ByVal Value As MarqueeSpinDirection) Me.spinDirectionValue = Value End Set End Property
[Category("Marquee")] [Browsable(true)] public int LightSize { get { return this.lightSizeValue; } set { if (value > 0 && value <= MaxLightSize) { this.lightSizeValue = value; this.DockPadding.All = 2 * value; } else { throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize"); } } } [Category("Marquee")] [Browsable(true)] public int LightPeriod { get { return this.lightPeriodValue; } set { if (value > 0) { this.lightPeriodValue = value; } else { throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 "); } } } [Category("Marquee")] [Browsable(true)] public Color LightColor { get { return this.lightColorValue; } set { // The LightColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.lightColorValue.ToArgb() != value.ToArgb()) { this.lightColorValue = value; this.lightBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public Color DarkColor { get { return this.darkColorValue; } set { // The DarkColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.darkColorValue.ToArgb() != value.ToArgb()) { this.darkColorValue = value; this.darkBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public int LightSpacing { get { return this.lightSpacingValue; } set { if (value >= 0) { this.lightSpacingValue = value; } else { throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0"); } } } [Category("Marquee")] [Browsable(true)] [EditorAttribute(typeof(LightShapeEditor), typeof(System.Drawing.Design.UITypeEditor))] public MarqueeLightShape LightShape { get { return this.lightShapeValue; } set { this.lightShapeValue = value; } } [Category("Marquee")] [Browsable(true)] public MarqueeSpinDirection SpinDirection { get { return this.spinDirectionValue; } set { this.spinDirectionValue = value; } }
實作 BackgroundWorker 元件的 DoWork 和 ProgressChanged 事件的處理常式。
DoWork 事件處理常式會沉睡由
UpdatePeriod
所指定的毫秒數,然後會引發 ProgressChanged 事件,直到程式碼藉由呼叫 CancelAsync 來停止動畫。ProgressChanged 事件處理常式會遞增「基底」(Base) 燈號的位置 (其他燈號的亮/暗狀態將由此來決定),並且會呼叫 Refresh 方法,導致控制項重新自我繪製。
' This method is called in the worker thread's context, ' so it must not make any calls into the MarqueeBorder ' control. Instead, it communicates to the control using ' the ProgressChanged event. ' ' The only work done in this event handler is ' to sleep for the number of milliseconds specified ' by UpdatePeriod, then raise the ProgressChanged event. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles backgroundWorker1.DoWork Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) ' This event handler will run until the client cancels ' the background task by calling CancelAsync. While Not worker.CancellationPending ' The Argument property of the DoWorkEventArgs ' object holds the value of UpdatePeriod, which ' was passed as the argument to the RunWorkerAsync ' method. Thread.Sleep(Fix(e.Argument)) ' The DoWork eventhandler does not actually report ' progress; the ReportProgress event is used to ' periodically alert the control to update its state. worker.ReportProgress(0) End While End Sub ' The ProgressChanged event is raised by the DoWork method. ' This event handler does work that is internal to the ' control. In this case, the currentOffset is incremented, ' and the control is told to repaint itself. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.currentOffset += 1 Me.Refresh() End Sub
// This method is called in the worker thread's context, // so it must not make any calls into the MarqueeBorder // control. Instead, it communicates to the control using // the ProgressChanged event. // // The only work done in this event handler is // to sleep for the number of milliseconds specified // by UpdatePeriod, then raise the ProgressChanged event. private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; // This event handler will run until the client cancels // the background task by calling CancelAsync. while (!worker.CancellationPending) { // The Argument property of the DoWorkEventArgs // object holds the value of UpdatePeriod, which // was passed as the argument to the RunWorkerAsync // method. Thread.Sleep((int)e.Argument); // The DoWork eventhandler does not actually report // progress; the ReportProgress event is used to // periodically alert the control to update its state. worker.ReportProgress(0); } } // The ProgressChanged event is raised by the DoWork method. // This event handler does work that is internal to the // control. In this case, the currentOffset is incremented, // and the control is told to repaint itself. private void backgroundWorker1_ProgressChanged( object sender, System.ComponentModel.ProgressChangedEventArgs e) { this.currentOffset++; this.Refresh(); }
實作 Helper 方法:
IsLit
和DrawLight
。IsLit
方法會決定在指定位置上的燈號色彩。「亮」(Lit) 的燈號是以LightColor
屬性所提供的色彩進行繪製,而「暗」(Dark) 的那些燈號則是以DarkColor
屬性所提供的色彩進行繪製。DrawLight
方法會使用適當的色彩、形狀及位置繪製燈號。' This method determines if the marquee light at lightIndex ' should be lit. The currentOffset field specifies where ' the "first" light is located, and the "position" of the ' light given by lightIndex is computed relative to this ' offset. If this position modulo lightPeriodValue is zero, ' the light is considered to be on, and it will be painted ' with the control's lightBrush. Protected Overridable Function IsLit(ByVal lightIndex As Integer) As Boolean Dim directionFactor As Integer = _ IIf(Me.spinDirectionValue = MarqueeSpinDirection.CW, -1, 1) Return (lightIndex + directionFactor * Me.currentOffset) Mod Me.lightPeriodValue = 0 End Function Protected Overridable Sub DrawLight( _ ByVal g As Graphics, _ ByVal brush As Brush, _ ByVal xPos As Integer, _ ByVal yPos As Integer) Select Case Me.lightShapeValue Case MarqueeLightShape.Square g.FillRectangle( _ brush, _ xPos, _ yPos, _ Me.lightSizeValue, _ Me.lightSizeValue) Exit Select Case MarqueeLightShape.Circle g.FillEllipse( _ brush, _ xPos, _ yPos, _ Me.lightSizeValue, _ Me.lightSizeValue) Exit Select Case Else Trace.Assert(False, "Unknown value for light shape.") Exit Select End Select End Sub
// This method determines if the marquee light at lightIndex // should be lit. The currentOffset field specifies where // the "first" light is located, and the "position" of the // light given by lightIndex is computed relative to this // offset. If this position modulo lightPeriodValue is zero, // the light is considered to be on, and it will be painted // with the control's lightBrush. protected virtual bool IsLit(int lightIndex) { int directionFactor = (this.spinDirectionValue == MarqueeSpinDirection.CW ? -1 : 1); return ( (lightIndex + directionFactor * this.currentOffset) % this.lightPeriodValue == 0 ); } protected virtual void DrawLight( Graphics g, Brush brush, int xPos, int yPos) { switch (this.lightShapeValue) { case MarqueeLightShape.Square: { g.FillRectangle(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue); break; } case MarqueeLightShape.Circle: { g.FillEllipse(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue); break; } default: { Trace.Assert(false, "Unknown value for light shape."); break; } } }
覆寫 OnLayout 和 OnPaint 方法。
OnPaint 方法會沿著
MarqueeBorder
控制項的邊緣繪製燈號。因為 OnPaint 方法會依據
MarqueeBorder
控制項的維度 (Dimension) 而定,所以每次配置變更時,就需要呼叫它。若要達成這項作業,請覆寫 OnLayout 並呼叫 Refresh。Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs) MyBase.OnLayout(levent) ' Repaint when the layout has changed. Me.Refresh() End Sub ' This method paints the lights around the border of the ' control. It paints the top row first, followed by the ' right side, the bottom row, and the left side. The color ' of each light is determined by the IsLit method and ' depends on the light's position relative to the value ' of currentOffset. Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim g As Graphics = e.Graphics g.Clear(Me.BackColor) MyBase.OnPaint(e) ' If the control is large enough, draw some lights. If Me.Width > MaxLightSize AndAlso Me.Height > MaxLightSize Then ' The position of the next light will be incremented ' by this value, which is equal to the sum of the ' light size and the space between two lights. Dim increment As Integer = _ Me.lightSizeValue + Me.lightSpacingValue ' Compute the number of lights to be drawn along the ' horizontal edges of the control. Dim horizontalLights As Integer = _ (Me.Width - increment) / increment ' Compute the number of lights to be drawn along the ' vertical edges of the control. Dim verticalLights As Integer = _ (Me.Height - increment) / increment ' These local variables will be used to position and ' paint each light. Dim xPos As Integer = 0 Dim yPos As Integer = 0 Dim lightCounter As Integer = 0 Dim brush As Brush ' Draw the top row of lights. Dim i As Integer For i = 0 To horizontalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) xPos += increment lightCounter += 1 Next i ' Draw the lights flush with the right edge of the control. xPos = Me.Width - Me.lightSizeValue ' Draw the right column of lights. 'Dim i As Integer For i = 0 To verticalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) yPos += increment lightCounter += 1 Next i ' Draw the lights flush with the bottom edge of the control. yPos = Me.Height - Me.lightSizeValue ' Draw the bottom row of lights. 'Dim i As Integer For i = 0 To horizontalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) xPos -= increment lightCounter += 1 Next i ' Draw the lights flush with the left edge of the control. xPos = 0 ' Draw the left column of lights. 'Dim i As Integer For i = 0 To verticalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) yPos -= increment lightCounter += 1 Next i End If End Sub
protected override void OnLayout(LayoutEventArgs levent) { base.OnLayout(levent); // Repaint when the layout has changed. this.Refresh(); } // This method paints the lights around the border of the // control. It paints the top row first, followed by the // right side, the bottom row, and the left side. The color // of each light is determined by the IsLit method and // depends on the light's position relative to the value // of currentOffset. protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.Clear(this.BackColor); base.OnPaint(e); // If the control is large enough, draw some lights. if (this.Width > MaxLightSize && this.Height > MaxLightSize) { // The position of the next light will be incremented // by this value, which is equal to the sum of the // light size and the space between two lights. int increment = this.lightSizeValue + this.lightSpacingValue; // Compute the number of lights to be drawn along the // horizontal edges of the control. int horizontalLights = (this.Width - increment) / increment; // Compute the number of lights to be drawn along the // vertical edges of the control. int verticalLights = (this.Height - increment) / increment; // These local variables will be used to position and // paint each light. int xPos = 0; int yPos = 0; int lightCounter = 0; Brush brush; // Draw the top row of lights. for (int i = 0; i < horizontalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); xPos += increment; lightCounter++; } // Draw the lights flush with the right edge of the control. xPos = this.Width - this.lightSizeValue; // Draw the right column of lights. for (int i = 0; i < verticalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); yPos += increment; lightCounter++; } // Draw the lights flush with the bottom edge of the control. yPos = this.Height - this.lightSizeValue; // Draw the bottom row of lights. for (int i = 0; i < horizontalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); xPos -= increment; lightCounter++; } // Draw the lights flush with the left edge of the control. xPos = 0; // Draw the left column of lights. for (int i = 0; i < verticalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); yPos -= increment; lightCounter++; } } }
建立自訂設計工具以遮蔽和篩選屬性
MarqueeControlRootDesigner
類別提供根目錄設計工具的實作。除了在 MarqueeControl
上作業的這個設計工具以外,您將需要特別與 MarqueeBorder
控制項關聯的自訂設計工具。這個設計工具會提供在自訂根目錄設計工具的內容中適合的自訂行為。
特別是,MarqueeBorderDesigner
將會「遮蔽」(Shadow) 及篩選 MarqueeBorder
控制項中的屬性,變更它們與設計環境的互動。
攔截對元件的屬性存取子的呼叫稱為「遮蔽」(Shadowing)。這種行為能讓設計工具追蹤使用者所設定的值,並選擇性地傳遞該值至正在設計的元件。
對於這個範例,Visible 和 Enabled 屬性將會由 MarqueeBorderDesigner
遮蔽,讓使用者不會在執行階段時無法檢視 MarqueeBorder
控制項或將其停用。
設計工具也可以加入和移除屬性。對於這個範例,Padding 屬性會在設計階段移除,因為 MarqueeBorder
控制項會依據 LightSize
屬性所指定的燈號大小,以程式設計方式設定邊框距離。
MarqueeBorderDesigner
的基底類別是 ComponentDesigner,具有能夠變更在設計階段由控制項公開的屬性 (Attribute)、屬性 (Property) 及事件的方法:
當使用這些方法來變更元件的公用介面時,您必須遵循下列規則:
只加入或移除 PreFilter 方法中的項目
只修改 PostFilter 方法中的現有項目
永遠在 PreFilter 方法中最先呼叫基底實作
永遠在 PostFilter 方法中最後呼叫基底實作
遵守這些規則能確保設計階段環境中的所有設計工具,對於正在設計的所有元件都具有一致的檢視。
ComponentDesigner 類別提供的字典可管理遮蔽屬性的值,讓您不需為建立特定執行個體變數的需要而煩惱。
若要建立自訂設計工具以遮蔽和篩選屬性
以滑鼠右鍵按一下 [設計] 資料夾,並加入新類別。提供主檔名 "MarqueeBorderDesigner" 給原始程式檔。
在 [程式碼編輯器] 中,開啟
MarqueeBorderDesigner
來源檔。在檔案頂端匯入下列命名空間:Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Windows.Forms; using System.Windows.Forms.Design;
將
MarqueeBorderDesigner
的宣告變更為繼承自 ParentControlDesigner。因為
MarqueeBorder
控制項可包含子控制項,所以MarqueeBorderDesigner
會繼承自 ParentControlDesigner,這將能處理父子互動。Namespace MarqueeControlLibrary.Design <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _ Public Class MarqueeBorderDesigner Inherits ParentControlDesigner
namespace MarqueeControlLibrary.Design { [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] public class MarqueeBorderDesigner : ParentControlDesigner {
覆寫 PreFilterProperties 的基底實作。
Protected Overrides Sub PreFilterProperties( _ ByVal properties As IDictionary) MyBase.PreFilterProperties(properties) If properties.Contains("Padding") Then properties.Remove("Padding") End If properties("Visible") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Visible"), PropertyDescriptor), _ New Attribute(-1) {}) properties("Enabled") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Enabled"), _ PropertyDescriptor), _ New Attribute(-1) {}) End Sub
protected override void PreFilterProperties(IDictionary properties) { base.PreFilterProperties(properties); if (properties.Contains("Padding")) { properties.Remove("Padding"); } properties["Visible"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Visible"], new Attribute[0]); properties["Enabled"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Enabled"], new Attribute[0]); }
實作 Enabled 和 Visible 屬性。這些實作會遮蔽控制項的屬性。
Public Property Visible() As Boolean Get Return CBool(ShadowProperties("Visible")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Visible") = Value End Set End Property Public Property Enabled() As Boolean Get Return CBool(ShadowProperties("Enabled")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Enabled") = Value End Set End Property
public bool Visible { get { return (bool)ShadowProperties["Visible"]; } set { this.ShadowProperties["Visible"] = value; } } public bool Enabled { get { return (bool)ShadowProperties["Enabled"]; } set { this.ShadowProperties["Enabled"] = value; } }
處理元件變更
MarqueeControlRootDesigner
類別會提供 MarqueeControl
執行個體的自訂設計階段經驗。大部分的設計階段功能都是繼承自 DocumentDesigner 類別;您的程式碼將會實作這兩種特定自訂:處理元件變更及加入設計工具動詞命令。
當使用者設計他們的 MarqueeControl
執行個體時,您的根目錄設計工具將會追蹤變更至 MarqueeControl
及其子控制項。設計階段環境提供便利的服務 IComponentChangeService,可用來追蹤元件狀態的變更。
您可以使用 GetService 方法查詢環境,藉此取得對此服務的參考。如果查詢成功,您的設計工具就可以附加 ComponentChanged 事件的處理常式,並執行在設計階段維持一致狀態所需要的任何工作。
在 MarqueeControlRootDesigner
類別的情況下,您將會在 MarqueeControl
所包含的每個 IMarqueeWidget
物件上呼叫 Refresh 方法。如此一來,在變更如父代的 Size 之類的屬性時, IMarqueeWidget
控制項就會適當地重新自我繪製。
若要處理元件變更
在 [程式碼編輯器] 中開啟
MarqueeControlRootDesigner
原始程式檔,並覆寫 Initialize 方法。呼叫 Initialize 的基底實作,並查詢 IComponentChangeService。MyBase.Initialize(component) Dim cs As IComponentChangeService = _ CType(GetService(GetType(IComponentChangeService)), _ IComponentChangeService) If (cs IsNot Nothing) Then AddHandler cs.ComponentChanged, AddressOf OnComponentChanged End If
base.Initialize(component); IComponentChangeService cs = GetService(typeof(IComponentChangeService)) as IComponentChangeService; if (cs != null) { cs.ComponentChanged += new ComponentChangedEventHandler(OnComponentChanged); }
實作 OnComponentChanged 事件處理常式。測試傳送元件的類型,如果該類型是
IMarqueeWidget
,便呼叫 Refresh 方法。Private Sub OnComponentChanged( _ ByVal sender As Object, _ ByVal e As ComponentChangedEventArgs) If TypeOf e.Component Is IMarqueeWidget Then Me.Control.Refresh() End If End Sub
private void OnComponentChanged( object sender, ComponentChangedEventArgs e) { if (e.Component is IMarqueeWidget) { this.Control.Refresh(); } }
加入設計工具動詞命令至自訂設計工具
設計工具動詞命令是連結至事件處理常式的功能表命令。設計工具動詞命令會在執行階段加入至元件的快速鍵功能表。如需詳細資訊,請參閱 DesignerVerb。
您將會加入兩個設計工具動詞命令至設計工具:[執行測試] 和 [停止測試]。這些動詞命令能讓您在設計階段檢視 MarqueeControl
的執行階段行為。這些動詞命令會加入至 MarqueeControlRootDesigner
。
當叫用 [執行測試] 時,動詞命令事件處理常式將會叫用 MarqueeControl
上的 StartMarquee
方法。當叫用 [停止測試] 時,動詞命令事件處理常式將會叫用 MarqueeControl
上的 StopMarquee
方法。StartMarquee
和 StopMarquee
方法的實作會在實作 IMarqueeWidget
的被收納的控制項 (Contained Control) 上呼叫這些方法,所以任何被收納的 IMarqueeWidget
控制項都將會參與測試。
若要加入設計工具動詞命令至自訂設計工具
在
MarqueeControlRootDesigner
類別中加入名為OnVerbRunTest
和OnVerbStopTest
的事件處理常式。Private Sub OnVerbRunTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Start() End Sub Private Sub OnVerbStopTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Stop() End Sub
private void OnVerbRunTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Start(); } private void OnVerbStopTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Stop(); }
將這些事件處理常式連接至對應的設計工具動詞命令。
MarqueeControlRootDesigner
會從基底類別繼承 DesignerVerbCollection。您將會建立兩個新的 DesignerVerb 物件,並將它們加入至 Initialize 方法中的這個集合。Me.Verbs.Add(New DesignerVerb("Run Test", _ New EventHandler(AddressOf OnVerbRunTest))) Me.Verbs.Add(New DesignerVerb("Stop Test", _ New EventHandler(AddressOf OnVerbStopTest)))
this.Verbs.Add( new DesignerVerb("Run Test", new EventHandler(OnVerbRunTest)) ); this.Verbs.Add( new DesignerVerb("Stop Test", new EventHandler(OnVerbStopTest)) );
建立自訂 UITypeEditor
當您建立使用者的自訂設計階段經驗時,通常會希望建立與 [屬性] 視窗的自訂互動。您可以藉由建立 UITypeEditor 以完成這動作。如需詳細資訊,請參閱 HOW TO:建立 UI 型別編輯器。
MarqueeBorder
控制項會在 [屬性] 視窗中公開數種屬性。這些屬性當中的兩種,即 MarqueeSpinDirection
和 MarqueeLightShape
,是由列舉型別所表示的。為了說明 UI 型別編輯器的使用,MarqueeLightShape
屬性將會有關聯的 UITypeEditor 類別。
若要建立自訂 UI 型別編輯器
在 [程式碼編輯器] 中,開啟
MarqueeBorder
來源檔。在
MarqueeBorder
類別的定義中,宣告名為LightShapeEditor
的類別,此類別衍生自 UITypeEditor。' This class demonstrates the use of a custom UITypeEditor. ' It allows the MarqueeBorder control's LightShape property ' to be changed at design time using a customized UI element ' that is invoked by the Properties window. The UI is provided ' by the LightShapeSelectionControl class. Friend Class LightShapeEditor Inherits UITypeEditor
// This class demonstrates the use of a custom UITypeEditor. // It allows the MarqueeBorder control's LightShape property // to be changed at design time using a customized UI element // that is invoked by the Properties window. The UI is provided // by the LightShapeSelectionControl class. internal class LightShapeEditor : UITypeEditor {
宣告稱為
editorService
的 IWindowsFormsEditorService 執行個體變數。Private editorService As IWindowsFormsEditorService = Nothing
private IWindowsFormsEditorService editorService = null;
覆寫 GetEditStyle 方法。這個實作會傳回 DropDown,告訴設計環境如何顯示
LightShapeEditor
。Public Overrides Function GetEditStyle( _ ByVal context As System.ComponentModel.ITypeDescriptorContext) _ As UITypeEditorEditStyle Return UITypeEditorEditStyle.DropDown End Function
public override UITypeEditorEditStyle GetEditStyle( System.ComponentModel.ITypeDescriptorContext context) { return UITypeEditorEditStyle.DropDown; }
覆寫 EditValue 方法。這個實作會查詢 IWindowsFormsEditorService 物件的設計環境。如果成功的話,它會建立
LightShapeSelectionControl
。會叫用 DropDownControl 方法來啟動LightShapeEditor
。這個引動過程的傳回值會傳回至設計環境。Public Overrides Function EditValue( _ ByVal context As ITypeDescriptorContext, _ ByVal provider As IServiceProvider, _ ByVal value As Object) As Object If (provider IsNot Nothing) Then editorService = _ CType(provider.GetService(GetType(IWindowsFormsEditorService)), _ IWindowsFormsEditorService) End If If (editorService IsNot Nothing) Then Dim selectionControl As _ New LightShapeSelectionControl( _ CType(value, MarqueeLightShape), _ editorService) editorService.DropDownControl(selectionControl) value = selectionControl.LightShape End If Return value End Function
public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { if (provider != null) { editorService = provider.GetService( typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; } if (editorService != null) { LightShapeSelectionControl selectionControl = new LightShapeSelectionControl( (MarqueeLightShape)value, editorService); editorService.DropDownControl(selectionControl); value = selectionControl.LightShape; } return value; }
建立自訂 UITypeEditor 的檢視控制項
MarqueeLightShape
屬性支援兩種燈號圖形類型:Square
和Circle
。您將會建立自訂控制項,此控制只用於在 [屬性] 視窗中圖形化顯示這些值的用途。這個自訂控制項將會由 UITypeEditor 用來與 [屬性] 視窗互動。
若要建立自訂 UI 型別編輯器的檢視控制項
將新的 UserControl 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "LightShapeSelectionControl" 給新的原始程式檔。
將兩個 Panel 控制項從 [工具箱] 拖曳至
LightShapeSelectionControl
。將它們命名為squarePanel
和circlePanel
。以並排方式來排列它們。將兩個 Panel 控制項的 Size 屬性設定為 (60, 60)。將squarePanel
控制項的 Location 屬性設定為 (8, 10)。將circlePanel
控制項的 Location 屬性設定為 (80, 10)。最後,將LightShapeSelectionControl
的 Size 屬性設定為 (150, 80)。在 [程式碼編輯器] 中,開啟
LightShapeSelectionControl
來源檔。在檔案的頂端匯入 System.Windows.Forms.Design 命名空間:
Imports System.Windows.Forms.Design
using System.Windows.Forms.Design;
Private editorService As IWindowsFormsEditorService
private IWindowsFormsEditorService editorService;
測試設計工具中的自訂控制項
此時,您就可以建置 MarqueeControlLibrary 專案了。建立繼承自 MarqueeControl
類別的控制項,並在表單上使用該控制項,以測試您的實作。
若要建立自訂 MarqueeControl 實作
在 Windows Form 設計工具中開啟表單
DemoMarqueeControl
。這樣將會建立DemoMarqueeControl
型別的執行個體,並且會在MarqueeControlRootDesigner
型別的執行個體中顯示該執行個體。在 [工具箱] 中開啟 [MarqueeControlLibrary 元件] 索引標籤。您會看到
MarqueeBorder
和MarqueeText
控制項可用於選取。拖曳
MarqueeBorder
控制項的執行個體至DemoMarqueeControl
設計介面。將此MarqueeBorder
控制項停駐至父控制項。將
MarqueeText
控制項的執行個體拖曳至DemoMarqueeControl
設計介面。建置方案。
以滑鼠右鍵按一下
DemoMarqueeControl
,並從快速鍵功能表中選取 [執行測試] 選項以開始動畫。按一下 [停止測試] 以停止動畫。以 [設計] 檢視開啟 [Form1]。
在表單上放置兩個 Button 控制項。將它們命名為
startButton
和stopButton
,並將 Text 屬性值分別變更為 Start 和 Stop。實作兩個 Button 控制項的 Click 事件處理常式。
在 [工具箱] 中開啟 [MarqueeControlTest 元件] 索引標籤。您會看到
DemoMarqueeControl
可用於選取。將
DemoMarqueeControl
的執行個體拖曳至 [Form1] 設計介面。在 Click 事件處理常式中叫用
DemoMarqueeControl
上的Start
和Stop
方法。
Private Sub startButton_Click(sender As Object, e As System.EventArgs)
Me.demoMarqueeControl1.Start()
End Sub 'startButton_Click
Private Sub stopButton_Click(sender As Object, e As System.EventArgs)
Me.demoMarqueeControl1.Stop()
End Sub 'stopButton_Click
private void startButton_Click(object sender, System.EventArgs e)
{
this.demoMarqueeControl1.Start();
}
private void stopButton_Click(object sender, System.EventArgs e)
{
this.demoMarqueeControl1.Stop();
}
後續步驟
MarqueeControlLibrary 示範自訂控制項與關聯的設計工具之簡單實作。您可以使用數種方式,讓這個範例更為複雜:
變更在設計工具中
DemoMarqueeControl
的屬性值。加入更多的MarqueBorder
控制項,並將它們停駐在父執行個體內,以建立巢狀作用。嘗試UpdatePeriod
和燈號相關屬性的不同設定。撰寫您自己的
IMarqueeWidget
實作。例如,您可以建立閃爍的「霓虹燈標誌」(Neon Sign) 或具有多個影像的動畫標誌。進一步自訂設計階段經驗。您可以嘗試遮蔽比 Enabled 和 Visible 更多的屬性,也可以加入新的屬性。加入新的設計工具動詞命令以簡化一般工作,例如停駐子控制項。
授權
MarqueeControl
。如需詳細資訊,請參閱 HOW TO:授權元件和控制項。控制序列化控制項的方式以及針對控制項產生程式碼的方式。如需詳細資訊,請參閱動態原始程式碼的產生和編譯。
請參閱
工作
HOW TO:建立採用設計階段功能的 Windows Form 控制項
參考
UserControl
ParentControlDesigner
DocumentDesigner
IRootDesigner
DesignerVerb
UITypeEditor
BackgroundWorker