共用方式為


逐步解說:建立採用設計階段功能的控制項

藉由製作相關聯的自訂設計工具,可以增強自訂控制項的設計階段體驗。

警告

此內容是針對 .NET Framework 所撰寫。 如果您使用 .NET 6 或更新版本,請謹慎使用此內容。 Windows Forms 的設計工具系統已變更,請務必檢閱自 .NET Framework 以來的設計工具變更一文。

此文章說明如何針對自訂控制項建立自訂設計工具。 您將實作 MarqueeControl 類型和稱為 MarqueeControlRootDesigner 的相關聯設計工具類別。

MarqueeControl 類型所實作的顯示器類似具有動畫風格燈光和閃爍文字的劇場霓虹燈。

適用於此控制項的設計工具會與設計環境互動,以提供自訂設計階段體驗。 透過自訂設計工具,您可以透過多種組合來組合具有動畫風格燈光和閃爍文字的自訂 MarqueeControl 實作。 您可以在表單上使用組合的控制項,就像任何其他 Windows Forms 控制項一樣。

完成此逐步解說之後,您的自訂控制項將看起來如下:

The app showing a marquee saying Text and a Start and Stop buttons.此應用程式顯示寫著「文字」的霓虹燈,以及 [開始] 和 [停止] 按鈕。

如需完整的程式碼清單,請參閱操作說明:建立採用設計階段功能的 Windows Forms 控制項 (英文)。

必要條件

為了完成此逐步解說,您將需要 Visual Studio。

建立專案

第一個步驟是建立應用程式專案。 您將使用此專案來建置裝載自訂控制項的應用程式。

在 Visual Studio 中,建立新的 Windows Forms 應用程式專案,並將其命名為 MarqueeControlTest

建立控制項程式庫專案

  1. 將 Windows Forms 控制項程式庫專案新增至方案。 將專案命名為 MarqueeControlLibrary

  2. 使用 [方案總管],視您選擇的語言而定,刪除名為 "UserControl1.cs" 或 "UserControl1.vb" 的來源檔案,藉以刪除專案的預設控制項。

  3. 將新的 UserControl 項目 (部分機器翻譯) 新增至 MarqueeControlLibrary 專案。 為新的來源檔案提供 MarqueeControl 的基底名稱。

  4. 使用 [方案總管],在 MarqueeControlLibrary 專案中建立新的資料夾。

  5. 以滑鼠右鍵按一下 [設計] 資料夾,然後新增類別。 將其命名為 MarqueeControlRootDesigner

  6. 您將需要使用來自 System.Design 組件的類型,因此,請新增這個對 MarqueeControlLibrary 專案的參考。

參考自訂控制項專案

您將使用 MarqueeControlTest 專案來測試自訂控制項。 當您新增對 MarqueeControlLibrary 組件的專案參考時,測試專案將會注意到自訂控制項。

MarqueeControlTest 專案中,新增對 MarqueeControlLibrary 組件的專案參考。 請務必使用 [加入參考] 對話方塊中的 [專案] 索引標籤,而不是直接參考 MarqueeControlLibrary 組件。

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

您的自訂控制項將衍生自 UserControl 類別 (部分機器翻譯)。 這讓您的控制項能夠包含其他控制項,並為您的控制項提供大量預設功能。

您的自訂控制項將具有一個相關聯的自訂設計工具。 這讓您能夠建立專為自訂控制項量身打造的獨特設計體驗。

您可以使用 DesignerAttribute 類別 (部分機器翻譯),將控制項與其設計工具產生關聯。 由於您正在開發自訂控制項的整個設計階段行為,因此,自訂設計工具將實作 IRootDesigner 介面 (部分機器翻譯)。

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

  1. 在 [程式碼編輯器] 中,開啟 MarqueeControl 來源檔案。 在檔案頂端,匯入下列命名空間:

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Drawing
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  2. DesignerAttribute (部分機器翻譯) 新增至 MarqueeControl 類別宣告。 這會將自訂控制項與其設計工具產生關聯。

    [Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )]
    public class MarqueeControl : UserControl
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _
     GetType(IRootDesigner))> _
    Public Class MarqueeControl
        Inherits UserControl
    
  3. 在 [程式碼編輯器] 中,開啟 MarqueeControl 來源檔案。 在檔案頂端,匯入下列命名空間:

    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;
    
    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
    
  4. MarqueeControlRootDesigner 的宣告變更為繼承自 DocumentDesigner 類別 (部分機器翻譯)。 套用 ToolboxItemFilterAttribute (部分機器翻譯),以指定設計工具與 [工具箱] 的互動。

    注意

    MarqueeControlRootDesigner 類別的定義已包含於稱為 MarqueeControlLibrary.Design 的命名空間中。 此宣告會將設計工具放置於保留給設計相關類型的特殊命名空間中。

    namespace MarqueeControlLibrary.Design
    {
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
        public class MarqueeControlRootDesigner : DocumentDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
        ToolboxItemFilterType.Require), _
        ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
        ToolboxItemFilterType.Require)> _
        Public Class MarqueeControlRootDesigner
            Inherits DocumentDesigner
    
  5. 定義 MarqueeControlRootDesigner 類別的建構函式。 在建構函式主體中插入 WriteLine 陳述式 (部分機器翻譯)。 這對偵錯很有用。

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

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

  1. 將新的 UserControl 項目 (部分機器翻譯) 新增至 MarqueeControlLibrary 專案。 為新的來源檔案提供 DemoMarqueeControl 的基底名稱。

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

    Imports MarqueeControlLibrary
    
    using MarqueeControlLibrary;
    
  3. MarqueeControlRootDesigner 的宣告變更為繼承自 DocumentDesigner 類別 (部分機器翻譯)。

  4. 組建專案。

  5. 在 Windows Forms 設計工具中,開啟 Form1。

  6. 在 [工具箱] 中尋找 [MarqueeControlTest 元件] 索引標籤並加以開啟。 將 DemoMarqueeControl 從 [工具箱] 拖曳到您的表單。

  7. 組建專案。

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

當您開發自訂設計階段體驗時,必須對控制項和元件進行偵錯。 有一個簡單的方式可設定專案,以允許在設計階段進行偵錯。 如需詳細資訊,請參閱逐步解說:在設計階段偵錯自訂的 Windows Forms 控制項

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

  2. 在 [MarqueeControlLibrary 屬性頁] 對話方塊中,選取 [偵錯] 頁面。

  3. 在 [開始動作] 區段中,選取 [啟動外部程式]。 您將針對個別的 Visual Studio 執行個體進行偵錯,因此,請按下省略符號 (The Ellipsis button (...) in the Properties window of Visual StudioVisual Studio [屬性] 視窗中的省略符號按鈕 (...)) 按鈕來瀏覽 Visual Studio IDE。 可執行檔的名稱是 devenv.exe,如果您已安裝至預設位置,則其路徑為 %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<版本>\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 (部分機器翻譯)。 其會公開兩個方法:Start (這會啟動霓虹燈動畫) 和 Stop (這會停止動畫)。 由於 MarqueeControl 包含實作 IMarqueeWidget 介面的子控制項,因此 StartStop 會列舉每個子控制項,並在實作 IMarqueeWidget 的每個子控制項上分別呼叫 StartMarqueeStopMarquee 方法。

MarqueeBorderMarqueeText 控制項的外觀取決於版面配置,因此,MarqueeControl 會覆寫 OnLayout 方法 (部分機器翻譯),並在此類型的子控制項上呼叫 PerformLayout (部分機器翻譯)。

這是 MarqueeControl 自訂的範圍。 執行階段功能是由 MarqueeBorderMarqueeText 控制項所實作,而設計階段功能是由 MarqueeBorderDesignerMarqueeControlRootDesigner 類別所實作。

實作自訂控制項

  1. 在 [程式碼編輯器] 中,開啟 MarqueeControl 來源檔案。 實作 StartStop 方法。

    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();
            }
        }
    }
    
    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
    
  2. 覆寫 OnLayout 方法。

    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();
            }
        }
    }
    
    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
    

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

MarqueeControl 將裝載兩種子控制項:MarqueeBorder 控制項和 MarqueeText 控制項。

  • MarqueeBorder:此控制項會在其邊緣周圍繪製「燈光」的框線。 燈光會依序閃爍,使其看起來像是在框線周圍移動。 燈光閃爍的速度是由稱為 UpdatePeriod 的屬性所控制。 其他數個自訂屬性會決定控制項外觀的其他層面。 有兩個稱為 StartMarqueeStopMarquee 的方法,可控制動畫的啟動和停止時間。

  • MarqueeText:此控制項會繪製閃爍的字串。 如同 MarqueeBorder 控制項,文字閃爍的速度是由 UpdatePeriod 屬性所控制。 MarqueeText 控制項也具有與 MarqueeBorder 控制項通用的 StartMarqueeStopMarquee 方法。

在設計階段,MarqueeControlRootDesigner 允許這兩個控制項類型以任意組合新增至 MarqueeControl

這兩個控制項的常見功能會被納入稱為 IMarqueeWidget 的介面中。 這可讓 MarqueeControl 探索任何與 Marquee 相關的子控制項,並給予其特殊待遇。

若要實作週期性動畫功能,您將使用來自 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.
        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;
        }
    }
    
    ' 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
    
  4. 將新的自訂控制項項目新增至 MarqueeControlLibrary 專案。 為新的來源檔案提供 "MarqueeText" 的基底名稱。

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

  6. 在 [屬性] 視窗中,將 BackgroundWorker 元件 (部分機器翻譯) 的 WorkerReportsProgressWorkerSupportsCancellation 屬性 (部分機器翻譯) 設定為 true。 這些設定可讓 BackgroundWorker 元件 (部分機器翻譯) 定期引發 ProgressChanged 事件 (部分機器翻譯) 和取消非同步更新。

    如需詳細資訊,請參閱 BackgroundWorker 元件 (部分機器翻譯)。

  7. 在 [程式碼編輯器] 中,開啟 MarqueeControl 來源檔案。 在檔案頂端,匯入下列命名空間:

    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;
    
    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
    
  8. MarqueeText 的宣告變更為繼承自 Label (部分機器翻譯),並實作 IMarqueeWidget 介面:

    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
    public partial class MarqueeText : Label, IMarqueeWidget
    {
    
    <ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeText
        Inherits Label
        Implements 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 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);
    }
    
    ' 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
    
  10. 實作 IMarqueeWidget 介面。

    StartMarqueeStopMarquee 方法會叫用 BackgroundWorker 元件 (部分機器翻譯) 的 RunWorkerAsync (部分機器翻譯) 和 CancelAsync 方法 (部分機器翻譯) 來啟動與停止動畫。

    Category (部分機器翻譯) 和 Browsable 屬性 (Attribute) (部分機器翻譯) 會套用至 UpdatePeriod 屬性 (Property),使其出現在名為 "Marquee" 的 [屬性] (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");
            }
        }
    }
    
    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
    
  11. 實作屬性存取子。 您會向用戶端公開兩個屬性:LightColorDarkColorCategory (部分機器翻譯) 和 Browsable 屬性 (Attribute) (部分機器翻譯) 會套用至這些屬性 (Property),因此,屬性 (Property) 會出現在稱為 "Marquee" 的 [屬性] (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);
            }
        }
    }
    
    <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
    
  12. 針對 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 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();
    }
    
    
    ' 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
    
  13. 覆寫 OnPaint 方法 (部分機器翻譯) 來啟用動畫。

    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);
    }
    
    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
    
  14. F6 以建置方案。

建立 MarqueeBorder 子控制項

相較於 MarqueeText 控制項,MarqueeBorder 控制項會稍微複雜一點。 其具有更多屬性,且 OnPaint 方法中的動畫更為複雜。 原則上,其與 MarqueeText 控制項相當類似。

因為 MarqueeBorder 控制項可以有子控制項,所以必須注意 Layout 事件 (部分機器翻譯)。

建立 MarqueeBorder 控制項

  1. 將新的自訂控制項項目新增至 MarqueeControlLibrary 專案。 為新的來源檔案提供 "MarqueeBorder" 的基底名稱。

  2. BackgroundWorker 元件從 [工具箱] 拖曳到您的 MarqueeText 控制項。 此元件將允許 MarqueeText 控制項以非同步方式自我更新。

  3. 在 [屬性] 視窗中,將 BackgroundWorker 元件 (部分機器翻譯) 的 WorkerReportsProgressWorkerSupportsCancellation 屬性 (部分機器翻譯) 設定為 true。 這些設定可讓 BackgroundWorker 元件 (部分機器翻譯) 定期引發 ProgressChanged 事件 (部分機器翻譯) 和取消非同步更新。 如需詳細資訊,請參閱 BackgroundWorker 元件 (部分機器翻譯)。

  4. 在 [屬性] 視窗中,選取 [事件] 按鈕。 針對 DoWork (部分機器翻譯) 和 ProgressChanged 事件 (部分機器翻譯) 附加處理常式。

  5. 在 [程式碼編輯器] 中,開啟 MarqueeControl 來源檔案。 在檔案頂端,匯入下列命名空間:

    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;
    
    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
    
  6. MarqueeBorder 的宣告變更為繼承自 Panel (部分機器翻譯),並實作 IMarqueeWidget 介面。

    [Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))]
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
    public partial class MarqueeBorder : Panel, IMarqueeWidget
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _
    ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeBorder
        Inherits Panel
        Implements IMarqueeWidget
    
  7. 宣告兩個列舉來管理 MarqueeBorder 控制項的狀態:MarqueeSpinDirection (這會決定框線周圍燈光「旋轉」的方向) 和 MarqueeLightShape (這會決定燈光的形狀 (方形或圓形))。 將這些宣告放置於 MarqueeBorder 類別宣告之前。

    // 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
    }
    
    ' 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
    
  8. 宣告對應至公開屬性的執行個體變數,並在建構函式中將其初始化。

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

    StartMarqueeStopMarquee 方法會叫用 BackgroundWorker 元件 (部分機器翻譯) 的 RunWorkerAsync (部分機器翻譯) 和 CancelAsync 方法 (部分機器翻譯) 來啟動與停止動畫。

    由於 MarqueeBorder 控制項可以包含子控制項,因此,StartMarquee 方法會列舉所有子控制項,並在實作 IMarqueeWidget 的子控制項上呼叫 StartMarqueeStopMarquee 方法具有類似的實作。

    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");
            }
        }
    }
    
    
    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
    
  10. 實作屬性存取子。 MarqueeBorder 控制項有數個屬性可控制其外觀。

    [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;
        }
    }
    
    
    <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
    
  11. 針對 BackgroundWorker 元件 (部分機器翻譯) 的 DoWork (部分機器翻譯) 和 ProgressChanged 事件 (部分機器翻譯) 實作處理常式。

    DoWork 事件處理常式 (部分機器翻譯) 會進入睡眠狀態 UpdatePeriod 所指定的毫秒數,然後引發 ProgressChanged 事件 (部分機器翻譯),直到您的程式碼呼叫 CancelAsync (部分機器翻譯) 來停止動畫為止。

    ProgressChanged 事件處理常式 (部分機器翻譯) 會遞增「基底」燈光的位置、從中判斷其他燈光的淺色/深色狀態,並呼叫 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 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();
    }
    
    ' 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
    
  12. 實作協助程式方法 IsLitDrawLight

    IsLit 方法會決定指定位置的燈光色彩。 「點亮」的燈光會以 LightColor 屬性所指定的色彩繪製,而「黑暗」的燈光會以 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 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;
                }
        }
    }
    
    ' 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
    
  13. 覆寫 OnLayoutOnPaint 方法。

    OnPaint 方法會沿著 MarqueeBorder 控制項邊緣繪製燈光。

    因為 OnPaint 方法相依於 MarqueeBorder 控制項的維度,所以,每當版面配置變更時,都需要呼叫它。 若要達成此目的,請覆寫 OnLayout (部分機器翻譯) 並呼叫 Refresh (部分機器翻譯)。

    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++;
            }
        }
    }
    
    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
    

建立自訂設計工具來為屬性製作陰影並進行篩選

MarqueeControlRootDesigner 類別會提供根設計工具的實作。 除了這個在 MarqueeControl 上運作的設計工具之外,您還需要特別與 MarqueeBorder 控制項相關聯的自訂設計工具。 此設計工具提供適合自訂根設計工具內容的自訂行為。

具體來說,MarqueeBorderDesigner 會為 MarqueeBorder 控制項上的特定屬性「製作陰影」並進行篩選,變更其與設計環境的互動。

攔截對元件屬性存取子的呼叫稱為「製作陰影」。其可讓設計工具追蹤使用者所設定的值,並選擇性地將該值傳遞到正在設計的元件。

針對此範例,MarqueeBorderDesigner 將為 Visible (部分機器翻譯) 和 Enabled 屬性 (英文) 製作陰影,這可防止使用者在設計階段使 MarqueeBorder 控制項無法顯示或停用。

設計工具也可以新增和移除屬性。 針對此範例,Padding 屬性 (英文) 將在設計階段移除,因為 MarqueeBorder 控制項會根據 LightSize 屬性所指定的燈光大小,以程式設計方式設定邊框間距。

MarqueeBorderDesigner 的基底類別是 ComponentDesigner (部分機器翻譯),其有方法可以變更控制項在設計階段公開的屬性 (Attribute)、屬性 (Property) 和事件:

使用這些方法變更元件的公用介面時,請遵循下列規則:

  • 僅新增或移除 PreFilter 方法中的項目

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

  • 一律在 PreFilter 方法中最先呼叫基底實作

  • 一律在 PostFilter 方法中最後呼叫基底實作

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

ComponentDesigner 類別 (部分機器翻譯) 提供字典來管理已製作陰影的屬性值,使您不需建立特定的執行個體變數。

建立自訂設計工具來為屬性製作陰影並進行篩選

  1. 以滑鼠右鍵按一下 [設計] 資料夾,然後新增類別。 為來源檔案提供 MarqueeBorderDesigner 的基底名稱。

  2. 在 [程式碼編輯器] 中,開啟 MarqueeBorderDesigner 來源檔案。 在檔案頂端,匯入下列命名空間:

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  3. MarqueeBorderDesigner 的宣告變更為繼承自 ParentControlDesigner (部分機器翻譯)。

    由於 MarqueeBorder 控制項可以包含子控制項,因此 MarqueeBorderDesigner 會繼承自 ParentControlDesigner (部分機器翻譯),以處理父代與子系的互動。

    namespace MarqueeControlLibrary.Design
    {
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
  4. 覆寫 PreFilterProperties (部分機器翻譯) 的基底實作。

    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]);
    }
    
    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
    
  5. 實作 EnabledVisible 屬性。 這些實作會為控制項的屬性製作陰影。

    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;
        }
    }
    
    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
    

處理元件變更

MarqueeControlRootDesigner 類別會為您的 MarqueeControl 執行個體提供自訂設計階段體驗。 大部分的設計階段功能都是繼承自 DocumentDesigner 類別 (部分機器翻譯)。 您的程式碼將實作兩個特定的自訂:處理元件變更,以及新增設計工具動詞命令。

當使用者設計其 MarqueeControl 執行個體時,您的根設計工具將追蹤 MarqueeControl 和其子控制項的變更。 設計階段環境提供一個方便的服務 (IComponentChangeService (部分機器翻譯)),可用來追蹤元件狀態的變更。

您可以使用 GetService 方法 (部分機器翻譯) 查詢環境,以取得對此服務的參考。 如果查詢成功,您的設計工具就可以針對 ComponentChanged 事件 (部分機器翻譯) 附加處理常式,並在設計階段執行維護一致狀態所需的任何工作。

MarqueeControlRootDesigner 類別的案例中,您將在 MarqueeControl 所包含的每個 IMarqueeWidget 物件上呼叫 Refresh 方法 (部分機器翻譯)。 當父系的 Size 之類的屬性變更時,這將導致 IMarqueeWidget 物件適當地自我重繪。

處理元件變更

  1. 在 [程式碼編輯器] 中開啟 MarqueeControlRootDesigner 來源檔案,並覆寫 Initialize 方法 (英文)。 呼叫 Initialize (英文) 的基底實作,並查詢 IComponentChangeService (部分機器翻譯)。

    base.Initialize(component);
    
    IComponentChangeService cs =
        GetService(typeof(IComponentChangeService))
        as IComponentChangeService;
    
    if (cs != null)
    {
        cs.ComponentChanged +=
            new ComponentChangedEventHandler(OnComponentChanged);
    }
    
    MyBase.Initialize(component)
    
    Dim cs As IComponentChangeService = _
    CType(GetService(GetType(IComponentChangeService)), _
    IComponentChangeService)
    
    If (cs IsNot Nothing) Then
        AddHandler cs.ComponentChanged, AddressOf OnComponentChanged
    End If
    
  2. 實作 OnComponentChanged 事件處理常式 (部分機器翻譯)。 測試傳送元件的類型,如果其為 IMarqueeWidget,則呼叫其 Refresh 方法 (部分機器翻譯)。

    private void OnComponentChanged(
        object sender,
        ComponentChangedEventArgs e)
    {
        if (e.Component is IMarqueeWidget)
        {
            this.Control.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
    

將設計工具動詞命令新增至自訂設計工具

設計工具動詞命令是連結至事件處理常式的功能表命令。 設計工具動詞命令會在設計階段新增至元件的捷徑功能表。 如需詳細資訊,請參閱DesignerVerb

您會將兩個設計工具動詞命令新增至設計工具:執行測試停止測試。 這些動詞命令將可讓您在設計階段檢視 MarqueeControl 的執行階段行為。 這些動詞命令將新增至 MarqueeControlRootDesigner

叫用 [執行測試] 時,動詞命令事件處理常式將會呼叫 MarqueeControl 上的 StartMarquee 方法。 叫用 [停止測試] 時,動詞命令事件處理常式將會呼叫 MarqueeControl 上的 StopMarquee 方法。 實作 StartMarqueeStopMarquee 方法,會在實作 IMarqueeWidget 的自主控制項上呼叫這些方法,因此,所有自主的 IMarqueeWidget 控制項也會參與測試。

將設計工具動詞命令新增至自訂設計工具

  1. MarqueeControlRootDesigner 類別中,新增名為 OnVerbRunTestOnVerbStopTest 的事件處理常式。

    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();
    }
    
    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
    
  2. 將這些事件處理常式連線到其對應的設計工具動詞命令。 MarqueeControlRootDesigner 會繼承來自其基底類別的 DesignerVerbCollection (部分機器翻譯)。 您將建立兩個新的 DesignerVerb 物件 (部分機器翻譯),並在 Initialize 方法 (英文) 中將其新增至此集合。

    this.Verbs.Add(
        new DesignerVerb("Run Test",
        new EventHandler(OnVerbRunTest))
        );
    
    this.Verbs.Add(
        new DesignerVerb("Stop Test",
        new EventHandler(OnVerbStopTest))
        );
    
    Me.Verbs.Add(New DesignerVerb("Run Test", _
    New EventHandler(AddressOf OnVerbRunTest)))
    
    Me.Verbs.Add(New DesignerVerb("Stop Test", _
    New EventHandler(AddressOf OnVerbStopTest)))
    

建立自訂 UITypeEditor

當您為使用者建立自訂設計階段體驗時,通常會想要建立與 [屬性] 視窗的自訂互動。 您可以透過建立 UITypeEditor (部分機器翻譯) 來完成此動作。

MarqueeBorder 控制項會在 [屬性] 視窗中公開數個屬性。 這其中兩個屬性 (MarqueeSpinDirectionMarqueeLightShape) 會由列舉表示。 為了說明 UI 類型編輯器的用法,MarqueeLightShape 屬性將有相關聯的 UITypeEditor 類別 (部分機器翻譯)。

建立自訂 UI 類型編輯器

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

  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.
    internal class 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
    
  3. 宣告稱為 editorServiceIWindowsFormsEditorService 執行個體變數 (部分機器翻譯)。

    private IWindowsFormsEditorService editorService = null;
    
    Private editorService As IWindowsFormsEditorService = Nothing
    
  4. 覆寫 OnLayout 方法。 此實作會傳回 DropDown (部分機器翻譯),告知設計環境如何顯示 LightShapeEditor

    public override UITypeEditorEditStyle GetEditStyle(
    System.ComponentModel.ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
    
    Public Overrides Function GetEditStyle( _
    ByVal context As System.ComponentModel.ITypeDescriptorContext) _
    As UITypeEditorEditStyle
        Return UITypeEditorEditStyle.DropDown
    End Function
    
    
  5. 覆寫 OnLayout 方法。 此實作會查詢 IWindowsFormsEditorService 物件 (部分機器翻譯) 的設計環境。 如果成功,其會建立 LightShapeSelectionControl。 會叫用 DropDownControl 方法 (英文) 來啟動 LightShapeEditor。 此叫用的傳回值會傳回到設計環境。

    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;
    }
    
    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
    

建立自訂 UITypeEditor 的檢視控制項

MarqueeLightShape 屬性支援兩種類型的淺色圖形:SquareCircle。 您將建立自訂控制項,僅用於以圖形方式在 [屬性] 視窗中顯示這些值。 您的 UITypeEditor (部分機器翻譯) 將使用此自訂控制項來與 [屬性] 視窗互動。

建立自訂 UI 類型編輯器的檢視控制項

  1. 將新的 UserControl 項目 (部分機器翻譯) 新增至 MarqueeControlLibrary 專案。 為新的來源檔案提供 LightShapeSelectionControl 的基底名稱。

  2. 從 [工具箱]Panel 控制項拖曳到 LightShapeSelectionControl。 將其命名為 squarePanelcirclePanel。 並排排列它們。 將這兩個 Panel 控制項 (部分機器翻譯) 的 Size 屬性 (部分機器翻譯) 設定為 [(60, 60)]。 將 squarePanel 控制項的 Location 屬性 (部分機器翻譯) 設定為 [(8, 10)]。 將 circlePanel 控制項的 Location 屬性設定為 [(80, 10)]。 最後,將 LightShapeSelectionControlSize 屬性 (部分機器翻譯) 設定為 [(150, 80)]

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

    Imports System.Windows.Forms.Design
    
    using System.Windows.Forms.Design;
    
  4. squarePanelcirclePanel 控制項實作 Click 事件處理常式 (部分機器翻譯)。 這些方法會叫用 CloseDropDown (英文) 來結束自訂 UITypeEditor 編輯工作階段 (部分機器翻譯)。

    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();
    }
    
    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
    
  5. 宣告稱為 editorServiceIWindowsFormsEditorService 執行個體變數 (部分機器翻譯)。

    Private editorService As IWindowsFormsEditorService
    
    private IWindowsFormsEditorService editorService;
    
  6. 宣告稱為 lightShapeValueMarqueeLightShape 執行個體變數。

    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
  7. LightShapeSelectionControl 建構函式中,將 Click 事件處理常式 (部分機器翻譯) 附加至 squarePanelcirclePanel 控制項的 Click 事件 (部分機器翻譯)。 此外,定義建構函式多載,將來自設計環境的 MarqueeLightShape 值指派給 lightShapeValue 欄位。

    // 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);
    }
    
    ' 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
    
  8. Dispose 方法中,中斷連結 Click 事件處理常式 (部分機器翻譯)。

    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 );
    }
    
    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
    
  9. 在方案總管中,按一下 [顯示所有檔案] 按鈕。 開啟 LightShapeSelectionControl.Designer.cs 或 LightShapeSelectionControl.Designer.vb 檔案,並移除 Dispose 方法 (部分機器翻譯) 的預設定義。

  10. 實作 LightShape 屬性。

    // 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;
            }
        }
    }
    
    ' 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
    
  11. 覆寫 OnLayout 方法。 此實作將繪製填滿的正方形和圓形。 其也將藉由在一個圖形或另一個圖形周圍繪製框線來醒目提示選取的值。

    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);
            }
        }	
    }
    
    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
    

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

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

建立自訂 MarqueeControl 實作

  1. 在 Windows Form 設計工具中開啟 DemoMarqueeControl。 這會建立 DemoMarqueeControl 類型的執行個體,並將其顯示在 MarqueeControlRootDesigner 類型的執行個體中。

  2. 在 [工具箱] 中,開啟 [MarqueeControlLibrary 元件] 索引標籤。您將看到 MarqueeBorderMarqueeText 控制項可供選取。

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

  4. MarqueeBorder 控制項的執行個體拖曳到 DemoMarqueeControl 設計介面。

  5. 建置方案。

  6. 以滑鼠右鍵按一下 DemoMarqueeControl,然後從捷徑功能表中選取 [執行測試] 選項,以啟動動畫。 按一下 [停止測試] 以停止動畫。

  7. 在設計檢視中開啟 [Form1]

  8. 將兩個 Button 控制項放置於表單上。 將其命名為 startButtonstopButton,並將 Text 屬性值 (部分機器翻譯) 分別變更為 [啟動] 和 [停止]

  9. 為這兩個 Button 控制項實作 Click 事件處理常式 (部分機器翻譯)。

  10. 在 [工具箱] 中,開啟 [MarqueeControlTest 元件] 索引標籤。您將看到 DemoMarqueeControl 變數可供選取。

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

  12. Click 事件處理常式 (部分機器翻譯) 中,叫用 DemoMarqueeControl 上的 StartStop 方法。

    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();
    }
    
  13. MarqueeControlTest 專案設定為啟始專案並加以執行。 您將看到顯示 DemoMarqueeControl 的表單。 選取 [啟動] 按鈕以開始動畫。 您應該會看到文字閃爍,且燈光在框線周圍移動。

下一步

MarqueeControlLibrary 示範自訂控制項和相關聯設計工具的簡單實作。 您可以透過數種方式來讓此範例變得更複雜:

  • 在設計工具中變更 DemoMarqueeControl 的屬性值。 新增更多 MarqueBorder 控制項,並將其停駐在其父執行個體內,以建立巢狀效果。 針對 UpdatePeriod 和燈光相關屬性的不同設定進行實驗。

  • 製作您自己的 IMarqueeWidget 實作。 例如,您可以建立閃爍的「霓虹燈」或具有多個影像的動畫風格標誌。

  • 進一步自訂設計階段體驗。 您可以嘗試為比 Enabled (英文) 和 Visible (部分機器翻譯) 還多的屬性製作陰影,而且可以新增屬性。 新增設計工具動詞命令來簡化常見工作,例如停駐子控制項。

  • 授權 MarqueeControl

  • 控制將控制項序列化的方式,以及為其產生程式碼的方式。 如需詳細資訊,請參閱產生和編譯動態原始程式碼 (部分機器翻譯)。

另請參閱