逐步解說:建立利用 Visual Studio 設計階段功能的 Windows Form 控制項

更新:2007 年 11 月

自訂控制項的設計階段經驗,可以透過撰寫關聯的自訂設計工具來加強。

這個逐步解說會說明如何建立自訂控制項的自訂設計工具。您將會實作 MarqueeControl 型別和關聯的設計工具類別 (稱為 MarqueeControlRootDesigner)。

MarqueeControl 型別會實作類似於劇院跑馬燈的顯示,具有動畫燈和閃爍的文字。

這個控制項的設計工具會和設計環境互動,以提供自訂設計階段經驗。有了自訂設計工具,您可以組合具有動畫燈和閃爍文字多種組合的自訂 MarqueeControl 實作。您可以在表單上使用組合的控制項,就像使用任何其他 Windows Form 控制項一樣。

逐步解說將說明的工作包括:

  • 建立專案

  • 建立控制項程式庫專案

  • 參考自訂控制項專案

  • 定義自訂控制項及其自訂設計工具

  • 建立自訂控制項的執行個體

  • 設定專案進行設計階段偵錯

  • 實作自訂控制項

  • 建立自訂控制項的子控制項

  • 建立 MarqueeBorder 子控制項

  • 建立自訂設計工具以遮蔽和篩選屬性

  • 處理元件變更

  • 加入設計工具動詞命令至自訂設計工具

  • 建立自訂 UITypeEditor

  • 測試設計工具中的自訂控制項

當完成時,您的自訂控制項看起來會如同下列所示:

MarqueeControl 可能的排列方式

如需完整的程式碼清單,請參閱 HOW TO:建立採用設計階段功能的 Windows Form 控制項

注意事項:

根據您目前使用的設定或版本,您所看到的對話方塊與功能表指令可能會與 [說明] 中描述的不同。如果要變更設定,請從 [工具] 功能表中選擇 [匯入和匯出設定]。如需詳細資訊,請參閱 Visual Studio 設定

必要條件

若要完成這個逐步解說,您必須要有:

  • 擁有足夠的使用權限,可以在安裝 Visual Studio 的電腦上建立並執行 Windows Form 應用程式專案。

建立專案

首先建立應用程式物件。您將會使用這個專案來建置裝載自訂控制項的應用程式。

若要建立專案

建立控制項程式庫專案

下一步就是建立控制項程式庫專案。您將建立新的自訂控制項及其對應的自訂設計工具。

若要建立控制項程式庫專案

  1. 加入 Windows 控制項程式庫專案至方案。如需詳細資訊,請參閱加入新的專案對話方塊。將專案命名為 "MarqueeControlLibrary"。

  2. 使用 [方案總管],刪除名為 "UserControl1.cs" 或 "UserControl1.vb" (依據您選擇的語言而定) 的原始程式檔 (Source File),藉此刪除專案的預設控制項。如需詳細資訊,請參閱 HOW TO:移除、刪除和排除項目

  3. 將新的 UserControl 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "MarqueeControl" 給新的原始程式檔。

  4. 使用 [方案總管],在 MarqueeControlLibrary 專案中建立新資料夾。如需詳細資訊,請參閱 HOW TO:加入新專案項目。將新資料夾命名為 "Design"。

  5. 以滑鼠右鍵按一下 [Design] 資料夾,並加入新類別。提供主檔名 "MarqueeControlRootDesigner" 給新的原始程式檔。

  6. 您將會需要使用 System.Design 組件的型別,所以請加入此參考至 MarqueeControlLibrary 專案。如需詳細資訊,請參閱 HOW TO:在 Visual Studio 中新增或移除參考 (C#)

參考自訂控制項專案

您將使用 MarqueeControlTest 專案以測試自訂控制項。在您加入專案參考至 MarqueeControlLibrary 組件時,測試專案會留意自訂控制項。

若要參考自訂控制項專案

  • 在 MarqueeControlTest 專案中,加入專案參考至 MarqueeControlLibrary 組件。請確認有使用 [加入參考] 對話方塊中的 [屬性] 索引標籤,而非直接參考 MarqueeControlLibrary 組件。

定義自訂控制項及其自訂設計工具

您的自訂控制項將衍生自 UserControl 類別。這能讓您的控制項包含其他控制項,且提供您的控制項許多預設功能。

您的自訂控制項將會有關聯的自訂設計工具。這能讓您建立特別為自訂控制項量身訂做的獨特設計經驗。

您可透過使用 DesignerAttribute 類別,將控制項與設計工具相關聯。因為您是開發自訂控制項的整個設計階段行為,所以自訂控制項將會實作 IRootDesigner 介面。

若要定義自訂控制項及其自訂設計工具

  1. 在 [程式碼編輯器] 中,開啟 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;
    
  2. 請將 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
        {
    
  3. 在 [程式碼編輯器] 中,開啟 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;
    
  4. 將 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
        {
    
  5. 定義 MarqueeControlRootDesigner 類別的建構函式。在建構函式主體中插入 WriteLine 陳述式。這對於偵錯用途將會非常有用。

    Public Sub New()
        Trace.WriteLine("MarqueeControlRootDesigner ctor")
    End Sub
    
    public MarqueeControlRootDesigner()
    {
        Trace.WriteLine("MarqueeControlRootDesigner ctor");
    }
    

建立自訂控制項的執行個體

若要觀察控制項的自訂設計階段行為,您要將控制項的執行個體放置在 MarqueeControlTest 專案中的表單上。

若要建立自訂控制項的執行個體

  1. 將新的 UserControl 項目加入至 MarqueeControlTest 專案。提供主檔名 "DemoMarqueeControl" 給新的原始程式檔。

  2. 在 [程式碼編輯器] 中,開啟 DemoMarqueeControl 檔案。在檔案的頂端匯入 MarqueeControlLibrary 命名空間:

Imports MarqueeControlLibrary
using MarqueeControlLibrary;
  1. 將 DemoMarqueeControl 的宣告變更為繼承自 MarqueeControl 類別。

  2. 建置專案。

  3. 在 Windows Form 設計工具中開啟 Form1。

  4. 在 [工具箱] 中尋找 [MarqueeControlTest 元件] 索引標籤,然後予以開啟。將 DemoMarqueeControl 從 [工具箱] 拖曳到表單上。

  5. 建置專案。

設定專案進行設計階段偵錯

您在開發自訂設計階段經驗時,將需要偵錯控制項和元件。有一種簡單的方法可以設定您的專案以允許設計階段時的偵錯。如需詳細資訊,請參閱逐步解說:在設計階段偵錯自訂的 Windows Form 控制項

若要設定設計階段偵錯的專案

  1. 以滑鼠右鍵按一下 MarqueeControlLibrary 專案,然後選取 [屬性]。

  2. 在 [MarqueeControlLibrary 屬性頁] 對話方塊中選取 [組態屬性] 頁。

  3. 在 [起始動作] 區段中選取 [起始外部程式]。您將會偵錯個別的 Visual Studio 執行個體,所以請按一下省略 (VisualStudioEllipsesButton 螢幕擷取畫面) 按鈕,以瀏覽 Visual Studio IDE。可執行檔的名稱為 devenv.exe,如果您安裝至預設位置,其路徑就會是 %programfiles%\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe。

  4. 按一下 [確定] 關閉對話方塊。

  5. 以滑鼠右鍵按一下 MarqueeControlLibrary 專案,然後選取 [設定為啟始專案],以啟用這個偵錯設定。

檢查點

您現在已準備好偵錯自訂控制項的設計階段行為。一旦您決定偵錯環境已設定正確,就可測試自訂控制項和自訂設計介面之間的關聯。

若要測試偵錯環境和設計工具關聯

  1. 請開啟 [程式碼編輯器] 中的 MarqueeControlRootDesigner,並在 WriteLine 陳述式上放置中斷點。

  2. 按 F5 以啟動偵錯工作階段。請注意,會建立新的 Visual Studio 執行個體。

  3. 在新的 Visual Studio 執行個體中開啟 "MarqueeControlTest" 方案。您可以輕易地找到方案,方法是從 [檔案] 功能表中選取 [最近使用的專案]。"MarqueeControlTest.sln" 方案檔將會列為最近使用的檔案。

  4. 在設計工具中開啟 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 類別實作。

若要實作自訂控制項

  1. 在 [程式碼編輯器] 中,開啟 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();
            }
        }
    }
    
  2. 覆寫 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 執行緒可能就無法跟上動畫的速度。

若要建立自訂控制項的子控制項

  1. 將新的類別項目加入至 MarqueeControlLibrary 專案。提供主檔名 "IMarqueeWidget" 給新的原始程式檔。

  2. 開啟 [程式碼編輯器] 的 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
    {
    
  3. 加入下列程式碼至 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;
        }
    }
    
  4. 將新的 [自訂控制項] 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "MarqueeText" 給新的原始程式檔。

  5. 從 [工具箱] 將 BackgroundWorker 元件拖曳至 MarqueeText 控制項。此元件允許 MarqueeText 控制項非同步自我更新。

  6. 在 [屬性] 視窗中,將 BackgroundWorker 元件的 WorkerReportsProgess 和 WorkerSupportsCancellation 屬性設定為 true。這些設定能讓 BackgroundWorker 元件週期性地引發 ProgressChanged 事件,並取消非同步更新。如需詳細資訊,請參閱 BackgroundWorker 元件

  7. 在 [程式碼編輯器] 中,開啟 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;
    
  8. 將 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
    {
    
  9. 宣告對應至公開屬性的執行個體變數,並在建構函式中初始化它們。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);
    }
    
  10. 實作 IMarqueeWidget 介面。

    StartMarquee 和 StopMarquee 方法會叫用 BackgroundWorker 元件的 RunWorkerAsyncCancelAsync 方法,以啟動和停止動畫。

    CategoryBrowsable 屬性 (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");
            }
        }
    }
    
  11. 實作屬性存取子。您將公開兩個屬性給用戶端:LightColor 和 DarkColor。CategoryBrowsable 屬性 (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);
            }
        }
    }
    
  12. 實作 BackgroundWorker 元件的 DoWorkProgressChanged 事件的處理常式。

    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();
    }
    
    
  13. 覆寫 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);
    }
    
  14. 按 F6 建置方案。

建立 MarqueeBorder 子控制項

MarqueeBorder 控制項是比 MarqueeText 控制項略微複雜一點的控制項。它具有較多的屬性,而在 OnPaint 方法中的動畫更為複雜。基本上,它和 MarqueeText 控制項十分類似。

因為 MarqueeBorder 控制項能擁有子控制項,所以需要留意 Layout 事件。

若要建立 MarqueeBorder 控制項

  1. 將新的 [自訂控制項] 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "MarqueeBorder" 給新的原始程式檔。

  2. 從 [工具箱] 將 BackgroundWorker 元件拖曳至 MarqueeBorder 控制項。這個元件允許 MarqueeBorder 控制項非同步自我更新。

  3. 在 [屬性] 視窗中,將 BackgroundWorker 元件的 WorkerReportsProgess 和 WorkerSupportsCancellation 屬性設定為 true。這些設定能讓 BackgroundWorker 元件週期性地引發 ProgressChanged 事件,並取消非同步更新。如需詳細資訊,請參閱 BackgroundWorker 元件

  4. 在 [屬性] 視窗中,按一下 [事件] 按鈕。附加 DoWorkProgressChanged 事件的處理常式。

  5. 在 [程式碼編輯器] 中,開啟 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;
    
  6. 變更 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
    {
    
  7. 宣告管理 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
    }
    
  8. 宣告對應至公開屬性的執行個體變數,並在建構函式中初始化它們。

    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);
    }
    
  9. 實作 IMarqueeWidget 介面。

    StartMarquee 和 StopMarquee 方法會叫用 BackgroundWorker 元件的 RunWorkerAsyncCancelAsync 方法,以啟動和停止動畫。

    因為 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");
            }
        }
    }
    
    
  10. 實作屬性存取子。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;
        }
    }
    
    
  11. 實作 BackgroundWorker 元件的 DoWorkProgressChanged 事件的處理常式。

    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();
    }
    
  12. 實作 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;
                }
        }
    }
    
  13. 覆寫 OnLayoutOnPaint 方法。

    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)。這種行為能讓設計工具追蹤使用者所設定的值,並選擇性地傳遞該值至正在設計的元件。

對於這個範例,VisibleEnabled 屬性將會由 MarqueeBorderDesigner 遮蔽,讓使用者不會在執行階段時無法檢視 MarqueeBorder 控制項或將其停用。

設計工具也可以加入和移除屬性。對於這個範例,Padding 屬性會在設計階段移除,因為 MarqueeBorder 控制項會依據 LightSize 屬性所指定的燈號大小,以程式設計方式設定邊框距離。

MarqueeBorderDesigner 的基底類別是 ComponentDesigner,具有能夠變更在設計階段由控制項公開的屬性 (Attribute)、屬性 (Property) 及事件的方法:

當使用這些方法來變更元件的公用介面時,您必須遵循下列規則:

  • 只加入或移除 PreFilter 方法中的項目

  • 只修改 PostFilter 方法中的現有項目

  • 永遠在 PreFilter 方法中最先呼叫基底實作

  • 永遠在 PostFilter 方法中最後呼叫基底實作

遵守這些規則能確保設計階段環境中的所有設計工具,對於正在設計的所有元件都具有一致的檢視。

ComponentDesigner 類別提供的字典可管理遮蔽屬性的值,讓您不需為建立特定執行個體變數的需要而煩惱。

若要建立自訂設計工具以遮蔽和篩選屬性

  1. 以滑鼠右鍵按一下 [設計] 資料夾,並加入新類別。提供主檔名 "MarqueeBorderDesigner" 給原始程式檔。

  2. 在 [程式碼編輯器] 中,開啟 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;
    
  3. 將 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
        {
    
  4. 覆寫 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]);
    }
    
  5. 實作 EnabledVisible 屬性。這些實作會遮蔽控制項的屬性。

    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 控制項就會適當地重新自我繪製。

若要處理元件變更

  1. 在 [程式碼編輯器] 中開啟 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);
    }
    
  2. 實作 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 控制項都將會參與測試。

若要加入設計工具動詞命令至自訂設計工具

  1. 在 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();
    }
    
  2. 將這些事件處理常式連接至對應的設計工具動詞命令。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 型別編輯器

  1. 在 [程式碼編輯器] 中,開啟 MarqueeBorder 來源檔。

  2. 在 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
    {
    
  3. 宣告稱為 editorService 的 IWindowsFormsEditorService 執行個體變數。

    Private editorService As IWindowsFormsEditorService = Nothing
    
    private IWindowsFormsEditorService editorService = null;
    
  4. 覆寫 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;
    }
    
  5. 覆寫 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 的檢視控制項

  1. MarqueeLightShape 屬性支援兩種燈號圖形類型:Square 和 Circle。您將會建立自訂控制項,此控制只用於在 [屬性] 視窗中圖形化顯示這些值的用途。這個自訂控制項將會由 UITypeEditor 用來與 [屬性] 視窗互動。

若要建立自訂 UI 型別編輯器的檢視控制項

  1. 將新的 UserControl 項目加入至 MarqueeControlLibrary 專案。提供主檔名 "LightShapeSelectionControl" 給新的原始程式檔。

  2. 將兩個 Panel 控制項從 [工具箱] 拖曳至 LightShapeSelectionControl。將它們命名為 squarePanel 和 circlePanel。以並排方式來排列它們。將兩個 Panel 控制項的 Size 屬性設定為 (60, 60)。將 squarePanel 控制項的 Location 屬性設定為 (8, 10)。將 circlePanel 控制項的 Location 屬性設定為 (80, 10)。最後,將 LightShapeSelectionControl 的 Size 屬性設定為 (150, 80)。

  3. 在 [程式碼編輯器] 中,開啟 LightShapeSelectionControl 來源檔。在檔案的頂端匯入 System.Windows.Forms.Design 命名空間:

Imports System.Windows.Forms.Design
using System.Windows.Forms.Design;
  1. 實作 squarePanel 和 circlePanel 控制項的 Click 事件處理常式。這些方法會叫用 CloseDropDown,以結束自訂 UITypeEditor 編輯工作階段。

    Private Sub squarePanel_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Me.lightShapeValue = MarqueeLightShape.Square
        Me.Invalidate(False)
        Me.editorService.CloseDropDown()
    
    End Sub
    
    
    Private Sub circlePanel_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Me.lightShapeValue = MarqueeLightShape.Circle
        Me.Invalidate(False)
        Me.editorService.CloseDropDown()
    
    End Sub
    
            private void squarePanel_Click(object sender, EventArgs e)
            {
                this.lightShapeValue = MarqueeLightShape.Square;
    
                this.Invalidate( false );
    
                this.editorService.CloseDropDown();
            }
    
            private void circlePanel_Click(object sender, EventArgs e)
            {
                this.lightShapeValue = MarqueeLightShape.Circle;
    
                this.Invalidate( false );
    
                this.editorService.CloseDropDown();
            }
    
  2. 宣告稱為 editorService 的 IWindowsFormsEditorService 執行個體變數。

Private editorService As IWindowsFormsEditorService
private IWindowsFormsEditorService editorService;
  1. 宣告稱為 lightShapeValue 的 MarqueeLightShape 執行個體變數。

    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
  2. 在 LightShapeSelectionControl 建構函式中,將 Click 事件處理常式附加至 squarePanel 和 circlePanel 控制項的 Click 事件。同時定義建構函式多載,可將 MarqueeLightShape 值從設計環境指派至 lightShapeValue 欄位。

    ' This constructor takes a MarqueeLightShape value from the
    ' design-time environment, which will be used to display
    ' the initial state.
     Public Sub New( _
     ByVal lightShape As MarqueeLightShape, _
     ByVal editorService As IWindowsFormsEditorService)
         ' This call is required by the Windows.Forms Form Designer.
         InitializeComponent()
    
         ' Cache the light shape value provided by the 
         ' design-time environment.
         Me.lightShapeValue = lightShape
    
         ' Cache the reference to the editor service.
         Me.editorService = editorService
    
         ' Handle the Click event for the two panels. 
         AddHandler Me.squarePanel.Click, AddressOf squarePanel_Click
         AddHandler Me.circlePanel.Click, AddressOf circlePanel_Click
     End Sub
    
            // This constructor takes a MarqueeLightShape value from the
            // design-time environment, which will be used to display
            // the initial state.
            public LightShapeSelectionControl( 
                MarqueeLightShape lightShape,
                IWindowsFormsEditorService editorService )
            {
                // This call is required by the designer.
                InitializeComponent();
    
                // Cache the light shape value provided by the 
                // design-time environment.
                this.lightShapeValue = lightShape;
    
                // Cache the reference to the editor service.
                this.editorService = editorService;
    
                // Handle the Click event for the two panels. 
                this.squarePanel.Click += new EventHandler(squarePanel_Click);
                this.circlePanel.Click += new EventHandler(circlePanel_Click);
            }
    
  3. Dispose 方法中,中斷連結 Click 事件處理常式。

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
    
            ' Be sure to unhook event handlers
            ' to prevent "lapsed listener" leaks.
            RemoveHandler Me.squarePanel.Click, AddressOf squarePanel_Click
            RemoveHandler Me.circlePanel.Click, AddressOf circlePanel_Click
    
            If (components IsNot Nothing) Then
                components.Dispose()
            End If
    
        End If
        MyBase.Dispose(disposing)
    End Sub
    
            protected override void Dispose( bool disposing )
            {
                if( disposing )
                {
                    // Be sure to unhook event handlers
                    // to prevent "lapsed listener" leaks.
                    this.squarePanel.Click -= 
                        new EventHandler(squarePanel_Click);
                    this.circlePanel.Click -= 
                        new EventHandler(circlePanel_Click);
    
                    if(components != null)
                    {
                        components.Dispose();
                    }
                }
                base.Dispose( disposing );
            }
    
  4. 在 [方案總管] 中按一下 [顯示所有檔案] 按鈕。開啟 LightShapeSelectionControl.Designer.cs 或 LightShapeSelectionControl.Designer.vb 檔,然後移除 Dispose 方法的預設定義。

  5. 實作 LightShape 屬性。

    ' LightShape is the property for which this control provides
    ' a custom user interface in the Properties window.
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            If Me.lightShapeValue <> Value Then
                Me.lightShapeValue = Value
            End If
        End Set
    
    End Property
    
            // LightShape is the property for which this control provides
            // a custom user interface in the Properties window.
            public MarqueeLightShape LightShape
            {
                get
                {
                    return this.lightShapeValue;
                }
    
                set
                {
                    if( this.lightShapeValue != value )
                    {
                        this.lightShapeValue = value;
                    }
                }
            }
    
  6. 覆寫 OnPaint 方法。這個實作將繪製中間填滿的方形與圓形。也會在某個圖案的周邊繪製框線,以醒目提示所選取的值。

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        MyBase.OnPaint(e)
    
        Dim gCircle As Graphics = Me.circlePanel.CreateGraphics()
        Try
            Dim gSquare As Graphics = Me.squarePanel.CreateGraphics()
            Try
                ' Draw a filled square in the client area of
                ' the squarePanel control.
                gSquare.FillRectangle( _
                Brushes.Red, _
                0, _
                0, _
                Me.squarePanel.Width, _
                Me.squarePanel.Height)
    
                ' If the Square option has been selected, draw a 
                ' border inside the squarePanel.
                If Me.lightShapeValue = MarqueeLightShape.Square Then
                    gSquare.DrawRectangle( _
                    Pens.Black, _
                    0, _
                    0, _
                    Me.squarePanel.Width - 1, _
                    Me.squarePanel.Height - 1)
                End If
    
                ' Draw a filled circle in the client area of
                ' the circlePanel control.
                gCircle.Clear(Me.circlePanel.BackColor)
                gCircle.FillEllipse( _
                Brushes.Blue, _
                0, _
                0, _
                Me.circlePanel.Width, _
                Me.circlePanel.Height)
    
                ' If the Circle option has been selected, draw a 
                ' border inside the circlePanel.
                If Me.lightShapeValue = MarqueeLightShape.Circle Then
                    gCircle.DrawRectangle( _
                    Pens.Black, _
                    0, _
                    0, _
                    Me.circlePanel.Width - 1, _
                    Me.circlePanel.Height - 1)
                End If
            Finally
                gSquare.Dispose()
            End Try
        Finally
            gCircle.Dispose()
        End Try
    End Sub
    
            protected override void OnPaint(PaintEventArgs e)
            {
                base.OnPaint (e);
    
                using( 
                    Graphics gSquare = this.squarePanel.CreateGraphics(),
                    gCircle = this.circlePanel.CreateGraphics() )
                {   
                    // Draw a filled square in the client area of
                    // the squarePanel control.
                    gSquare.FillRectangle(
                        Brushes.Red, 
                        0,
                        0,
                        this.squarePanel.Width,
                        this.squarePanel.Height
                        );
    
                    // If the Square option has been selected, draw a 
                    // border inside the squarePanel.
                    if( this.lightShapeValue == MarqueeLightShape.Square )
                    {
                        gSquare.DrawRectangle( 
                            Pens.Black,
                            0,
                            0,
                            this.squarePanel.Width-1,
                            this.squarePanel.Height-1);
                    }
    
                    // Draw a filled circle in the client area of
                    // the circlePanel control.
                    gCircle.Clear( this.circlePanel.BackColor );
                    gCircle.FillEllipse( 
                        Brushes.Blue, 
                        0,
                        0,
                        this.circlePanel.Width, 
                        this.circlePanel.Height
                        );
    
                    // If the Circle option has been selected, draw a 
                    // border inside the circlePanel.
                    if( this.lightShapeValue == MarqueeLightShape.Circle )
                    {
                        gCircle.DrawRectangle( 
                            Pens.Black,
                            0,
                            0,
                            this.circlePanel.Width-1,
                            this.circlePanel.Height-1);
                    }
                }   
            }
    

測試設計工具中的自訂控制項

此時,您就可以建置 MarqueeControlLibrary 專案了。建立繼承自 MarqueeControl 類別的控制項,並在表單上使用該控制項,以測試您的實作。

若要建立自訂 MarqueeControl 實作

  1. 在 Windows Form 設計工具中開啟表單 DemoMarqueeControl。這樣將會建立 DemoMarqueeControl 型別的執行個體,並且會在 MarqueeControlRootDesigner 型別的執行個體中顯示該執行個體。

  2. 在 [工具箱] 中開啟 [MarqueeControlLibrary 元件] 索引標籤。您會看到 MarqueeBorder 和 MarqueeText 控制項可用於選取。

  3. 拖曳 MarqueeBorder 控制項的執行個體至 DemoMarqueeControl 設計介面。將此 MarqueeBorder 控制項停駐至父控制項。

  4. 將 MarqueeText 控制項的執行個體拖曳至 DemoMarqueeControl 設計介面。

  5. 建置方案。

  6. 以滑鼠右鍵按一下 DemoMarqueeControl,並從快速鍵功能表中選取 [執行測試] 選項以開始動畫。按一下 [停止測試] 以停止動畫。

  7. 以 [設計] 檢視開啟 [Form1]。

  8. 在表單上放置兩個 Button 控制項。將它們命名為 startButton 和 stopButton,並將 Text 屬性值分別變更為 Start 和 Stop。

  9. 實作兩個 Button 控制項的 Click 事件處理常式。

  10. 在 [工具箱] 中開啟 [MarqueeControlTest 元件] 索引標籤。您會看到 DemoMarqueeControl 可用於選取。

  11. 將 DemoMarqueeControl 的執行個體拖曳至 [Form1] 設計介面。

  12. 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();
}
  1. 將 MarqueeControlTest 專案設定為啟動專案然後予以執行。您將會看到顯示 DemoMarqueeControl 的表單。按一下 [開始] 按鈕開始動畫。您應該會看到文字閃爍,以及沿著框線四周移動的燈光。

後續步驟

MarqueeControlLibrary 示範自訂控制項與關聯的設計工具之簡單實作。您可以使用數種方式,讓這個範例更為複雜:

  • 變更在設計工具中 DemoMarqueeControl 的屬性值。加入更多的 MarqueBorder 控制項,並將它們停駐在父執行個體內,以建立巢狀作用。嘗試 UpdatePeriod 和燈號相關屬性的不同設定。

  • 撰寫您自己的 IMarqueeWidget 實作。例如,您可以建立閃爍的「霓虹燈標誌」(Neon Sign) 或具有多個影像的動畫標誌。

  • 進一步自訂設計階段經驗。您可以嘗試遮蔽比 EnabledVisible 更多的屬性,也可以加入新的屬性。加入新的設計工具動詞命令以簡化一般工作,例如停駐子控制項。

  • 授權 MarqueeControl。如需詳細資訊,請參閱 HOW TO:授權元件和控制項

  • 控制序列化控制項的方式以及針對控制項產生程式碼的方式。如需詳細資訊,請參閱動態原始程式碼的產生和編譯

請參閱

工作

HOW TO:建立採用設計階段功能的 Windows Form 控制項

參考

UserControl

ParentControlDesigner

DocumentDesigner

IRootDesigner

DesignerVerb

UITypeEditor

BackgroundWorker

其他資源

擴充設計階段支援

自訂設計工具

.NET 圖案程式庫:簡單的設計工具