演练:创建一个利用 Visual Studio 设计时功能的 Windows 窗体控件
通过创作关联的自定义设计器可以改善自定义控件的设计时体验。
本演练演示如何为自定义控件创建自定义设计器。您将实现一个 MarqueeControl
类型以及一个名为 MarqueeControlRootDesigner
的关联设计器类。
MarqueeControl
类型实现类似舞台字幕的显示效果,带有变幻灯光和闪烁文本。
此控件的设计器与设计环境交互以提供自定义设计时体验。通过自定义设计器,可以在自定义 MarqueeControl
实现中按照各种搭配方式组合变幻灯光和闪烁文本。在窗体上可以像使用其他任何 Windows 窗体控件一样使用组合的控件。
本演练中阐释的任务包括:
创建项目
创建控件库项目
引用自定义控件项目
定义自定义控件及其自定义设计器
创建自定义控件的实例
设置项目以便进行设计时调试
实现自定义控件
创建自定义控件的子控件
创建 MarqueeBorder 子控件
创建自定义设计器以隐藏和筛选属性
处理组件更改
将设计器谓词添加到自定义设计器
创建自定义 UITypeEditor
在设计器中测试自定义控件
完成这些操作后,自定义控件看起来会类似于下面这样:
有关完整的代码列表,请参见如何:创建利用设计时功能的 Windows 窗体控件。
备注
显示的对话框和菜单命令可能会与“帮助”中的描述不同,具体取决于您现用的设置或版本。若要更改设置,请在“工具”菜单上选择“导入和导出设置”。有关更多信息,请参见 Visual Studio 设置。
先决条件
若要完成本演练,您需要:
- 具有充分的权限以便能够在安装有 Visual Studio 的计算机上创建和运行 Windows 窗体应用程序项目。
创建项目
第一步是创建应用程序项目。将使用此项目生成承载自定义控件的应用程序。
创建项目
- 创建一个名为“MarqueeControlTest”的 Windows 应用程序项目。有关更多信息,请参见 如何:创建 Windows 应用程序项目。
创建控件库项目
下一步是创建控件库项目。将创建一个新的自定义控件及其相应的自定义设计器。
创建控件库项目
将 Windows 控件库项目添加到解决方案。有关更多信息,请参见 “添加新项目”对话框。将项目命名为“MarqueeControlLibrary”。
使用“解决方案资源管理器”通过删除名为“UserControl1.cs”或“UserControl1.vb”的源文件来删除项目的默认控件,具体删除哪个源文件取决于您选择的语言。有关更多信息,请参见 如何:移除、删除和排除项。
将新的 UserControl 项添加到 MarqueeControlLibrary 项目。将新源文件的基名称命名为“MarqueeControl”。
使用“解决方案资源管理器”在 MarqueeControlLibrary 项目中新建一个文件夹。有关更多信息,请参见 如何:添加新项目项。将新文件夹命名为“Design”。
右击“设计”文件夹并添加一个新类。将源文件的基名称命名为“MarqueeControlRootDesigner”。
您将需要使用 System.Design 程序集中的类型,因此请将此引用添加到 MarqueeControlTest 项目。有关更多信息,请参见 如何:在 Visual Studio 中添加和移除引用(C#、J#)。
引用自定义控件项目
将使用 MarqueeControlTest 项目测试自定义控件。在添加对 MarqueeControlLibrary 程序集的项目引用时,该测试项目就会知悉该自定义控件。
引用自定义控件项目
- 在 MarqueeControlTest 项目中添加对 MarqueeControlLibrary 程序集的项目引用。请确保使用“添加引用”对话框中的“项目”选项卡,而不要直接引用 MarqueeControlLibrary 程序集。
定义自定义控件及其自定义设计器
自定义控件将从 UserControl 类派生。这允许您的控件包含其他控件,并为您的控件提供大量默认功能。
自定义控件将有一个关联的自定义设计器。这允许您创建专为自定义控件定制的独特设计体验。
将通过使用 DesignerAttribute 类将该控件与其设计器关联起来。由于要开发自定义控件的整个设计时行为,自定义设计器将实现 IRootDesigner 接口。
定义自定义控件及其自定义设计器
在“代码编辑器”中打开
MarqueeControl
源文件。在文件顶部导入以下命名空间:Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Drawing Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Windows.Forms; using System.Windows.Forms.Design;
将 DesignerAttribute 添加到
MarqueeControl
类声明。这将自定义控件与其设计器关联起来。<Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _ GetType(IRootDesigner))> _ Public Class MarqueeControl Inherits UserControl
[Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )] public class MarqueeControl : UserControl {
在“代码编辑器”中打开
MarqueeControlRootDesigner
源文件。在文件顶部导入以下命名空间:Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing.Design Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing.Design; using System.Windows.Forms; using System.Windows.Forms.Design;
更改
MarqueeControlRootDesigner
的声明,以便从 DocumentDesigner 类继承。应用 ToolboxItemFilterAttribute,以指定该设计器与“工具箱”进行交互。注意
MarqueeControlRootDesigner
类的定义放在名为“MarqueeControlLibrary.Design”的命名空间中。此声明将设计器放在一个为设计相关类型保留的专用命名空间中。Namespace MarqueeControlLibrary.Design <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _ Public Class MarqueeControlRootDesigner Inherits DocumentDesigner
namespace MarqueeControlLibrary.Design { [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] public class MarqueeControlRootDesigner : DocumentDesigner {
定义
MarqueeControlRootDesigner
类的构造函数。将一条 WriteLine 语句插入到构造函数体中。这对调试很有用。Public Sub New() Trace.WriteLine("MarqueeControlRootDesigner ctor") End Sub
public MarqueeControlRootDesigner() { Trace.WriteLine("MarqueeControlRootDesigner ctor"); }
创建自定义控件的实例
若要观察控件的自定义设计时行为,需将控件实例置于 MarqueeControlTest 项目中的窗体上。
创建自定义控件的实例
将新的 UserControl 项添加到 MarqueeControlTest 项目。将新源文件的基名称命名为“DemoMarqueeControl”。
在“代码编辑器”中打开
DemoMarqueeControl
文件。在文件顶部导入 MarqueeControlLibrary 命名空间:
Imports MarqueeControlLibrary
using MarqueeControlLibrary;
设置项目以便进行设计时调试
开发自定义设计时体验时,将有必要调试您的控件和组件。要设置项目以允许在设计时调试,有一种简单的方法。有关更多信息,请参见 演练:设计时调试自定义 Windows 窗体控件。
设置项目以便进行设计时调试
右击 MarqueeControlLibrary 项目并选择“属性”。
在“MarqueeControlLibrary 属性页”对话框中选择“配置属性”页。
在“启动操作”部分,选择“启动外部程序”。您将要调试一个单独的 Visual Studio 实例,因此请单击省略号 (
) 按钮浏览找到 Visual Studio IDE。可执行文件的名称为 devenv.exe,如果您安装在默认位置,其路径将为“C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe”。
单击“确定”关闭对话框。
右击 MarqueeControlLibrary 项目并选择“设置为启动项目”以启用此调试配置。
检查点
现在就可以调试自定义控件的设计时行为了。确定正确设置了调试环境之后,将测试自定义控件与自定义设计器之间的关联。
测试调试环境和设计器关联
在“代码编辑器”中打开
MarqueeControlRootDesigner
源文件,并在 WriteLine 语句上放置一个断点。按 F5 启动调试会话。注意,将创建一个 Visual Studio 新实例。
在 Visual Studio 的新实例中打开“MarqueeControlTest”解决方案。通过从“文件”菜单选择“最近的项目”可以很方便地找到该解决方案。“MarqueeControlTest.sln”解决方案文件将会作为最新使用过的文件列出来。
在设计器中打开
DemoMarqueeControl
。注意,Visual Studio 调试实例将获得焦点,并且执行会在断点处停止。按 F5 继续调试会话。
此时,开发和调试自定义控件及其关联的自定义设计器所需的一切准备工作均已就绪。本演练的其余部分将关注于实现控件和设计器功能的细节。
实现自定义控件
MarqueeControl
是一个略加定制的 UserControl。它公开两个方法:一个是启动字幕动画的 Start
,一个是停止动画的 Stop
。由于 MarqueeControl
包含一些可实现 IMarqueeWidget
接口的子控件,因此,Start
和 Stop
将枚举每个子控件,并对实现 IMarqueeWidget
的每个子控件分别调用 StartMarquee
和 StopMarquee
方法。
MarqueeBorder
和 MarqueeText
控件的外观依赖于布局,因此 MarqueeControl
将重写 OnLayout 方法并对此类型的子控件调用 PerformLayout。
这就是 MarqueeControl
自定义的范围。运行时功能由 MarqueeBorder
和 MarqueeText
控件实现,而设计时功能则由 MarqueeBorderDesigner
和 MarqueeControlRootDesigner
类实现。
实现自定义控件
在“代码编辑器”中打开
MarqueeControl
源文件。实现Start
和Stop
方法。Public Sub Start() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so ' find each IMarqueeWidget child and call its ' StartMarquee method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl End Sub Public Sub [Stop]() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl End Sub
public void Start() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so // find each IMarqueeWidget child and call its // StartMarquee method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } } public void Stop() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } }
重写 OnLayout 方法。
Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs) MyBase.OnLayout(levent) ' Repaint all IMarqueeWidget children if the layout ' has changed. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) cntrl.PerformLayout() End If Next cntrl End Sub
protected override void OnLayout(LayoutEventArgs levent) { base.OnLayout (levent); // Repaint all IMarqueeWidget children if the layout // has changed. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { Control control = cntrl as Control; control.PerformLayout(); } } }
创建自定义控件的子控件
MarqueeControl
将承载两种子控件:MarqueeBorder
控件和 MarqueeText
控件。
MarqueeBorder
:此控件用紧密排列的“灯”围绕其边缘绘制边框。这些灯会依次闪烁,所以灯光看起来似乎在边框上流转。灯光闪烁的速度由一个名为UpdatePeriod
的属性来控制。其他几个自定义属性相应确定该控件外观的其他方面。名称为别为StartMarquee
和StopMarquee
的两个方法控制动画开始和停止的时间。MarqueeText
:此控件绘制一个闪烁的字符串。与MarqueeBorder
控件类似,文本闪烁的速度由UpdatePeriod
属性控制。MarqueeText
控件还与MarqueeBorder
控件共有StartMarquee
和StopMarquee
方法。
在设计时,MarqueeControlRootDesigner
允许将这两个控件类型以任意组合方式添加到 MarqueeControl
中。
这两个控件的公共特性包含在一个名为 IMarqueeWidget
的接口中。这允许 MarqueeControl
发现任何与 Marquee 相关的子控件并对其进行特殊处理。
为实现周期动画特性,需要使用 System.ComponentModel 命名空间中的 BackgroundWorker 对象。您可以使用 Timer 对象,但存在较多 IMarqueeWidget
对象时,单个 UI 线程可能无法跟上动画的速度。
创建自定义控件的子控件
将新的类项添加到 MarqueeControlLibrary 项目。将新源文件的基名称命名为“IMarqueeWidget”。
在“代码编辑器”中打开
IMarqueeWidget
源文件,并将声明从 class 更改为 interface:' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget
// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget {
将下面的代码添加到
IMarqueeWidget
接口以公开操作字幕动画的两个方法和一个属性:' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget ' This method starts the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StartMarquee on all ' its IMarqueeWidget child controls. Sub StartMarquee() ' This method stops the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StopMarquee on all ' its IMarqueeWidget child controls. Sub StopMarquee() ' This method specifies the refresh rate for the animation, ' in milliseconds. Property UpdatePeriod() As Integer End Interface
// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget { // This method starts the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StartMarquee on all // its IMarqueeWidget child controls. void StartMarquee(); // This method stops the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StopMarquee on all // its IMarqueeWidget child controls. void StopMarquee(); // This method specifies the refresh rate for the animation, // in milliseconds. int UpdatePeriod { get; set; } }
将新的“自定义控件”项添加到 MarqueeControlLibrary 项目中。将新源文件的基名称命名为“MarqueeText”。
从“工具箱”中将一个 BackgroundWorker 组件拖到
MarqueeText
控件上。此组件将允许MarqueeText
控件对其自身进行异步更新。在“属性”窗口中,将 BackgroundWorker 组件的 WorkerReportsProgess 和 WorkerSupportsCancellation 属性设置为 true。这些设置允许 BackgroundWorker 组件定期引发 ProgressChanged 事件和取消异步更新。有关更多信息,请参见BackgroundWorker 组件。
在“代码编辑器”中打开
MarqueeText
源文件。在文件顶部导入以下命名空间:Imports System Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
更改
MarqueeText
的声明以便从 Label 继承并实现IMarqueeWidget
接口:<ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeText Inherits Label Implements IMarqueeWidget
[ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] public partial class MarqueeText : Label, IMarqueeWidget {
声明与公开的属性对应的实例变量,并在构造器中对其初始化。
isLit
字段确定是否用由LightColor
属性给定的颜色绘制文本。' When isLit is true, the text is painted in the light color; ' When isLit is false, the text is painted in the dark color. ' This value changes whenever the BackgroundWorker component ' raises the ProgressChanged event. Private isLit As Boolean = True ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightColorValue As Color Private darkColorValue As Color ' These brushes are used to paint the light and dark ' colors of the text. Private lightBrush As Brush Private darkBrush As Brush ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) End Sub 'New
// When isLit is true, the text is painted in the light color; // When isLit is false, the text is painted in the dark color. // This value changes whenever the BackgroundWorker component // raises the ProgressChanged event. private bool isLit = true; // These fields back the public properties. private int updatePeriodValue = 50; private Color lightColorValue; private Color darkColorValue; // These brushes are used to paint the light and dark // colors of the text. private Brush lightBrush; private Brush darkBrush; // This component updates the control asynchronously. private BackgroundWorker backgroundWorker1; public MarqueeText() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); }
实现
IMarqueeWidget
接口。StartMarquee
和StopMarquee
方法调用 BackgroundWorker 组件的 RunWorkerAsync 和 CancelAsync 方法来启动和停止动画。对
UpdatePeriod
属性 (Property) 应用 Category 和 Browsable 属性 (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"); } } }
实现属性访问器。将向客户端公开两个属性:
LightColor
和DarkColor
。对这些属性 (Property) 应用 Category 和 Browsable 属性 (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); } } }
实现 BackgroundWorker 组件的 DoWork 和 ProgressChanged 事件的处理程序。
DoWork 事件处理程序休眠一段由
UpdatePeriod
指定的时间(以毫秒表示),然后引发 ProgressChanged 事件,直至代码通过调用 CancelAsync 停止动画。ProgressChanged 事件处理程序在文本的亮和暗这两种状态之间切换,给人以闪烁的感觉。
' This method is called in the worker thread's context, ' so it must not make any calls into the MarqueeText control. ' Instead, it communicates to the control using the ' ProgressChanged event. ' ' The only work done in this event handler is ' to sleep for the number of milliseconds specified ' by UpdatePeriod, then raise the ProgressChanged event. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles backgroundWorker1.DoWork Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) ' This event handler will run until the client cancels ' the background task by calling CancelAsync. While Not worker.CancellationPending ' The Argument property of the DoWorkEventArgs ' object holds the value of UpdatePeriod, which ' was passed as the argument to the RunWorkerAsync ' method. Thread.Sleep(Fix(e.Argument)) ' The DoWork eventhandler does not actually report ' progress; the ReportProgress event is used to ' periodically alert the control to update its state. worker.ReportProgress(0) End While End Sub ' The ProgressChanged event is raised by the DoWork method. ' This event handler does work that is internal to the ' control. In this case, the text is toggled between its ' light and dark state, and the control is told to ' repaint itself. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.isLit = Not Me.isLit Me.Refresh() End Sub
// This method is called in the worker thread's context, // so it must not make any calls into the MarqueeText control. // Instead, it communicates to the control using the // ProgressChanged event. // // The only work done in this event handler is // to sleep for the number of milliseconds specified // by UpdatePeriod, then raise the ProgressChanged event. private void backgroundWorker1_DoWork( object sender, System.ComponentModel.DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; // This event handler will run until the client cancels // the background task by calling CancelAsync. while (!worker.CancellationPending) { // The Argument property of the DoWorkEventArgs // object holds the value of UpdatePeriod, which // was passed as the argument to the RunWorkerAsync // method. Thread.Sleep((int)e.Argument); // The DoWork eventhandler does not actually report // progress; the ReportProgress event is used to // periodically alert the control to update its state. worker.ReportProgress(0); } } // The ProgressChanged event is raised by the DoWork method. // This event handler does work that is internal to the // control. In this case, the text is toggled between its // light and dark state, and the control is told to // repaint itself. private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e) { this.isLit = !this.isLit; this.Refresh(); }
重写 OnPaint 方法以启用动画。
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) ' The text is painted in the light or dark color, ' depending on the current value of isLit. Me.ForeColor = IIf(Me.isLit, Me.lightColorValue, Me.darkColorValue) MyBase.OnPaint(e) End Sub
protected override void OnPaint(PaintEventArgs e) { // The text is painted in the light or dark color, // depending on the current value of isLit. this.ForeColor = this.isLit ? this.lightColorValue : this.darkColorValue; base.OnPaint(e); }
按 F6 键生成解决方案。
创建 MarqueeBorder 子控件
MarqueeBorder
控件比 MarqueeText
控件稍复杂。它有更多属性,并且 OnPaint 方法中的动画也更为繁杂。大体上,它与 MarqueeText
控件非常类似。
由于 MarqueeBorder
控件可有子控件,因此需要向其通知 Layout 事件。
创建 MarqueeBorder 控件
将新的“自定义控件”项添加到 MarqueeControlLibrary 项目中。将新源文件的基名称命名为“MarqueeBorder”。
从“工具箱”中将一个 BackgroundWorker 组件拖到
MarqueeBorder
控件上。此组件将允许MarqueeBorder
控件对其自身进行异步更新。在“属性”窗口中,将 BackgroundWorker 组件的 WorkerReportsProgess 和 WorkerSupportsCancellation 属性设置为 true。这些设置允许 BackgroundWorker 组件定期引发 ProgressChanged 事件和取消异步更新。有关更多信息,请参见BackgroundWorker 组件。
在“属性”窗口中,单击“事件”按钮。为 DoWork 和 ProgressChanged 事件附加处理程序。
在“代码编辑器”中打开
MarqueeBorder
源文件。在文件顶部导入以下命名空间:Imports System Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Drawing.Design Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Drawing.Design; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
更改
MarqueeBorder
的声明以便从 Panel 继承以及实现IMarqueeWidget
接口。<Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeBorder Inherits Panel Implements IMarqueeWidget
[Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] public partial class MarqueeBorder : Panel, IMarqueeWidget {
声明两个枚举以用于管理
MarqueeBorder
控件的状态:一个是MarqueeSpinDirection
,它确定灯光绕边框“旋转”的方向;另一个是MarqueeLightShape
,它确定灯光的形状(方形还是圆形)。将这些声明放在MarqueeBorder
类声明之前。' This defines the possible values for the MarqueeBorder ' control's SpinDirection property. Public Enum MarqueeSpinDirection CW CCW End Enum ' This defines the possible values for the MarqueeBorder ' control's LightShape property. Public Enum MarqueeLightShape Square Circle End Enum
// This defines the possible values for the MarqueeBorder // control's SpinDirection property. public enum MarqueeSpinDirection { CW, CCW } // This defines the possible values for the MarqueeBorder // control's LightShape property. public enum MarqueeLightShape { Square, Circle }
声明与公开的属性对应的实例变量,并在构造函数中对其初始化。
Public Shared MaxLightSize As Integer = 10 ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightSizeValue As Integer = 5 Private lightPeriodValue As Integer = 3 Private lightSpacingValue As Integer = 1 Private lightColorValue As Color Private darkColorValue As Color Private spinDirectionValue As MarqueeSpinDirection = MarqueeSpinDirection.CW Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square ' These brushes are used to paint the light and dark ' colors of the marquee lights. Private lightBrush As Brush Private darkBrush As Brush ' This field tracks the progress of the "first" light as it ' "travels" around the marquee border. Private currentOffset As Integer = 0 ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As System.ComponentModel.BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) ' The MarqueeBorder control manages its own padding, ' because it requires that any contained controls do ' not overlap any of the marquee lights. Dim pad As Integer = 2 * (Me.lightSizeValue + Me.lightSpacingValue) Me.Padding = New Padding(pad, pad, pad, pad) SetStyle(ControlStyles.OptimizedDoubleBuffer, True) End Sub
public static int MaxLightSize = 10; // These fields back the public properties. private int updatePeriodValue = 50; private int lightSizeValue = 5; private int lightPeriodValue = 3; private int lightSpacingValue = 1; private Color lightColorValue; private Color darkColorValue; private MarqueeSpinDirection spinDirectionValue = MarqueeSpinDirection.CW; private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square; // These brushes are used to paint the light and dark // colors of the marquee lights. private Brush lightBrush; private Brush darkBrush; // This field tracks the progress of the "first" light as it // "travels" around the marquee border. private int currentOffset = 0; // This component updates the control asynchronously. private System.ComponentModel.BackgroundWorker backgroundWorker1; public MarqueeBorder() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); // The MarqueeBorder control manages its own padding, // because it requires that any contained controls do // not overlap any of the marquee lights. int pad = 2 * (this.lightSizeValue + this.lightSpacingValue); this.Padding = new Padding(pad, pad, pad, pad); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); }
实现
IMarqueeWidget
接口。StartMarquee
和StopMarquee
方法调用 BackgroundWorker 组件的 RunWorkerAsync 和 CancelAsync 方法来启动和停止动画。由于
MarqueeBorder
控件可以包含子控件,所以StartMarquee
方法将枚举所有子控件,并对其中实现了IMarqueeWidget
的子控件调用StartMarquee
。StopMarquee
方法有类似实现。Public Overridable Sub StartMarquee() _ Implements IMarqueeWidget.StartMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StartMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl ' Start the updating thread and pass it the UpdatePeriod. Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod) End Sub Public Overridable Sub StopMarquee() _ Implements IMarqueeWidget.StopMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl ' Stop the updating thread. Me.backgroundWorker1.CancelAsync() End Sub <Category("Marquee"), Browsable(True)> _ Public Overridable Property UpdatePeriod() As Integer _ Implements IMarqueeWidget.UpdatePeriod Get Return Me.updatePeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.updatePeriodValue = Value Else Throw New ArgumentOutOfRangeException("UpdatePeriod", _ "must be > 0") End If End Set End Property
public virtual void StartMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StartMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } // Start the updating thread and pass it the UpdatePeriod. this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod); } public virtual void StopMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } // Stop the updating thread. this.backgroundWorker1.CancelAsync(); } [Category("Marquee")] [Browsable(true)] public virtual int UpdatePeriod { get { return this.updatePeriodValue; } set { if (value > 0) { this.updatePeriodValue = value; } else { throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0"); } } }
实现属性访问器。
MarqueeBorder
控件有几个用来控制其自身外观的属性。<Category("Marquee"), Browsable(True)> _ Public Property LightSize() As Integer Get Return Me.lightSizeValue End Get Set(ByVal Value As Integer) If Value > 0 AndAlso Value <= MaxLightSize Then Me.lightSizeValue = Value Me.DockPadding.All = 2 * Value Else Throw New ArgumentOutOfRangeException("LightSize", _ "must be > 0 and < MaxLightSize") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightPeriod() As Integer Get Return Me.lightPeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.lightPeriodValue = Value Else Throw New ArgumentOutOfRangeException("LightPeriod", _ "must be > 0 ") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightColor() As Color Get Return Me.lightColorValue End Get Set(ByVal Value As Color) ' The LightColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then Me.lightColorValue = Value Me.lightBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property DarkColor() As Color Get Return Me.darkColorValue End Get Set(ByVal Value As Color) ' The DarkColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then Me.darkColorValue = Value Me.darkBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightSpacing() As Integer Get Return Me.lightSpacingValue End Get Set(ByVal Value As Integer) If Value >= 0 Then Me.lightSpacingValue = Value Else Throw New ArgumentOutOfRangeException("LightSpacing", _ "must be >= 0") End If End Set End Property <Category("Marquee"), Browsable(True), _ EditorAttribute(GetType(LightShapeEditor), _ GetType(System.Drawing.Design.UITypeEditor))> _ Public Property LightShape() As MarqueeLightShape Get Return Me.lightShapeValue End Get Set(ByVal Value As MarqueeLightShape) Me.lightShapeValue = Value End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property SpinDirection() As MarqueeSpinDirection Get Return Me.spinDirectionValue End Get Set(ByVal Value As MarqueeSpinDirection) Me.spinDirectionValue = Value End Set End Property
[Category("Marquee")] [Browsable(true)] public int LightSize { get { return this.lightSizeValue; } set { if (value > 0 && value <= MaxLightSize) { this.lightSizeValue = value; this.DockPadding.All = 2 * value; } else { throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize"); } } } [Category("Marquee")] [Browsable(true)] public int LightPeriod { get { return this.lightPeriodValue; } set { if (value > 0) { this.lightPeriodValue = value; } else { throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 "); } } } [Category("Marquee")] [Browsable(true)] public Color LightColor { get { return this.lightColorValue; } set { // The LightColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.lightColorValue.ToArgb() != value.ToArgb()) { this.lightColorValue = value; this.lightBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public Color DarkColor { get { return this.darkColorValue; } set { // The DarkColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.darkColorValue.ToArgb() != value.ToArgb()) { this.darkColorValue = value; this.darkBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public int LightSpacing { get { return this.lightSpacingValue; } set { if (value >= 0) { this.lightSpacingValue = value; } else { throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0"); } } } [Category("Marquee")] [Browsable(true)] [EditorAttribute(typeof(LightShapeEditor), typeof(System.Drawing.Design.UITypeEditor))] public MarqueeLightShape LightShape { get { return this.lightShapeValue; } set { this.lightShapeValue = value; } } [Category("Marquee")] [Browsable(true)] public MarqueeSpinDirection SpinDirection { get { return this.spinDirectionValue; } set { this.spinDirectionValue = value; } }
实现 BackgroundWorker 组件的 DoWork 和 ProgressChanged 事件的处理程序。
DoWork 事件处理程序休眠一段由
UpdatePeriod
指定的时间(以毫秒表示),然后引发 ProgressChanged 事件,直至代码通过调用 CancelAsync 停止动画。ProgressChanged 事件处理程序递增“基”灯的位置,从基灯确定其他各灯的亮/暗状态;并调用 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(); }
实现这两个帮助器方法:
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; } } }
重写 OnLayout 和 OnPaint 方法。
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
控件上的某些属性,更改它们与设计环境的交互。
对组件的属性访问器的调用的截获操作称为“隐藏”。它允许设计器跟踪由用户设置的值,并且还可以将该值传递到要设计的组件。
对于此示例,Visible 和 Enabled 属性将被 MarqueeBorderDesigner
隐藏起来,这能够防止用户在设计时使 MarqueeBorder
控件不可见或被禁用。
设计器还可以添加和移除属性。对于此示例,由于 MarqueeBorder
控件基于由 LightSize
属性指定的灯光尺寸以编程方式设置边距,所以将在设计时移除 Padding 属性。
MarqueeBorderDesigner
的基类是 ComponentDesigner,该类包含能够更改控件在设计时公开的属性 (Attribute)、属性 (Property) 和事件的方法:
使用这些方法更改组件的公共接口时,必须遵循下列规则:
仅在 PreFilter 方法中移除项
仅在 PostFilter 方法中修改现有项
在 PreFilter 方法中始终首先调用基实现
在 PostFilter 方法中始终最后调用基实现
遵循这些规则可确保设计时环境中的所有这些设计器能够一致地显示所有要设计的组件。
ComponentDesigner 类提供了一个字典,以便管理被隐藏的属性的值,从而免除了您创建特定实例变量的需要。
创建自定义设计器以隐藏和筛选属性
右击“设计”文件夹并添加一个新类。将源文件的基名称命名为“MarqueeBorderDesigner”。
在“代码编辑器”中打开
MarqueeBorderDesigner
源文件。在文件顶部导入以下命名空间:Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Windows.Forms; using System.Windows.Forms.Design;
更改
MarqueeBorderDesigner
的声明,以便从 ParentControlDesigner 继承。由于
MarqueeBorder
控件可包含子控件,MarqueeBorderDesigner
将从处理父子交互的 ParentControlDesigner 继承。Namespace MarqueeControlLibrary.Design <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _ Public Class MarqueeBorderDesigner Inherits ParentControlDesigner
namespace MarqueeControlLibrary.Design { [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] public class MarqueeBorderDesigner : ParentControlDesigner {
重写 PreFilterProperties 的基实现。
Protected Overrides Sub PreFilterProperties( _ ByVal properties As IDictionary) MyBase.PreFilterProperties(properties) If properties.Contains("Padding") Then properties.Remove("Padding") End If properties("Visible") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Visible"), PropertyDescriptor), _ New Attribute(-1) {}) properties("Enabled") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Enabled"), _ PropertyDescriptor), _ New Attribute(-1) {}) End Sub
protected override void PreFilterProperties(IDictionary properties) { base.PreFilterProperties(properties); if (properties.Contains("Padding")) { properties.Remove("Padding"); } properties["Visible"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Visible"], new Attribute[0]); properties["Enabled"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Enabled"], new Attribute[0]); }
实现 Enabled 和 Visible 属性。这些实现隐藏了该控件的属性。
Public Property Visible() As Boolean Get Return CBool(ShadowProperties("Visible")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Visible") = Value End Set End Property Public Property Enabled() As Boolean Get Return CBool(ShadowProperties("Enabled")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Enabled") = Value End Set End Property
public bool Visible { get { return (bool)ShadowProperties["Visible"]; } set { this.ShadowProperties["Visible"] = value; } } public bool Enabled { get { return (bool)ShadowProperties["Enabled"]; } set { this.ShadowProperties["Enabled"] = value; } }
处理组件更改
MarqueeControlRootDesigner
类为您的 MarqueeControl
实例提供自定义设计时体验。大多数设计时功能是从 DocumentDesigner 类继承的;您的代码将实现两个特定的定制:处理组件更改和添加设计器谓词。
用户设计 MarqueeControl
实例时,根设计器将跟踪对 MarqueeControl
及其子控件的更改。设计时环境提供了一项便利服务 IComponentChangeService 以用于跟踪对组件状态的更改。
通过使用 GetService 方法查询环境可获得对此服务的引用。如果查询成功,设计器可为 ComponentChanged 事件附加处理程序,执行在设计时维护状态一致所需的任何任务。
对于 MarqueeControlRootDesigner
类,将对 MarqueeControl
所包含的每个 IMarqueeWidget
对象调用 Refresh 方法。这将导致 IMarqueeWidget
对象在诸如其父控件的 Size 等属性更改时相应地重新绘制自身。
处理组件更改
在“代码编辑器”中打开
MarqueeControlRootDesigner
源文件,并重写 Initialize 方法。调用 Initialize 的基实现并查询 IComponentChangeService 是否存在。MyBase.Initialize(component) Dim cs As IComponentChangeService = _ CType(GetService(GetType(IComponentChangeService)), _ IComponentChangeService) If (cs IsNot Nothing) Then AddHandler cs.ComponentChanged, AddressOf OnComponentChanged End If
base.Initialize(component); IComponentChangeService cs = GetService(typeof(IComponentChangeService)) as IComponentChangeService; if (cs != null) { cs.ComponentChanged += new ComponentChangedEventHandler(OnComponentChanged); }
实现 OnComponentChanged 事件处理程序。测试发送组件的类型,如果类型为
IMarqueeWidget
,则调用其 Refresh 方法。Private Sub OnComponentChanged( _ ByVal sender As Object, _ ByVal e As ComponentChangedEventArgs) If TypeOf e.Component Is IMarqueeWidget Then Me.Control.Refresh() End If End Sub
private void OnComponentChanged( object sender, ComponentChangedEventArgs e) { if (e.Component is IMarqueeWidget) { this.Control.Refresh(); } }
将设计器谓词添加到自定义设计器
设计器谓词是与事件处理程序链接的菜单命令。设计器谓词在设计时被添加到组件的快捷菜单上。有关更多信息,请参见 DesignerVerb。
将向设计器添加两个设计器谓词:“运行测试”和“停止测试”。这些谓词将允许您在设计时查看 MarqueeControl
的运行时行为。这些谓词将被添加到 MarqueeControlRootDesigner
。
调用“运行测试”时,谓词事件处理程序将对 MarqueeControl
调用 StartMarquee
方法。调用“停止测试”时,谓词事件处理程序将对 MarqueeControl
调用 StopMarquee
方法。StartMarquee
和 StopMarquee
方法的实现对实现 IMarqueeWidget
的被包含的控件调用这些方法,因此被包含的任何 IMarqueeWidget
控件也将参与测试。
将设计器谓词添加到自定义设计器
在
MarqueeControlRootDesigner
类中添加名为OnVerbRunTest
和OnVerbStopTest
的事件处理程序。Private Sub OnVerbRunTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Start() End Sub Private Sub OnVerbStopTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Stop() End Sub
private void OnVerbRunTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Start(); } private void OnVerbStopTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Stop(); }
将这些事件处理程序连接至其对应的设计器谓词。
MarqueeControlRootDesigner
从其基类继承 DesignerVerbCollection。将在 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 类型编辑器
在“代码编辑器”中打开
MarqueeBorder
源文件。在
MarqueeBorder
类的定义中声明一个名为LightShapeEditor
、从 UITypeEditor 派生的类。' This class demonstrates the use of a custom UITypeEditor. ' It allows the MarqueeBorder control's LightShape property ' to be changed at design time using a customized UI element ' that is invoked by the Properties window. The UI is provided ' by the LightShapeSelectionControl class. Friend Class LightShapeEditor Inherits UITypeEditor
// This class demonstrates the use of a custom UITypeEditor. // It allows the MarqueeBorder control's LightShape property // to be changed at design time using a customized UI element // that is invoked by the Properties window. The UI is provided // by the LightShapeSelectionControl class. internal class LightShapeEditor : UITypeEditor {
声明一个名为
editorService
的 IWindowsFormsEditorService 实例变量。Private editorService As IWindowsFormsEditorService = Nothing
private IWindowsFormsEditorService editorService = null;
重写 GetEditStyle 方法。此实现返回 DropDown,它指示设计环境如何显示
LightShapeEditor
。Public Overrides Function GetEditStyle( _ ByVal context As System.ComponentModel.ITypeDescriptorContext) _ As UITypeEditorEditStyle Return UITypeEditorEditStyle.DropDown End Function
public override UITypeEditorEditStyle GetEditStyle( System.ComponentModel.ITypeDescriptorContext context) { return UITypeEditorEditStyle.DropDown; }
重写 EditValue 方法。此实现在设计环境中查询一个 IWindowsFormsEditorService 对象。如果成功,它将创建一个
LightShapeSelectionControl
。将调用 DropDownControl 方法以启动LightShapeEditor
。此调用的返回值将被返回到设计环境。Public Overrides Function EditValue( _ ByVal context As ITypeDescriptorContext, _ ByVal provider As IServiceProvider, _ ByVal value As Object) As Object If (provider IsNot Nothing) Then editorService = _ CType(provider.GetService(GetType(IWindowsFormsEditorService)), _ IWindowsFormsEditorService) End If If (editorService IsNot Nothing) Then Dim selectionControl As _ New LightShapeSelectionControl( _ CType(value, MarqueeLightShape), _ editorService) editorService.DropDownControl(selectionControl) value = selectionControl.LightShape End If Return value End Function
public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { if (provider != null) { editorService = provider.GetService( typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; } if (editorService != null) { LightShapeSelectionControl selectionControl = new LightShapeSelectionControl( (MarqueeLightShape)value, editorService); editorService.DropDownControl(selectionControl); value = selectionControl.LightShape; } return value; }
创建自定义 UITypeEditor 的视图控件
MarqueeLightShape
属性支持两种类型的灯光形状:Square
和Circle
。将专为以图形方式在“属性”窗口中显示这些值创建一个自定义控件。UITypeEditor 将使用此自定义控件来与“属性”窗口交互。
创建自定义 UI 类型编辑器的视图控件
将新的 UserControl 项添加到 MarqueeControlLibrary 项目。将新源文件的基名称命名为“LightShapeSelectionControl”。
从“工具箱”中将两个 Panel 控件拖到
LightShapeSelectionControl
上。将它们命名为squarePanel
和circlePanel
,并使它们并排排列。将这两个 Panel 控件的 Size 属性均设置为 (60, 60)。将squarePanel
控件的 Location 属性设置为 (8, 10)。将circlePanel
控件的 Location 属性设置为 (80, 10)。最后,将LightShapeSelectionControl
的 Size 属性设置为 (150, 80)。在“代码编辑器”中打开
LightShapeSelectionControl
源文件。在文件顶部导入 System.Windows.Forms.Design 命名空间:
Imports System.Windows.Forms.Design
using System.Windows.Forms.Design;
Private editorService As IWindowsFormsEditorService
private IWindowsFormsEditorService editorService;
在设计器中测试自定义控件
此时,您就可以生成 MarqueeControlLibrary 项目了。通过创建一个从 MarqueeControl
类继承的控件,并在窗体中使用该控件,来测试您实现的自定义控件。
创建自定义 MarqueeControl 实现
在 Windows 窗体设计器中打开
DemoMarqueeControl
。这将创建一个DemoMarqueeControl
类型的实例,并在MarqueeControlRootDesigner
类型的一个实例中显示它。在“工具箱”中打开“MarqueeControlLibrary 组件”选项卡。将显示
MarqueeBorder
和MarqueeText
控件以供选择。将
MarqueeBorder
控件的一个实例拖到DemoMarqueeControl
设计图面上。将此MarqueeBorder
控件停靠到父控件上。将
MarqueeText
控件的一个实例拖到DemoMarqueeControl
设计图面上。生成解决方案。
右击
DemoMarqueeControl
,然后从快捷菜单中选择“运行测试”选项启动动画。单击“停止测试”以停止动画。在“设计”视图中打开“Form1”。
将两个 Button 控件放置至窗体上。将其分别命名为
startButton
和stopButton
,并将 Text 属性值分别更改为“启动”和“停止”****。分别为这两个 Button 控件实现 Click 事件处理程序。
在“工具箱”中打开“MarqueeControlTest 组件”选项卡。将显示
DemoMarqueeControl
以供选择。将
DemoMarqueeControl
的一个实例拖到“Form1”设计图面上。在 Click 事件处理程序中对
DemoMarqueeControl
调用Start
和Stop
方法。
Private Sub startButton_Click(sender As Object, e As System.EventArgs)
Me.demoMarqueeControl1.Start()
End Sub 'startButton_Click
Private Sub stopButton_Click(sender As Object, e As System.EventArgs)
Me.demoMarqueeControl1.Stop()
End Sub 'stopButton_Click
private void startButton_Click(object sender, System.EventArgs e)
{
this.demoMarqueeControl1.Start();
}
private void stopButton_Click(object sender, System.EventArgs e)
{
this.demoMarqueeControl1.Stop();
}
后续步骤
MarqueeControlLibrary 演示了自定义控件及其关联设计器的一个简单实现。您可以采用以下几种方式来使此示例更复杂些:
在设计器中更改
DemoMarqueeControl
的属性值。添加更多MarqueBorder
控件,并将其停靠在它们的父实例中以产生嵌套效果。尝试对UpdatePeriod
和灯光相关属性进行不同的设置。创作自己的
IMarqueeWidget
的实现。例如,可以创建一个闪烁的“霓虹标志”或带有多个图像的动画标志。进一步自定义设计时体验。可以尝试在 Enabled 和 Visible 之外隐藏更多属性,而且可以添加新的属性。添加新的设计器谓词以简化常见任务,例如停靠子控件。
为
MarqueeControl
添加许可控制。有关更多信息,请参见 如何:授予组件和控件许可权限。控制如何序列化控件以及如何为控件生成代码。有关更多信息,请参见 动态源代码生成和编译。
请参见
任务
参考
UserControl
ParentControlDesigner
DocumentDesigner
IRootDesigner
DesignerVerb
UITypeEditor
BackgroundWorker
其他资源
扩展设计时支持
自定义设计器
.NET Shape Library: A Sample Designer(.NET 形状库:示例设计器)