演练:创建利用设计时功能的控件
可以通过创作关联自定义设计器来增强自定义控件的设计时体验。
注意
本文专为 .NET Framework 而撰写。 如果使用 .NET 6 或更高版本,请谨慎使用此内容。 设计器系统已更改Windows 窗体,因此请务必阅读自 .NET Framework 以来的设计器更改一文。
本文演示如何为自定义控件创建自定义设计器。 你会实现 MarqueeControl
类型和名为 MarqueeControlRootDesigner
的关联设计器类。
MarqueeControl
类型实现类似于剧院字幕的显示,带有动画灯和闪烁文本。
此控件的设计器与设计环境交互,以提供自定义设计时体验。 使用自定义设计器可以组装自定义 MarqueeControl
实现,带有多种组合的动画灯和闪烁文本。 可以与任何其他 Windows 窗体控件一样,组装的控件用于窗体。
完成本演练后,自定义控件将如下所示:
有关完整代码列表,请参阅如何:创建利用设计时功能的 Windows 窗体控件。
先决条件
若要完成本演练,需要具有 Visual Studio。
创建项目
第一步是创建应用程序项目。 将使用此项目生成承载自定义控件的应用程序。
在 Visual Studio 中创建新的 Windows 窗体应用程序项目,将其命名为 MarqueeControlTest。
创建控件库项目
将 Windows 窗体控件库项目添加到解决方案。 将项目命名为 MarqueeControlLibrary。
使用“解决方案资源管理器”,根据所选语言删除名为“UserControl1.cs”或“UserControl1.vb”的源文件,从而删除项目的默认控件。
向
MarqueeControlLibrary
项目添加一个新 UserControl 项。 为新源文件提供基名称 MarqueeControl。使用“解决方案资源管理器”,在
MarqueeControlLibrary
项目中创建新文件夹。右键单击“Design”文件夹并添加一个新类。 将其命名为 MarqueeControlRootDesigner。
需要使用 System.Design 程序集中的类型,因此将此引用添加
MarqueeControlLibrary
项目。
引用自定义控件项目
你会使用 MarqueeControlTest
项目测试自定义控件。 添加对 MarqueeControlLibrary
程序集的项目引用后,测试项目会感知到自定义控件。
在 MarqueeControlTest
项目中,添加对 MarqueeControlLibrary
程序集的项目引用。 请务必在“添加引用”对话框中使用“项目”选项卡,而不是直接引用 MarqueeControlLibrary
程序集。
定义自定义控件及其自定义设计器
自定义控件会从 UserControl 类派生。 这使控件可以包含其他控件,并且可为控件提供了大量默认功能。
自定义控件会具有关联自定义设计器。 这使你可以创造专为自定义控件定制的独特设计体验。
使用 DesignerAttribute 类将控件与其设计器关联。 因为你在开发自定义控件的整个设计时行为,所以自定义设计器会实现 IRootDesigner 接口。
定义自定义控件及其自定义设计器
在“代码编辑器”中打开
MarqueeControl
源文件。 在该文件顶部导入以下命名空间:using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Windows.Forms; using System.Windows.Forms.Design;
Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Drawing Imports System.Windows.Forms Imports System.Windows.Forms.Design
将 DesignerAttribute 添加到
MarqueeControl
类声明。 这会将自定义控件与其设计器关联。[Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )] public class MarqueeControl : UserControl {
<Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _ GetType(IRootDesigner))> _ Public Class MarqueeControl Inherits UserControl
在“代码编辑器”中打开
MarqueeControlRootDesigner
源文件。 在该文件顶部导入以下命名空间:using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing.Design; using System.Windows.Forms; using System.Windows.Forms.Design;
Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing.Design Imports System.Windows.Forms Imports System.Windows.Forms.Design
更改
MarqueeControlRootDesigner
的声明以从 DocumentDesigner 类继承。 应用 ToolboxItemFilterAttribute 以指定设计器与工具箱的交互。注意
MarqueeControlRootDesigner
类的定义已包含在名为 MarqueeControlLibrary.Design 的命名空间中。 此声明将设计器置于为与设计相关的类型保留的特殊命名空间中。namespace MarqueeControlLibrary.Design { [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] public class MarqueeControlRootDesigner : DocumentDesigner {
Namespace MarqueeControlLibrary.Design <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ Public Class MarqueeControlRootDesigner Inherits DocumentDesigner
定义
MarqueeControlRootDesigner
类的构造函数。 在构造函数主体中插入 WriteLine 语句。 这对调试十分有用。public MarqueeControlRootDesigner() { Trace.WriteLine("MarqueeControlRootDesigner ctor"); }
Public Sub New() Trace.WriteLine("MarqueeControlRootDesigner ctor") End Sub
创建自定义控件的实例
向
MarqueeControlTest
项目添加一个新 UserControl 项。 为新源文件提供基名称 DemoMarqueeControl。在“代码编辑器”中打开
DemoMarqueeControl
文件。 在该文件顶部导入MarqueeControlLibrary
命名空间:Imports MarqueeControlLibrary
using MarqueeControlLibrary;
更改
DemoMarqueeControl
的声明以从MarqueeControl
类继承。生成项目。
在 Windows 窗体设计器中打开 Form1。
在“工具箱”中找到“MarqueeControlTest 组件”选项卡,然后打开它。 将
DemoMarqueeControl
从“工具箱”拖到窗体上。生成项目。
设置项目以便进行设计时调试
开发自定义设计时体验时,需要调试控件和组件。 可以通过一种简单方法设置项目,以便可以在设计时进行调试。 有关详细信息,请参阅演练:设计时调试自定义 Windows 窗体控件。
右键单击
MarqueeControlLibrary
项目,然后选择“属性”。在“MarqueeControlLibrary 属性页”对话框中,选择“调试”页面。
在“启动操作”部分中,选择“启动外部程序”。 你会调试 Visual Studio 的单独实例,因此请单击省略号 () 按钮以浏览 Visual Studio IDE。 可执行文件的名称为 devenv.exe,如果安装到默认位置,则其路径为 %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<edition>\Common7\IDE\devenv.exe。
选择“确定”关闭对话框 。
右键单击 MarqueeControlLibrary 项目,然后选择“设为启动项目”以启用此调试配置。
Checkpoint
你现在已准备好调试自定义控件的设计时行为。 确定调试环境设置正确后,便会测试自定义控件与自定义设计器之间的关联。
测试调试环境和设计器关联
在“代码编辑器”中打开 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 void Start() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so // find each IMarqueeWidget child and call its // StartMarquee method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } } public void Stop() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } }
Public Sub Start() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so ' find each IMarqueeWidget child and call its ' StartMarquee method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl End Sub Public Sub [Stop]() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl End Sub
重写 OnLayout 方法。
protected override void OnLayout(LayoutEventArgs levent) { base.OnLayout (levent); // Repaint all IMarqueeWidget children if the layout // has changed. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { Control control = cntrl as Control; control.PerformLayout(); } } }
Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs) MyBase.OnLayout(levent) ' Repaint all IMarqueeWidget children if the layout ' has changed. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) cntrl.PerformLayout() End If Next cntrl End Sub
为自定义控件创建子控件
MarqueeControl
会承载两种类型的子控件:MarqueeBorder
控件和 MarqueeText
控件。
MarqueeBorder
:此控件围绕其边缘绘制“灯”的边框。 灯按顺序闪烁,因此它们似乎在围绕边框移动。 灯闪烁的速度通过名为UpdatePeriod
的属性进行控制。 其他几个自定义属性可确定控件外观的其他方面。 两个方法(名为StartMarquee
和StopMarquee
)会控制动画开始和停止的时间。MarqueeText
:此控件绘制闪烁字符串。 与MarqueeBorder
控件一样,文本闪烁的速度通过UpdatePeriod
属性进行控制。MarqueeText
控件还具有StartMarquee
和StopMarquee
方法(与MarqueeBorder
控件相同)。
在设计时,MarqueeControlRootDesigner
允许将这两种控件类型采用任意组合添加到 MarqueeControl
。
两个控件的共同功能融入到名为 IMarqueeWidget
的接口中。 这使 MarqueeControl
可以发现任何与字幕相关的子控件,并对它们进行特殊处理。
若要实现定期动画功能,会使用 System.ComponentModel 命名空间中的 BackgroundWorker 对象。 可以使用 Timer 对象,但当存在许多 IMarqueeWidget
对象时,单个 UI 线程可能无法跟上动画。
为自定义控件创建子控件
向
MarqueeControlLibrary
项目添加一个新类项。 为新源文件提供基名称“IMarqueeWidget”。在“代码编辑器”中打开
IMarqueeWidget
源文件,并将声明从class
更改为interface
:// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget {
' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget
将以下代码添加到
IMarqueeWidget
接口,以公开两个方法和一个操作字幕动画的属性:// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget { // This method starts the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StartMarquee on all // its IMarqueeWidget child controls. void StartMarquee(); // This method stops the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StopMarquee on all // its IMarqueeWidget child controls. void StopMarquee(); // This method specifies the refresh rate for the animation, // in milliseconds. int UpdatePeriod { get; set; } }
' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget ' This method starts the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StartMarquee on all ' its IMarqueeWidget child controls. Sub StartMarquee() ' This method stops the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StopMarquee on all ' its IMarqueeWidget child controls. Sub StopMarquee() ' This method specifies the refresh rate for the animation, ' in milliseconds. Property UpdatePeriod() As Integer End Interface
向
MarqueeControlLibrary
项目添加一个新“自定义控件”项。 为新源文件提供基名称“MarqueeText”。将 BackgroundWorker 组件从“工具箱”拖动到
MarqueeText
控件上。 此组件允许MarqueeText
控件异步更新自身。在“属性”窗口中,将 BackgroundWorker 组件的
WorkerReportsProgress
和 WorkerSupportsCancellation 属性设置为 true。 这些设置允许 BackgroundWorker 组件定期引发 ProgressChanged 事件并取消异步更新。有关详细信息,请参阅 BackgroundWorker 组件。
在“代码编辑器”中打开
MarqueeText
源文件。 在该文件顶部导入以下命名空间:using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
更改
MarqueeText
的声明以从 Label 继承并实现IMarqueeWidget
接口:[ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] public partial class MarqueeText : Label, IMarqueeWidget {
<ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeText Inherits Label Implements IMarqueeWidget
声明与公开属性对应的实例变量,并在构造函数中初始化它们。
isLit
字段确定文本是否采用LightColor
属性提供的颜色进行绘制。// When isLit is true, the text is painted in the light color; // When isLit is false, the text is painted in the dark color. // This value changes whenever the BackgroundWorker component // raises the ProgressChanged event. private bool isLit = true; // These fields back the public properties. private int updatePeriodValue = 50; private Color lightColorValue; private Color darkColorValue; // These brushes are used to paint the light and dark // colors of the text. private Brush lightBrush; private Brush darkBrush; // This component updates the control asynchronously. private BackgroundWorker backgroundWorker1; public MarqueeText() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); }
' When isLit is true, the text is painted in the light color; ' When isLit is false, the text is painted in the dark color. ' This value changes whenever the BackgroundWorker component ' raises the ProgressChanged event. Private isLit As Boolean = True ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightColorValue As Color Private darkColorValue As Color ' These brushes are used to paint the light and dark ' colors of the text. Private lightBrush As Brush Private darkBrush As Brush ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) End Sub
实现
IMarqueeWidget
接口。StartMarquee
和StopMarquee
方法调用 BackgroundWorker 组件的 RunWorkerAsync 和 CancelAsync 方法以启动和停止动画。Category 和 Browsable 特性会应用于
UpdatePeriod
属性,使它出现在属性窗口名为“Marquee”的自定义部分中。public virtual void StartMarquee() { // Start the updating thread and pass it the UpdatePeriod. this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod); } public virtual void StopMarquee() { // Stop the updating thread. this.backgroundWorker1.CancelAsync(); } [Category("Marquee")] [Browsable(true)] public int UpdatePeriod { get { return this.updatePeriodValue; } set { if (value > 0) { this.updatePeriodValue = value; } else { throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0"); } } }
Public Overridable Sub StartMarquee() _ Implements IMarqueeWidget.StartMarquee ' Start the updating thread and pass it the UpdatePeriod. Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod) End Sub Public Overridable Sub StopMarquee() _ Implements IMarqueeWidget.StopMarquee ' Stop the updating thread. Me.backgroundWorker1.CancelAsync() End Sub <Category("Marquee"), Browsable(True)> _ Public Property UpdatePeriod() As Integer _ Implements IMarqueeWidget.UpdatePeriod Get Return Me.updatePeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.updatePeriodValue = Value Else Throw New ArgumentOutOfRangeException("UpdatePeriod", "must be > 0") End If End Set End Property
实现属性访问器。 你会向客户端公开两个属性:
LightColor
和DarkColor
。 Category 和 Browsable 特性会应用于这些属性,使它们出现在属性窗口名为“Marquee”的自定义部分中。[Category("Marquee")] [Browsable(true)] public Color LightColor { get { return this.lightColorValue; } set { // The LightColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.lightColorValue.ToArgb() != value.ToArgb()) { this.lightColorValue = value; this.lightBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public Color DarkColor { get { return this.darkColorValue; } set { // The DarkColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.darkColorValue.ToArgb() != value.ToArgb()) { this.darkColorValue = value; this.darkBrush = new SolidBrush(value); } } }
<Category("Marquee"), Browsable(True)> _ Public Property LightColor() As Color Get Return Me.lightColorValue End Get Set(ByVal Value As Color) ' The LightColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then Me.lightColorValue = Value Me.lightBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property DarkColor() As Color Get Return Me.darkColorValue End Get Set(ByVal Value As Color) ' The DarkColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then Me.darkColorValue = Value Me.darkBrush = New SolidBrush(Value) End If End Set End Property
为 BackgroundWorker 组件的 DoWork 和 ProgressChanged 事件实现处理程序。
DoWork 事件处理程序会按照
UpdatePeriod
指定的毫秒数进行休眠,然后引发 ProgressChanged 事件,直到代码通过调用 CancelAsync 来停止动画。ProgressChanged 事件处理程序会在浅色与深色状态之间切换文本,以呈现闪烁的外观。
// This method is called in the worker thread's context, // so it must not make any calls into the MarqueeText control. // Instead, it communicates to the control using the // ProgressChanged event. // // The only work done in this event handler is // to sleep for the number of milliseconds specified // by UpdatePeriod, then raise the ProgressChanged event. private void backgroundWorker1_DoWork( object sender, System.ComponentModel.DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; // This event handler will run until the client cancels // the background task by calling CancelAsync. while (!worker.CancellationPending) { // The Argument property of the DoWorkEventArgs // object holds the value of UpdatePeriod, which // was passed as the argument to the RunWorkerAsync // method. Thread.Sleep((int)e.Argument); // The DoWork eventhandler does not actually report // progress; the ReportProgress event is used to // periodically alert the control to update its state. worker.ReportProgress(0); } } // The ProgressChanged event is raised by the DoWork method. // This event handler does work that is internal to the // control. In this case, the text is toggled between its // light and dark state, and the control is told to // repaint itself. private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e) { this.isLit = !this.isLit; this.Refresh(); }
' This method is called in the worker thread's context, ' so it must not make any calls into the MarqueeText control. ' Instead, it communicates to the control using the ' ProgressChanged event. ' ' The only work done in this event handler is ' to sleep for the number of milliseconds specified ' by UpdatePeriod, then raise the ProgressChanged event. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles backgroundWorker1.DoWork Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) ' This event handler will run until the client cancels ' the background task by calling CancelAsync. While Not worker.CancellationPending ' The Argument property of the DoWorkEventArgs ' object holds the value of UpdatePeriod, which ' was passed as the argument to the RunWorkerAsync ' method. Thread.Sleep(Fix(e.Argument)) ' The DoWork eventhandler does not actually report ' progress; the ReportProgress event is used to ' periodically alert the control to update its state. worker.ReportProgress(0) End While End Sub ' The ProgressChanged event is raised by the DoWork method. ' This event handler does work that is internal to the ' control. In this case, the text is toggled between its ' light and dark state, and the control is told to ' repaint itself. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.isLit = Not Me.isLit Me.Refresh() End Sub
替代 OnPaint 方法以启用动画。
protected override void OnPaint(PaintEventArgs e) { // The text is painted in the light or dark color, // depending on the current value of isLit. this.ForeColor = this.isLit ? this.lightColorValue : this.darkColorValue; base.OnPaint(e); }
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) ' The text is painted in the light or dark color, ' depending on the current value of isLit. Me.ForeColor = IIf(Me.isLit, Me.lightColorValue, Me.darkColorValue) MyBase.OnPaint(e) End Sub
按 F6 生成解决方案。
创建 MarqueeBorder 子控件
MarqueeBorder
控件比 MarqueeText
控件稍微复杂一些。 它具有更多属性,并且 OnPaint 方法中的动画更加复杂。 原则上,它与 MarqueeText
控件非常相似。
由于 MarqueeBorder
控件可以具有子控件,因此需要注意 Layout 事件。
创建 MarqueeBorder 控件
向
MarqueeControlLibrary
项目添加一个新“自定义控件”项。 为新源文件提供基名称“MarqueeBorder”。将 BackgroundWorker 组件从“工具箱”拖动到
MarqueeBorder
控件上。 此组件允许MarqueeBorder
控件异步更新自身。在“属性”窗口中,将 BackgroundWorker 组件的
WorkerReportsProgress
和 WorkerSupportsCancellation 属性设置为 true。 这些设置允许 BackgroundWorker 组件定期引发 ProgressChanged 事件并取消异步更新。 有关详细信息,请参阅 BackgroundWorker 组件。在“属性”窗口中选择“事件”按钮。 为 DoWork 和 ProgressChanged 事件附加处理程序。
在“代码编辑器”中打开
MarqueeBorder
源文件。 在该文件顶部导入以下命名空间:using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Drawing.Design; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Drawing.Design Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
更改
MarqueeBorder
的声明以从 Panel 继承并实现IMarqueeWidget
接口。[Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] public partial class MarqueeBorder : Panel, IMarqueeWidget {
<Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeBorder Inherits Panel Implements IMarqueeWidget
声明用于管理
MarqueeBorder
控件状态的两个枚举:MarqueeSpinDirection
用于确定灯围绕边框“旋转”的方向;以及MarqueeLightShape
,用于确定灯的形状(方形或圆形)。 将这些声明置于MarqueeBorder
类声明之前。// This defines the possible values for the MarqueeBorder // control's SpinDirection property. public enum MarqueeSpinDirection { CW, CCW } // This defines the possible values for the MarqueeBorder // control's LightShape property. public enum MarqueeLightShape { Square, Circle }
' This defines the possible values for the MarqueeBorder ' control's SpinDirection property. Public Enum MarqueeSpinDirection CW CCW End Enum ' This defines the possible values for the MarqueeBorder ' control's LightShape property. Public Enum MarqueeLightShape Square Circle End Enum
声明与公开属性对应的实例变量,并在构造函数中初始化它们。
public static int MaxLightSize = 10; // These fields back the public properties. private int updatePeriodValue = 50; private int lightSizeValue = 5; private int lightPeriodValue = 3; private int lightSpacingValue = 1; private Color lightColorValue; private Color darkColorValue; private MarqueeSpinDirection spinDirectionValue = MarqueeSpinDirection.CW; private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square; // These brushes are used to paint the light and dark // colors of the marquee lights. private Brush lightBrush; private Brush darkBrush; // This field tracks the progress of the "first" light as it // "travels" around the marquee border. private int currentOffset = 0; // This component updates the control asynchronously. private System.ComponentModel.BackgroundWorker backgroundWorker1; public MarqueeBorder() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); // The MarqueeBorder control manages its own padding, // because it requires that any contained controls do // not overlap any of the marquee lights. int pad = 2 * (this.lightSizeValue + this.lightSpacingValue); this.Padding = new Padding(pad, pad, pad, pad); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); }
Public Shared MaxLightSize As Integer = 10 ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightSizeValue As Integer = 5 Private lightPeriodValue As Integer = 3 Private lightSpacingValue As Integer = 1 Private lightColorValue As Color Private darkColorValue As Color Private spinDirectionValue As MarqueeSpinDirection = MarqueeSpinDirection.CW Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square ' These brushes are used to paint the light and dark ' colors of the marquee lights. Private lightBrush As Brush Private darkBrush As Brush ' This field tracks the progress of the "first" light as it ' "travels" around the marquee border. Private currentOffset As Integer = 0 ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As System.ComponentModel.BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) ' The MarqueeBorder control manages its own padding, ' because it requires that any contained controls do ' not overlap any of the marquee lights. Dim pad As Integer = 2 * (Me.lightSizeValue + Me.lightSpacingValue) Me.Padding = New Padding(pad, pad, pad, pad) SetStyle(ControlStyles.OptimizedDoubleBuffer, True) End Sub
实现
IMarqueeWidget
接口。StartMarquee
和StopMarquee
方法调用 BackgroundWorker 组件的 RunWorkerAsync 和 CancelAsync 方法以启动和停止动画。由于
MarqueeBorder
控件可以包含子控件,因此StartMarquee
方法会枚举所有子控件并对实现IMarqueeWidget
的子控件调用StartMarquee
。StopMarquee
方法具有类似的实现。public virtual void StartMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StartMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } // Start the updating thread and pass it the UpdatePeriod. this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod); } public virtual void StopMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } // Stop the updating thread. this.backgroundWorker1.CancelAsync(); } [Category("Marquee")] [Browsable(true)] public virtual int UpdatePeriod { get { return this.updatePeriodValue; } set { if (value > 0) { this.updatePeriodValue = value; } else { throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0"); } } }
Public Overridable Sub StartMarquee() _ Implements IMarqueeWidget.StartMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StartMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl ' Start the updating thread and pass it the UpdatePeriod. Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod) End Sub Public Overridable Sub StopMarquee() _ Implements IMarqueeWidget.StopMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl ' Stop the updating thread. Me.backgroundWorker1.CancelAsync() End Sub <Category("Marquee"), Browsable(True)> _ Public Overridable Property UpdatePeriod() As Integer _ Implements IMarqueeWidget.UpdatePeriod Get Return Me.updatePeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.updatePeriodValue = Value Else Throw New ArgumentOutOfRangeException("UpdatePeriod", _ "must be > 0") End If End Set End Property
实现属性访问器。
MarqueeBorder
控件具有多个用于控制其外观的属性。[Category("Marquee")] [Browsable(true)] public int LightSize { get { return this.lightSizeValue; } set { if (value > 0 && value <= MaxLightSize) { this.lightSizeValue = value; this.DockPadding.All = 2 * value; } else { throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize"); } } } [Category("Marquee")] [Browsable(true)] public int LightPeriod { get { return this.lightPeriodValue; } set { if (value > 0) { this.lightPeriodValue = value; } else { throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 "); } } } [Category("Marquee")] [Browsable(true)] public Color LightColor { get { return this.lightColorValue; } set { // The LightColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.lightColorValue.ToArgb() != value.ToArgb()) { this.lightColorValue = value; this.lightBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public Color DarkColor { get { return this.darkColorValue; } set { // The DarkColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.darkColorValue.ToArgb() != value.ToArgb()) { this.darkColorValue = value; this.darkBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public int LightSpacing { get { return this.lightSpacingValue; } set { if (value >= 0) { this.lightSpacingValue = value; } else { throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0"); } } } [Category("Marquee")] [Browsable(true)] [EditorAttribute(typeof(LightShapeEditor), typeof(System.Drawing.Design.UITypeEditor))] public MarqueeLightShape LightShape { get { return this.lightShapeValue; } set { this.lightShapeValue = value; } } [Category("Marquee")] [Browsable(true)] public MarqueeSpinDirection SpinDirection { get { return this.spinDirectionValue; } set { this.spinDirectionValue = value; } }
<Category("Marquee"), Browsable(True)> _ Public Property LightSize() As Integer Get Return Me.lightSizeValue End Get Set(ByVal Value As Integer) If Value > 0 AndAlso Value <= MaxLightSize Then Me.lightSizeValue = Value Me.DockPadding.All = 2 * Value Else Throw New ArgumentOutOfRangeException("LightSize", _ "must be > 0 and < MaxLightSize") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightPeriod() As Integer Get Return Me.lightPeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.lightPeriodValue = Value Else Throw New ArgumentOutOfRangeException("LightPeriod", _ "must be > 0 ") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightColor() As Color Get Return Me.lightColorValue End Get Set(ByVal Value As Color) ' The LightColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then Me.lightColorValue = Value Me.lightBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property DarkColor() As Color Get Return Me.darkColorValue End Get Set(ByVal Value As Color) ' The DarkColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then Me.darkColorValue = Value Me.darkBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightSpacing() As Integer Get Return Me.lightSpacingValue End Get Set(ByVal Value As Integer) If Value >= 0 Then Me.lightSpacingValue = Value Else Throw New ArgumentOutOfRangeException("LightSpacing", _ "must be >= 0") End If End Set End Property <Category("Marquee"), Browsable(True), _ EditorAttribute(GetType(LightShapeEditor), _ GetType(System.Drawing.Design.UITypeEditor))> _ Public Property LightShape() As MarqueeLightShape Get Return Me.lightShapeValue End Get Set(ByVal Value As MarqueeLightShape) Me.lightShapeValue = Value End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property SpinDirection() As MarqueeSpinDirection Get Return Me.spinDirectionValue End Get Set(ByVal Value As MarqueeSpinDirection) Me.spinDirectionValue = Value End Set End Property
为 BackgroundWorker 组件的 DoWork 和 ProgressChanged 事件实现处理程序。
DoWork 事件处理程序会按照
UpdatePeriod
指定的毫秒数进行休眠,然后引发 ProgressChanged 事件,直到代码通过调用 CancelAsync 来停止动画。ProgressChanged 事件处理程序会递增“基本”灯(可用于确定其他灯的浅色/深色状态)的位置,并调用 Refresh 方法来使控件重新绘制自身。
// This method is called in the worker thread's context, // so it must not make any calls into the MarqueeBorder // control. Instead, it communicates to the control using // the ProgressChanged event. // // The only work done in this event handler is // to sleep for the number of milliseconds specified // by UpdatePeriod, then raise the ProgressChanged event. private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; // This event handler will run until the client cancels // the background task by calling CancelAsync. while (!worker.CancellationPending) { // The Argument property of the DoWorkEventArgs // object holds the value of UpdatePeriod, which // was passed as the argument to the RunWorkerAsync // method. Thread.Sleep((int)e.Argument); // The DoWork eventhandler does not actually report // progress; the ReportProgress event is used to // periodically alert the control to update its state. worker.ReportProgress(0); } } // The ProgressChanged event is raised by the DoWork method. // This event handler does work that is internal to the // control. In this case, the currentOffset is incremented, // and the control is told to repaint itself. private void backgroundWorker1_ProgressChanged( object sender, System.ComponentModel.ProgressChangedEventArgs e) { this.currentOffset++; this.Refresh(); }
' This method is called in the worker thread's context, ' so it must not make any calls into the MarqueeBorder ' control. Instead, it communicates to the control using ' the ProgressChanged event. ' ' The only work done in this event handler is ' to sleep for the number of milliseconds specified ' by UpdatePeriod, then raise the ProgressChanged event. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles backgroundWorker1.DoWork Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) ' This event handler will run until the client cancels ' the background task by calling CancelAsync. While Not worker.CancellationPending ' The Argument property of the DoWorkEventArgs ' object holds the value of UpdatePeriod, which ' was passed as the argument to the RunWorkerAsync ' method. Thread.Sleep(Fix(e.Argument)) ' The DoWork eventhandler does not actually report ' progress; the ReportProgress event is used to ' periodically alert the control to update its state. worker.ReportProgress(0) End While End Sub ' The ProgressChanged event is raised by the DoWork method. ' This event handler does work that is internal to the ' control. In this case, the currentOffset is incremented, ' and the control is told to repaint itself. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.currentOffset += 1 Me.Refresh() End Sub
实现帮助程序方法
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 virtual bool IsLit(int lightIndex) { int directionFactor = (this.spinDirectionValue == MarqueeSpinDirection.CW ? -1 : 1); return ( (lightIndex + directionFactor * this.currentOffset) % this.lightPeriodValue == 0 ); } protected virtual void DrawLight( Graphics g, Brush brush, int xPos, int yPos) { switch (this.lightShapeValue) { case MarqueeLightShape.Square: { g.FillRectangle(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue); break; } case MarqueeLightShape.Circle: { g.FillEllipse(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue); break; } default: { Trace.Assert(false, "Unknown value for light shape."); break; } } }
' This method determines if the marquee light at lightIndex ' should be lit. The currentOffset field specifies where ' the "first" light is located, and the "position" of the ' light given by lightIndex is computed relative to this ' offset. If this position modulo lightPeriodValue is zero, ' the light is considered to be on, and it will be painted ' with the control's lightBrush. Protected Overridable Function IsLit(ByVal lightIndex As Integer) As Boolean Dim directionFactor As Integer = _ IIf(Me.spinDirectionValue = MarqueeSpinDirection.CW, -1, 1) Return (lightIndex + directionFactor * Me.currentOffset) Mod Me.lightPeriodValue = 0 End Function Protected Overridable Sub DrawLight( _ ByVal g As Graphics, _ ByVal brush As Brush, _ ByVal xPos As Integer, _ ByVal yPos As Integer) Select Case Me.lightShapeValue Case MarqueeLightShape.Square g.FillRectangle( _ brush, _ xPos, _ yPos, _ Me.lightSizeValue, _ Me.lightSizeValue) Exit Select Case MarqueeLightShape.Circle g.FillEllipse( _ brush, _ xPos, _ yPos, _ Me.lightSizeValue, _ Me.lightSizeValue) Exit Select Case Else Trace.Assert(False, "Unknown value for light shape.") Exit Select End Select End Sub
-
OnPaint 方法沿
MarqueeBorder
控件边缘绘制灯。由于 OnPaint 方法依赖于
MarqueeBorder
控件的尺寸,因此每当布局发生更改时,都需要调用它。 若要实现此目的,请替代 OnLayout 并调用 Refresh。protected override void OnLayout(LayoutEventArgs levent) { base.OnLayout(levent); // Repaint when the layout has changed. this.Refresh(); } // This method paints the lights around the border of the // control. It paints the top row first, followed by the // right side, the bottom row, and the left side. The color // of each light is determined by the IsLit method and // depends on the light's position relative to the value // of currentOffset. protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.Clear(this.BackColor); base.OnPaint(e); // If the control is large enough, draw some lights. if (this.Width > MaxLightSize && this.Height > MaxLightSize) { // The position of the next light will be incremented // by this value, which is equal to the sum of the // light size and the space between two lights. int increment = this.lightSizeValue + this.lightSpacingValue; // Compute the number of lights to be drawn along the // horizontal edges of the control. int horizontalLights = (this.Width - increment) / increment; // Compute the number of lights to be drawn along the // vertical edges of the control. int verticalLights = (this.Height - increment) / increment; // These local variables will be used to position and // paint each light. int xPos = 0; int yPos = 0; int lightCounter = 0; Brush brush; // Draw the top row of lights. for (int i = 0; i < horizontalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); xPos += increment; lightCounter++; } // Draw the lights flush with the right edge of the control. xPos = this.Width - this.lightSizeValue; // Draw the right column of lights. for (int i = 0; i < verticalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); yPos += increment; lightCounter++; } // Draw the lights flush with the bottom edge of the control. yPos = this.Height - this.lightSizeValue; // Draw the bottom row of lights. for (int i = 0; i < horizontalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); xPos -= increment; lightCounter++; } // Draw the lights flush with the left edge of the control. xPos = 0; // Draw the left column of lights. for (int i = 0; i < verticalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); yPos -= increment; lightCounter++; } } }
Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs) MyBase.OnLayout(levent) ' Repaint when the layout has changed. Me.Refresh() End Sub ' This method paints the lights around the border of the ' control. It paints the top row first, followed by the ' right side, the bottom row, and the left side. The color ' of each light is determined by the IsLit method and ' depends on the light's position relative to the value ' of currentOffset. Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim g As Graphics = e.Graphics g.Clear(Me.BackColor) MyBase.OnPaint(e) ' If the control is large enough, draw some lights. If Me.Width > MaxLightSize AndAlso Me.Height > MaxLightSize Then ' The position of the next light will be incremented ' by this value, which is equal to the sum of the ' light size and the space between two lights. Dim increment As Integer = _ Me.lightSizeValue + Me.lightSpacingValue ' Compute the number of lights to be drawn along the ' horizontal edges of the control. Dim horizontalLights As Integer = _ (Me.Width - increment) / increment ' Compute the number of lights to be drawn along the ' vertical edges of the control. Dim verticalLights As Integer = _ (Me.Height - increment) / increment ' These local variables will be used to position and ' paint each light. Dim xPos As Integer = 0 Dim yPos As Integer = 0 Dim lightCounter As Integer = 0 Dim brush As Brush ' Draw the top row of lights. Dim i As Integer For i = 0 To horizontalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) xPos += increment lightCounter += 1 Next i ' Draw the lights flush with the right edge of the control. xPos = Me.Width - Me.lightSizeValue ' Draw the right column of lights. 'Dim i As Integer For i = 0 To verticalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) yPos += increment lightCounter += 1 Next i ' Draw the lights flush with the bottom edge of the control. yPos = Me.Height - Me.lightSizeValue ' Draw the bottom row of lights. 'Dim i As Integer For i = 0 To horizontalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) xPos -= increment lightCounter += 1 Next i ' Draw the lights flush with the left edge of the control. xPos = 0 ' Draw the left column of lights. 'Dim i As Integer For i = 0 To verticalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) yPos -= increment lightCounter += 1 Next i End If End Sub
创建自定义设计器以隐藏和筛选属性
MarqueeControlRootDesigner
类提供根设计器的实现。 除了对 MarqueeControl
控件进行操作的此设计器之外,还需要一个专门与 MarqueeBorder
控件关联的自定义设计器。 此设计器提供适用于自定义根设计器上下文的自定义行为。
具体而言,MarqueeBorderDesigner
会“隐藏”并筛选控件 MarqueeBorder
上的某些属性,从而更改它们与设计环境的交互。
截获对组件属性访问器的调用称为“隐藏”。它使设计器可以跟踪用户设置的值,并选择性地将该值传递给进行设计的组件。
对于此示例,Visible 和 Enabled 属性会由 MarqueeBorderDesigner
隐藏,这可防止用户在设计期间使 MarqueeBorder
控件不可见或禁用。
设计器还可以添加和移除属性。 对于此示例,Padding 属性会在设计时移除,因为 MarqueeBorder
控件以编程方式基于 LightSize
属性指定的灯大小设置填充。
MarqueeBorderDesigner
的基类是 ComponentDesigner,它具有可以在设计时更改控件公开的特性、属性和事件的方法:
使用这些方法更改组件的公共接口时,请遵循以下规则:
仅在
PreFilter
方法中添加或移除项仅在
PostFilter
方法中的修改现有项始终先在
PreFilter
方法中调用基实现始终最后在
PostFilter
方法中调用基实现
遵循这些规则可确保设计时环境中的所有设计器都具有进行设计的所有组件的一致视图。
ComponentDesigner 类提供一个字典,用于管理隐藏属性的值,这可减少创建特定实例变量的需要。
创建自定义设计器以隐藏和筛选属性
右键单击“Design”文件夹并添加一个新类。 为源文件提供基名称“MarqueeBorderDesigner”。
在“代码编辑器”中打开 MarqueeBorderDesigner 源文件。 在该文件顶部导入以下命名空间:
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Windows.Forms; using System.Windows.Forms.Design;
Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Windows.Forms Imports System.Windows.Forms.Design
更改
MarqueeBorderDesigner
的声明以从 ParentControlDesigner 继承。由于
MarqueeBorder
控件可以包含子控件,因此MarqueeBorderDesigner
继承自 ParentControlDesigner,后者可处理父子交互。namespace MarqueeControlLibrary.Design { public class MarqueeBorderDesigner : ParentControlDesigner {
Namespace MarqueeControlLibrary.Design Public Class MarqueeBorderDesigner Inherits ParentControlDesigner
替代 PreFilterProperties 的基实现。
protected override void PreFilterProperties(IDictionary properties) { base.PreFilterProperties(properties); if (properties.Contains("Padding")) { properties.Remove("Padding"); } properties["Visible"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Visible"], new Attribute[0]); properties["Enabled"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Enabled"], new Attribute[0]); }
Protected Overrides Sub PreFilterProperties( _ ByVal properties As IDictionary) MyBase.PreFilterProperties(properties) If properties.Contains("Padding") Then properties.Remove("Padding") End If properties("Visible") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Visible"), PropertyDescriptor), _ New Attribute(-1) {}) properties("Enabled") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Enabled"), _ PropertyDescriptor), _ New Attribute(-1) {}) End Sub
实现 Enabled 和 Visible 属性。 这些实现会隐藏控件的属性。
public bool Visible { get { return (bool)ShadowProperties["Visible"]; } set { this.ShadowProperties["Visible"] = value; } } public bool Enabled { get { return (bool)ShadowProperties["Enabled"]; } set { this.ShadowProperties["Enabled"] = value; } }
Public Property Visible() As Boolean Get Return CBool(ShadowProperties("Visible")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Visible") = Value End Set End Property Public Property Enabled() As Boolean Get Return CBool(ShadowProperties("Enabled")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Enabled") = Value End Set End Property
处理组件更改
MarqueeControlRootDesigner
类为 MarqueeControl
实例提供自定义设计时体验。 大多数设计时功能继承自 DocumentDesigner 类。 代码会实现两个特定自定义:处理组件更改和添加设计器谓词。
当用户设计其 MarqueeControl
实例时,根设计器会跟踪对 MarqueeControl
及其子控件的更改。 设计时环境提供了一种方便服务 IComponentChangeService,用于跟踪对组件状态的更改。
可通过使用 GetService 方法查询环境来获取对此服务的引用。 如果查询成功,设计器可以附加 ComponentChanged 事件的处理程序,并执行在设计时维护一致状态所需的任何任务。
对于 MarqueeControlRootDesigner
类,你会对 MarqueeControl
包含的每个 IMarqueeWidget
对象调用 Refresh 方法。 这会使 IMarqueeWidget
对象在其父级的 Size 等属性更改时相应地重新绘制自身。
处理组件更改
在“代码编辑器”中打开
MarqueeControlRootDesigner
源文件,然后替代 Initialize 方法。 调用 Initialize 的基实现,并查询 IComponentChangeService。base.Initialize(component); IComponentChangeService cs = GetService(typeof(IComponentChangeService)) as IComponentChangeService; if (cs != null) { cs.ComponentChanged += new ComponentChangedEventHandler(OnComponentChanged); }
MyBase.Initialize(component) Dim cs As IComponentChangeService = _ CType(GetService(GetType(IComponentChangeService)), _ IComponentChangeService) If (cs IsNot Nothing) Then AddHandler cs.ComponentChanged, AddressOf OnComponentChanged End If
实现 OnComponentChanged 事件处理程序。 测试发送组件的类型,如果是
IMarqueeWidget
,则调用其 Refresh 方法。private void OnComponentChanged( object sender, ComponentChangedEventArgs e) { if (e.Component is IMarqueeWidget) { this.Control.Refresh(); } }
Private Sub OnComponentChanged( _ ByVal sender As Object, _ ByVal e As ComponentChangedEventArgs) If TypeOf e.Component Is IMarqueeWidget Then Me.Control.Refresh() End If End Sub
向自定义设计器添加设计器谓词
设计器谓词是与事件处理程序链接的菜单命令。 设计器谓词会在设计时添加到组件的快捷菜单中。 有关详细信息,请参阅 DesignerVerb。
你会向设计器添加两个设计器谓词:“Run Test”和“Stop Test”。 这些谓词使你可以在设计时查看 MarqueeControl
的运行时行为。 这些谓词会添加到 MarqueeControlRootDesigner
中。
调用“Run Test”时,谓词事件处理程序会对 MarqueeControl
调用 StartMarquee
方法。 调用“Stop Test”时,谓词事件处理程序会对 MarqueeControl
调用 StopMarquee
方法。 StartMarquee
和 StopMarquee
方法的实现会对实现 IMarqueeWidget
的包含控件调用这些方法,因此任何包含 IMarqueeWidget
的控件也会参与测试。
向自定义设计器添加设计器谓词
在
MarqueeControlRootDesigner
类中,添加名为OnVerbRunTest
和OnVerbStopTest
的事件处理程序。private void OnVerbRunTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Start(); } private void OnVerbStopTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Stop(); }
Private Sub OnVerbRunTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Start() End Sub Private Sub OnVerbStopTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Stop() End Sub
将这些事件处理程序连接到对应的设计器谓词。
MarqueeControlRootDesigner
从其基类继承 DesignerVerbCollection。 你会创建两个新 DesignerVerb 对象,并在 Initialize 方法中将它们添加到此集合中。this.Verbs.Add( new DesignerVerb("Run Test", new EventHandler(OnVerbRunTest)) ); this.Verbs.Add( new DesignerVerb("Stop Test", new EventHandler(OnVerbStopTest)) );
Me.Verbs.Add(New DesignerVerb("Run Test", _ New EventHandler(AddressOf OnVerbRunTest))) Me.Verbs.Add(New DesignerVerb("Stop Test", _ New EventHandler(AddressOf OnVerbStopTest)))
创建自定义 UITypeEditor
为用户创建自定义设计时体验时,通常需要创建与属性窗口的自定义交互。 可以通过创建 UITypeEditor 来实现此目的。
MarqueeBorder
控件在属性窗口中公开多个属性。 其中两个属性 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. internal class LightShapeEditor : UITypeEditor {
' This class demonstrates the use of a custom UITypeEditor. ' It allows the MarqueeBorder control's LightShape property ' to be changed at design time using a customized UI element ' that is invoked by the Properties window. The UI is provided ' by the LightShapeSelectionControl class. Friend Class LightShapeEditor Inherits UITypeEditor
声明名为
editorService
的 IWindowsFormsEditorService 实例变量。private IWindowsFormsEditorService editorService = null;
Private editorService As IWindowsFormsEditorService = Nothing
重写 GetEditStyle 方法。 此实现返回 DropDown,它会告知设计环境如何显示
LightShapeEditor
。public override UITypeEditorEditStyle GetEditStyle( System.ComponentModel.ITypeDescriptorContext context) { return UITypeEditorEditStyle.DropDown; }
Public Overrides Function GetEditStyle( _ ByVal context As System.ComponentModel.ITypeDescriptorContext) _ As UITypeEditorEditStyle Return UITypeEditorEditStyle.DropDown End Function
重写 EditValue 方法。 此实现在设计环境中查询 IWindowsFormsEditorService 对象。 如果成功,它会创建
LightShapeSelectionControl
。 调用 DropDownControl 方法以启动LightShapeEditor
。 此调用的返回值会返回到设计环境。public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { if (provider != null) { editorService = provider.GetService( typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; } if (editorService != null) { LightShapeSelectionControl selectionControl = new LightShapeSelectionControl( (MarqueeLightShape)value, editorService); editorService.DropDownControl(selectionControl); value = selectionControl.LightShape; } return value; }
Public Overrides Function EditValue( _ ByVal context As ITypeDescriptorContext, _ ByVal provider As IServiceProvider, _ ByVal value As Object) As Object If (provider IsNot Nothing) Then editorService = _ CType(provider.GetService(GetType(IWindowsFormsEditorService)), _ IWindowsFormsEditorService) End If If (editorService IsNot Nothing) Then Dim selectionControl As _ New LightShapeSelectionControl( _ CType(value, MarqueeLightShape), _ editorService) editorService.DropDownControl(selectionControl) value = selectionControl.LightShape End If Return value End Function
为自定义 UITypeEditor 创建视图控件
MarqueeLightShape
属性支持两种类型的灯形状:Square
和 Circle
。 你会创建一个仅用于在属性窗口中以图形方式显示这些值的自定义控件。 此自定义控件会由 UITypeEditor 用于与属性窗口交互。
为自定义 UI 类型编辑器创建视图控件
向
MarqueeControlLibrary
项目添加一个新 UserControl 项。 为新源文件提供基名称 LightShapeSelectionControl。从“工具箱”将两个 Panel 控件拖动到
LightShapeSelectionControl
上。 将它们分别命名为squarePanel
和circlePanel
。 并排排列它们。 将两个 Panel 控件的 Size 属性设置为 (60, 60)。 将 Location 控件的squarePanel
属性设置为 (8, 10)。 将 Location 控件的circlePanel
属性设置为 (80, 10)。 最后,将LightShapeSelectionControl
的 Size 属性设置为 (150, 80)。在“代码编辑器”中打开
LightShapeSelectionControl
源文件。 在该文件顶部导入 System.Windows.Forms.Design 命名空间:Imports System.Windows.Forms.Design
using System.Windows.Forms.Design;
为
squarePanel
和circlePanel
控件实现 Click 事件处理程序。 这些方法调用 CloseDropDown 以结束自定义 UITypeEditor 编辑会话。private void squarePanel_Click(object sender, EventArgs e) { this.lightShapeValue = MarqueeLightShape.Square; this.Invalidate( false ); this.editorService.CloseDropDown(); } private void circlePanel_Click(object sender, EventArgs e) { this.lightShapeValue = MarqueeLightShape.Circle; this.Invalidate( false ); this.editorService.CloseDropDown(); }
Private Sub squarePanel_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Me.lightShapeValue = MarqueeLightShape.Square Me.Invalidate(False) Me.editorService.CloseDropDown() End Sub Private Sub circlePanel_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Me.lightShapeValue = MarqueeLightShape.Circle Me.Invalidate(False) Me.editorService.CloseDropDown() End Sub
声明名为
editorService
的 IWindowsFormsEditorService 实例变量。Private editorService As IWindowsFormsEditorService
private IWindowsFormsEditorService editorService;
声明名为
lightShapeValue
的MarqueeLightShape
实例变量。private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
在
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 LightShapeSelectionControl( MarqueeLightShape lightShape, IWindowsFormsEditorService editorService ) { // This call is required by the designer. InitializeComponent(); // Cache the light shape value provided by the // design-time environment. this.lightShapeValue = lightShape; // Cache the reference to the editor service. this.editorService = editorService; // Handle the Click event for the two panels. this.squarePanel.Click += new EventHandler(squarePanel_Click); this.circlePanel.Click += new EventHandler(circlePanel_Click); }
' This constructor takes a MarqueeLightShape value from the ' design-time environment, which will be used to display ' the initial state. Public Sub New( _ ByVal lightShape As MarqueeLightShape, _ ByVal editorService As IWindowsFormsEditorService) ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Cache the light shape value provided by the ' design-time environment. Me.lightShapeValue = lightShape ' Cache the reference to the editor service. Me.editorService = editorService ' Handle the Click event for the two panels. AddHandler Me.squarePanel.Click, AddressOf squarePanel_Click AddHandler Me.circlePanel.Click, AddressOf circlePanel_Click End Sub
在 Dispose 方法中,拆离 Click 事件处理程序。
protected override void Dispose( bool disposing ) { if( disposing ) { // Be sure to unhook event handlers // to prevent "lapsed listener" leaks. this.squarePanel.Click -= new EventHandler(squarePanel_Click); this.circlePanel.Click -= new EventHandler(circlePanel_Click); if(components != null) { components.Dispose(); } } base.Dispose( disposing ); }
Protected Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then ' Be sure to unhook event handlers ' to prevent "lapsed listener" leaks. RemoveHandler Me.squarePanel.Click, AddressOf squarePanel_Click RemoveHandler Me.circlePanel.Click, AddressOf circlePanel_Click If (components IsNot Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub
在“解决方案资源管理器”中,单击“显示所有文件”按钮。 打开 LightShapeSelectionControl.Designer.cs 或 LightShapeSelectionControl.Designer.vb 文件,并移除 Dispose 方法的默认定义。
实现
LightShape
属性。// LightShape is the property for which this control provides // a custom user interface in the Properties window. public MarqueeLightShape LightShape { get { return this.lightShapeValue; } set { if( this.lightShapeValue != value ) { this.lightShapeValue = value; } } }
' LightShape is the property for which this control provides ' a custom user interface in the Properties window. Public Property LightShape() As MarqueeLightShape Get Return Me.lightShapeValue End Get Set(ByVal Value As MarqueeLightShape) If Me.lightShapeValue <> Value Then Me.lightShapeValue = Value End If End Set End Property
重写 OnPaint 方法。 此实现会绘制填充的方形和圆形。 它还会通过围绕一种形状或另一种形状绘制边框来突出显示所选值。
protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); using( Graphics gSquare = this.squarePanel.CreateGraphics(), gCircle = this.circlePanel.CreateGraphics() ) { // Draw a filled square in the client area of // the squarePanel control. gSquare.FillRectangle( Brushes.Red, 0, 0, this.squarePanel.Width, this.squarePanel.Height ); // If the Square option has been selected, draw a // border inside the squarePanel. if( this.lightShapeValue == MarqueeLightShape.Square ) { gSquare.DrawRectangle( Pens.Black, 0, 0, this.squarePanel.Width-1, this.squarePanel.Height-1); } // Draw a filled circle in the client area of // the circlePanel control. gCircle.Clear( this.circlePanel.BackColor ); gCircle.FillEllipse( Brushes.Blue, 0, 0, this.circlePanel.Width, this.circlePanel.Height ); // If the Circle option has been selected, draw a // border inside the circlePanel. if( this.lightShapeValue == MarqueeLightShape.Circle ) { gCircle.DrawRectangle( Pens.Black, 0, 0, this.circlePanel.Width-1, this.circlePanel.Height-1); } } }
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase.OnPaint(e) Dim gCircle As Graphics = Me.circlePanel.CreateGraphics() Try Dim gSquare As Graphics = Me.squarePanel.CreateGraphics() Try ' Draw a filled square in the client area of ' the squarePanel control. gSquare.FillRectangle( _ Brushes.Red, _ 0, _ 0, _ Me.squarePanel.Width, _ Me.squarePanel.Height) ' If the Square option has been selected, draw a ' border inside the squarePanel. If Me.lightShapeValue = MarqueeLightShape.Square Then gSquare.DrawRectangle( _ Pens.Black, _ 0, _ 0, _ Me.squarePanel.Width - 1, _ Me.squarePanel.Height - 1) End If ' Draw a filled circle in the client area of ' the circlePanel control. gCircle.Clear(Me.circlePanel.BackColor) gCircle.FillEllipse( _ Brushes.Blue, _ 0, _ 0, _ Me.circlePanel.Width, _ Me.circlePanel.Height) ' If the Circle option has been selected, draw a ' border inside the circlePanel. If Me.lightShapeValue = MarqueeLightShape.Circle Then gCircle.DrawRectangle( _ Pens.Black, _ 0, _ 0, _ Me.circlePanel.Width - 1, _ Me.circlePanel.Height - 1) End If Finally gSquare.Dispose() End Try Finally gCircle.Dispose() End Try End Sub
在设计器中测试自定义控件
此时,可以生成 MarqueeControlLibrary
项目。 通过创建从 MarqueeControl
类继承的控件并在窗体中使用它来测试实现。
创建自定义 MarqueeControl 实现
在 Windows 窗体设计器中打开
DemoMarqueeControl
。 这会创建DemoMarqueeControl
类型的实例,并在MarqueeControlRootDesigner
类型的实例中显示它。在“工具箱”中,打开“MarqueeControlLibrary 组件”选项卡。你会看到可用于选择的
MarqueeBorder
和MarqueeText
控件。将
MarqueeBorder
控件的实例拖动到DemoMarqueeControl
设计图面上。 将此MarqueeBorder
控件停靠到父控件。将
MarqueeText
控件的实例拖动到DemoMarqueeControl
设计图面上。生成解决方案。
右键单击
DemoMarqueeControl
,并从快捷菜单中选择“Run Test”选项以启动动画。 单击“Stop Test”以停止动画。在设计视图中打开“Form1”。
在窗体上放置两个 Button 控件。 将它们分别命名为
startButton
和stopButton
,并将 Text 属性值分别更改为“启动”和“停止”。在“工具箱”中,打开“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(); }
将
MarqueeControlTest
项目设置为启动项目并运行它。 你会看到窗体显示你的DemoMarqueeControl
。 选择“启动”按钮启动动画。 你应该会看到文本闪烁,并且灯围绕边框移动。
后续步骤
MarqueeControlLibrary
演示自定义控件和关联设计器的简单实现。 可以通过多种方式使此示例更加复杂:
在设计器中更改
DemoMarqueeControl
的属性值。 添加更多MarqueBorder
控件并将其停靠在其父实例中,以创建嵌套效果。 对UpdatePeriod
以及与灯相关的属性尝试不同的设置。创作自己的
IMarqueeWidget
实现。 例如,可以创建闪烁的“霓虹灯”或具有多个图像的动画符号。进一步自定义设计时体验。 可以尝试隐藏比 Enabled 和 Visible 更多的属性,并且可以添加新属性。 添加新设计器谓词,以简化常见任务(如停靠子控件)。
许可
MarqueeControl
。控制控件的序列化方式以及为其生成代码的方式。 有关详细信息,请参阅动态源代码生成和编译。