演练:创建一个利用 Visual Studio 设计时功能的 Windows 窗体控件

更新:2007 年 11 月

通过创作关联的自定义设计器可以改善自定义控件的设计时体验。

本演练演示如何为自定义控件创建自定义设计器。您将实现一个 MarqueeControl 类型以及一个名为 MarqueeControlRootDesigner 的关联设计器类。

MarqueeControl 类型实现类似舞台字幕的显示效果,带有变幻灯光和闪烁文本。

此控件的设计器与设计环境交互以提供自定义设计时体验。通过自定义设计器,可以在自定义 MarqueeControl 实现中按照各种搭配方式组合变幻灯光和闪烁文本。在窗体上可以像使用其他任何 Windows 窗体控件一样使用组合的控件。

本演练演示如下任务:

  • 创建项目

  • 创建控件库项目

  • 引用自定义控件项目

  • 定义自定义控件及其自定义设计器

  • 创建自定义控件的实例

  • 设置项目以便进行设计时调试

  • 实现自定义控件

  • 创建自定义控件的子控件

  • 创建 MarqueeBorder 子控件

  • 创建自定义设计器以隐藏和筛选属性

  • 处理组件更改

  • 将设计器谓词添加到自定义设计器

  • 创建自定义 UITypeEditor

  • 在设计器中测试自定义控件

完成这些操作后,自定义控件看起来会类似于下面这样:

可能的 MarqueeControl 排列

有关完整的代码清单,请参见如何:创建利用设计时功能的 Windows 窗体控件

说明:

显示的对话框和菜单命令可能会与“帮助”中的描述不同,具体取决于您的当前设置或版本。若要更改设置,请在“工具”菜单上选择“导入和导出设置”。有关更多信息,请参见 Visual Studio 设置

先决条件

若要完成本演练,您需要:

  • 足够的权限,以便能够在安装 Visual Studio 的计算机上创建和运行 Windows 窗体应用程序项目。

创建项目

第一步是创建应用程序项目。将使用此项目生成承载自定义控件的应用程序。

创建项目

创建控件库项目

下一步是创建控件库项目。将创建一个新的自定义控件及其相应的自定义设计器。

创建控件库项目

  1. 将 Windows 控件库项目添加到解决方案。有关更多信息,请参见“添加新项目”对话框。将项目命名为“MarqueeControlLibrary”。

  2. 使用“解决方案资源管理器”通过删除名为“UserControl1.cs”或“UserControl1.vb”的源文件来删除项目的默认控件,具体删除哪个源文件取决于您选择的语言。有关更多信息,请参见如何:移除、删除和排除项

  3. 将新的 UserControl 项添加到 MarqueeControlLibrary 项目。将新源文件的基名称命名为“MarqueeControl”。

  4. 使用“解决方案资源管理器”在 MarqueeControlLibrary 项目中新建一个文件夹。有关更多信息,请参见如何:添加新项目项。将新文件夹命名为“Design”。

  5. 右击“设计”文件夹并添加一个新类。将源文件的基名称命名为“MarqueeControlRootDesigner”。

  6. 您将需要使用 System.Design 程序集中的类型,因此请将此引用添加到 MarqueeControlLibrary 项目。有关更多信息,请参见如何:在 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 类声明。这将自定义控件与其设计器关联起来。

    <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 窗体设计器中打开 Form1。

  4. 在“工具箱”中找到“MarqueeControlTest 组件”选项卡并将其打开。从“工具箱”中将一个 DemoMarqueeControl 拖到窗体上。

  5. 生成项目。

设置项目以便进行设计时调试

开发自定义设计时体验时,将有必要调试您的控件和组件。若要设置项目以允许在设计时调试,有一种简单的方法。有关更多信息,请参见演练:设计时调试自定义 Windows 窗体控件

设置项目以便进行设计时调试

  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。它公开两个方法:一个是启动字幕动画的 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:此控件用紧密排列的“灯”围绕其边缘绘制边框。这些灯会依次闪烁,所以灯光看起来似乎在边框上流转。灯光闪烁的速度由一个名为 UpdatePeriod 的属性来控制。其他几个自定义属性相应确定该控件外观的其他方面。名称为别为 StartMarquee 和 StopMarquee 的两个方法控制动画开始和停止的时间。

  • MarqueeText:此控件绘制一个闪烁的字符串。与 MarqueeBorder 控件类似,文本闪烁的速度由 UpdatePeriod 属性控制。MarqueeText 控件还与 MarqueeBorder 控件共有 StartMarquee 和 StopMarquee 方法。

在设计时,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.
       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 方法来启动和停止动画。

    对 UpdatePeriod 属性 (Property) 应用 CategoryBrowsable 属性 (Attribute),以便在“属性”窗口中名为“Marquee”的自定义部分显示该属性 (Property)。

    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。对这些属性 (Property) 应用 CategoryBrowsable 属性 (Attribute),以便在“属性”窗口中名为“Marquee”的自定义部分显示这些属性 (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 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,它确定灯光绕边框“旋转”的方向;另一个是 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 事件处理程序递增“基”灯的位置,从基灯确定其他各灯的亮/暗状态;并调用 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. 实现这两个帮助器方法:IsLit 和 DrawLight。

    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 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 控件的尺寸,所以只要布局更改就需要调用它。为此,请重写 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 将“隐藏”和筛选 MarqueeBorder 控件上的某些属性,更改它们与设计环境的交互。

对组件的属性访问器的调用的截获操作称为“隐藏”。它允许设计器跟踪由用户设置的值,并且还可以将该值传递到要设计的组件。

对于此示例,VisibleEnabled 属性将被 MarqueeBorderDesigner 隐藏起来,这能够防止用户在设计时使 MarqueeBorder 控件不可见或被禁用。

设计器还可以添加和移除属性。对于此示例,由于 MarqueeBorder 控件基于由 LightSize 属性指定的灯光尺寸以编程方式设置边距,所以将在设计时移除 Padding 属性。

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 方法。这将导致 IMarqueeWidget 对象在诸如其父控件的 Size 等属性更改时相应地重新绘制自身。

处理组件更改

  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 的被包含的控件调用这些方法,因此被包含的任何 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。将在 Initialize 方法中创建两个新的 DesignerVerb 对象并将其添加至此集合。

    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 来实现。有关更多信息,请参见如何:创建用户界面类型编辑器

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 窗体设计器中打开 DemoMarqueeControl。这将创建一个 DemoMarqueeControl 类型的实例,并在 MarqueeControlRootDesigner 类型的一个实例中显示它。

  2. 在“工具箱”中打开“MarqueeControlLibrary 组件”选项卡。将显示 MarqueeBorder 和 MarqueeText 控件以供选择。

  3. 将 MarqueeBorder 控件的一个实例拖到 DemoMarqueeControl 设计图面上。将此 MarqueeBorder 控件停靠到父控件上。

  4. 将 MarqueeText 控件的一个实例拖到 DemoMarqueeControl 设计图面上。

  5. 生成解决方案。

  6. 右击 DemoMarqueeControl,然后从快捷菜单中选择“运行测试”选项启动动画。单击“停止测试”以停止动画。

  7. 在“设计”视图中打开“Form1”。

  8. 将两个 Button 控件放置至窗体上。将其分别命名为 startButton 和 stopButton,并将 Text 属性值分别更改为“启动”和“停止”。

  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 的实现。例如,可以创建一个闪烁的“霓虹标志”或带有多个图像的动画标志。

  • 进一步自定义设计时体验。可以尝试在 EnabledVisible 之外隐藏更多属性,而且可以添加新的属性。添加新的设计器谓词以简化常见任务,例如停靠子控件。

  • 为 MarqueeControl 添加许可控制。有关更多信息,请参见如何:授予组件和控件许可权限

  • 控制如何序列化控件以及如何为控件生成代码。有关更多信息,请参见 动态源代码生成和编译

请参见

任务

如何:创建利用设计时功能的 Windows 窗体控件

参考

UserControl

ParentControlDesigner

DocumentDesigner

IRootDesigner

DesignerVerb

UITypeEditor

BackgroundWorker

其他资源

扩展设计时支持

自定义设计器

.NET Shape Library: A Sample Designer(.NET 形状库:示例设计器)