设计时开发 Windows 窗体控件

设计时开发 Windows 窗体控件

.NET Framework 为控件创作者提供了丰富的控件创作技术。 作者不再局限于设计作为现有控件集合的复合控件。 通过继承,可根据现有复合控件或现有 Windows 窗体控件创建自己的控件。 还可以自己设计实现自定义绘制的控件。 这些选项对可视化界面的设计和功能赋予了很大的灵活性。 若要利用这些功能,应熟悉基于对象的编程概念。

1、如何:创作 Windows 窗体的控件

控件表示用户和程序之间的图形链接。 控件可以提供或处理数据、接受用户输入、响应事件或执行连接用户和应用程序的其他功能(任意数量)。 因为控件本质上是具有图形界面的组件,所以它能提供组件所提供的所有功能并提供用户交互。 控件是针对特定目的而创建的,而创作控件只是另一种编程任务。 考虑到这一点,以下步骤概述了控件的创作过程。 链接提供有关各个步骤的附加信息。

创作控件

  1. 确定希望控件完成的操作或它在应用程序中所起的作用。 要考虑的因素有:

    • 需要哪种图形界面?

    • 此控件会处理哪些特定用户交互?

    • 是否有现有控件提供所需功能?

    • 通过组合几个 Windows 窗体控件可以获得所需功能吗?

  2. 如果控件需要一个对象模型,请确定如何在整个对象模型中分配功能,并在控件和任何子对象之间划分功能。 如果打算创作一个复杂的控件,或者想要并入一些功能,则对象模型可能会有用。

  3. 确定所需控件类型(例如,用户控件、自定义控件、继承的 Windows 窗体控件)。

  4. 将功能表示为控件及其子对象或子级结构的属性、方法和事件,并分配相应的访问级别(例如,公共、受保护等)。

  5. 如果需要为控件自定义绘制,请为其添加代码。

  6. 如果控件从 UserControl 继承,可以通过生成控件项目并在“UserControl 测试容器”中运行它,从而测试其运行时行为。

  7. 还可以通过创建新项目(如 Windows 应用程序)并将其放入容器来测试和调试控件。

  8. 添加每个功能时,将功能添加到测试项目以执行新功能。

  9. 重复上述步骤,优化设计。

  10. 打包和部署控件。

2、如何:创作复合控件

可通过多种方式使用复合控件。 可将其作为 Windows 桌面应用程序项目的一部分创作,并只在该项目的窗体上使用它们。 或者,可在 Windows 控件库项目中创作它们,将该项目编译为程序集,在其他项目中使用这些控件。 甚至可以从它们继承,并针对特殊情况,使用视觉对象继承快速对它们进行自定义。

创作复合控件

  1. Visual Studio 中创建新的 Windows 应用程序项目,将其命名为 DemoControlHost

  2. “项目” 菜单上,单击 “添加用户控件”

  3. 在“添加新项”对话框中,为类文件(.cs 文件)提供希望复合控件具有的名称。

  4. 选择“添加”按钮为复合控件创建类文件。

  5. 将控件从“工具箱”添加到复合控件表面。

  6. 将代码置于事件过程中,处理由复合控件或其构成控件所引发的事件。

  7. 关闭复合控件的设计器,并在出现提示时保存文件。

  8. 在“生成”菜单中,单击“生成解决方案”。

    必须生成项目,自定义控件才可在“工具箱”中显示。

  9. 使用“工具箱”的“DemoControlHost”选项卡将控件的实例添加到 Form1

创作控件类库

  1. 打开新的“Windows 控件库”项目。

    默认情况下,项目包含一个复合控件。

  2. 按照上述步骤中的说明添加控件和代码。

  3. 选择不想继承类更改的控件,并将此控件的“Modifiers”属性设置为“专用”。

  4. 生成 DLL

从控件类库中的复合控件中继承

  1. 在“文件”菜单上,指向“添加”并选择“新建项目”,将新的“Windows 应用程序”项目添加到解决方案中。

  2. 在“解决方案资源管理器”中,右键单击新项目的“引用”文件夹,并选择“添加引用”以打开“添加引用”对话框。

  3. 选择“项目”选项卡,然后双击控件库项目。

  4. 在“生成”菜单中,单击“生成解决方案”。

  5. 在“解决方案资源管理器”中,右键单击控件库项目并从快捷菜单中选择“添加新项”。

  6. 从“添加新项目”对话框中选择“继承的用户控件”模板。

  7. 在“继承选择器”对话框中,双击要继承的控件。

    已将新控件添加到你的项目。

  8. 打开新控件的可视化设计器,并添加其他构成控件。

    可看到从 DLL 中的复合控件继承的构成控件,还可以更改“Modifiers”属性为“Public”的控件的属性。 但不能更改“Modifiers”属性为“Private”的控件的属性。

3、如何:从 UserControl 类继承

若要通过自定义代码将一个或多个 Windows 窗体控件的功能进行组合,可以创建一个用户控件。 用户控件将快速控件开发、标准 Windows 窗体控件功能以及自定义属性和方法的多功能组合在一起。 开始创建用户控件时,系统将为你提供一个可见的设计器,可以将标准 Windows 窗体控件放置在该设计器中。 这些控件保留其所有继承的功能以及标准控件的外观和行为。 但是,一旦将这些控件内置到用户控件中,便不能再通过代码来使用。 用户控件执行其自身的绘图工作,同时也处理与控件相关联的所有基本功能。

创建用户控件

  1. Visual Studio 中,创建新的“Windows 控件库”项目。

    新创建的项目中将包含一个空白用户控件。

  2. 将控件从“工具箱”的“Windows 窗体”选项卡中拖到设计器上。

  3. 应对这些控件进行定位并设计成你希望它们显示在最终用户控件中的样子。 如果要允许开发人员访问构成控件,则必须将这些控件声明为公共的,或有选择地公开构成控件的属性。

  4. 实现控件将纳入的任何自定义方法或属性。

  5. F5 生成项目并在“UserControl 测试容器”中运行该控件。

4、如何:从现有 Windows 窗体控件继承

如果想要扩展现有控件的功能,可以通过继承创建一个派生自现有控件的控件。 从现有控件继承时,将继承该控件的的所有功能和可视属性。 例如,如果要创建从 Button 继承的控件,则新控件的外观和行为将与标准的 Button 控件完全一样。 然后可以通过实现自定义方法和属性来扩展或修改新控件的功能。 在某些控件中,还可以通过重写继承控件的 OnPaint 方法来更改其可视外观。

创建继承的控件

  1. Visual Studio 中创建新的“Windows 窗体应用程序”项目。

  2. 从“项目”菜单中选择“添加新项”。

    此时会显示“添加新项”对话框。

  3. 在“添加新项”对话框中,双击“自定义控件”。

    一个新的自定义控件将被添加到项目中。

  4. 如果你正在使用:

    • C#,请在“代码编辑器”中打开 CustomControl1.cs

  5. 找到从 Control 继承的类声明。

  6. 将基类更改为要从中继承的控件。

    例如,如果想从 Button 继承,请将类声明更改为以下内容:

    public partial class CustomControl1 : System.Windows.Forms.Button
  7. 如果想要修改控件的图形外观,请重写 OnPaint 方法。

    备注

    重写 OnPaint 将不允许你修改所有控件的外观。 那些由 Windows 完成其所有绘制的控件(例如 TextBox)从不调用其 OnPaint 方法,因此永远不会使用自定义代码。 请参阅想要修改的特定控件的帮助文档,以查看 OnPaint 是否可用。 如果控件未将 OnPaint 作为成员方法列出,则不能通过重写此方法来更改其外观。

    protected override void OnPaint(PaintEventArgs pe)
    {
       base.OnPaint(pe);
       // 插入代码进行自定义绘画。如果你想完全改变控件的外观,不要调用base.OnPaint(pe)。
    }
  8. 保存并测试控件。

5、如何:从 Control 类继承

如果想要创建在 Windows 窗体上使用的完全自定义控件,则应从 Control 类继承。 尽管从 Control 类继承需要你进行更多的规划和实现,但同时也为你提供了最大程度的选择自由。 从 Control 继承时,将继承使控件能够工作的最基本功能。 Control 类中固有的功能将处理用户通过键盘和鼠标的输入,定义控件的边界和大小,提供窗口句柄,以及提供信息处理和安全功能。 它没有纳入任何绘图功能(这里指的是控件的图形界面的实际呈现),也没有纳入任何特定的用户交互功能。 必须通过自定义代码提供所有的这些功能。

创建自定义控件

  1. Visual Studio 中,创建一个新的 Windows 应用程序Windows 控件库项目

  2. 从“项目”菜单中,选择“添加类”。

  3. 在“添加新项”对话框中,单击“自定义控件”。

    一个新的自定义控件将被添加到项目中。

  4. F7 打开自定义控件的“代码编辑器”。

  5. 找到 OnPaint 方法,该方法除了调用基类的 OnPaint 方法外,其他情况均为空。

  6. 修改代码以纳入控件所需的任何自定义绘图。

  7. 实现控件将纳入的任何自定义方法、属性或事件。

  8. 保存并测试控件。

6、如何:设计时将控件与窗体边缘对齐

通过设置 Dock 属性的值,可以使控件与窗体的边缘对齐。 此属性指定控件在窗体中的驻留位置。 可以将 Dock 属性设置为下列值:

设置控件上的效果
Bottom停靠到窗体底部。
Fill占据窗体中的所有剩余空间。
Left停靠到窗体的左侧。
None不在任何位置停靠,它显示在由其 Location 指定的位置。
Right停靠到窗体的右侧。
Top停靠到窗体的顶部。

在设计时设置控件的 Dock 属性

  1. Visual Studio 中的 Windows 窗体设计器中,选择控件。

  2. 在“属性”窗口中,单击 Dock 属性旁边的下拉框。

    此时将显示一个表示六种可能的 Dock 设置的图形界面。

  3. 选择相应的设置。

  4. 控件现在将以设置指定的方式停靠。

7、如何:在“选择工具箱项”对话框中显示控件

开发和分发控件时,你可能希望这些控件显示在 Visual Studio 的“选择工具箱项”对话框中,并在右键单击“工具箱”并选择“选择项”时显示该对话框。 可以使用 AssemblyFoldersEx 注册过程使控件能够在此对话框中显示。

要在“选择工具箱项”对话框中显示控件,请执行以下操作:

  • 将控件程序集安装到全局程序集缓存中。

    -或-

  • 使用 AssemblyFoldersEx 注册过程注册控件及其关联的设计时程序集。 AssemblyFoldersEx 是一个注册表位置,第三方供应商在其中存储他们支持的每个框架版本的路径。 设计时解析可在此注册表位置中查找参考程序集。 注册表脚本可以指定要显示在工具箱中的控件。

8、如何:为控件提供工具箱位图

如果希望在 Visual Studio 的“工具箱”中为控件显示特殊图标,可通过使用 ToolboxBitmapAttribute 指定特定的图像。 此类是一个特性,是一种可以附加到其他类上的特殊类。

通过使用 ToolboxBitmapAttribute,可以指定一个字符串来指示一个 16 x 16 像素位图的路径和文件名。 添加到“工具箱”后,此位图显示在对应的控件旁边。 还可指定 Type,在这种情况下会加载与该类型关联的位图。 如果同时指定 Type 和字符串,则控件会在包含 Type 参数所指定类型的程序集中搜索名称由字符串参数指定的图像资源。

指定控件的工具箱位图

ToolboxBitmapAttribute 添加到控件的类声明中,对于 Visual C# 则应置于类声明之上。

// 指定与按钮类型关联的位图。
[ToolboxBitmap(typeof(Button))]
class MyControl1 : UserControl
{
}
// 指定位图文件。
[ToolboxBitmap(@"C:\Documents and Settings\Joe\MyPics\myImage.bmp")]
class MyControl2 : UserControl
{
}
// 指定指示要搜索的程序集和要查找的映像资源的名称的类型。
[ToolboxBitmap(typeof(MyControl), "MyControlBitmap")]
class MyControl : UserControl
{
}

重新生成项目。

备注

对于自动生成的控件和组件,位图不会出现在工具箱中。 若要查看位图,请使用“选择工具箱项”对话框重载控件。

9、如何:测试 UserControl 的运行时行为

开发 UserControl 时,需要测试它的运行时行为。 可以创建单独的基于 Windows 的应用程序项目并将控件放在测试窗体中,但是此过程很不方便。 一种更快、更简单的方法是使用 Visual Studio 提供的 UserControl 测试容器。 此测试容器直接从 Windows 控件库项目启动。

重要

为了让测试容器加载 UserControl,该控件必须至少具有一个公共构造函数。

测试 UserControl 的运行时行为

  1. Visual Studio 中,创建一个 Windows 控件库项目,并将其命名为 TestContainerExample

  2. 在“Windows 窗体设计器”中,将 Label 控件从“工具箱”拖到控件的设计图面中。

  3. 按 F5 生成项目并运行 UserControl 测试容器。 测试容器与 UserControl 一起显示在“预览”窗格中。

  4. 选择“预览”窗格右侧的 PropertyGrid 控件中显示的 BackColor 属性。 将其值更改为“ControlDark”。 可以观察到控件变为较深的颜色。 尝试更改其他属性值并观察其对控件的影响。

  5. 单击“预览”窗格下方的“停靠填充用户控件”复选框。 可以观察到该控件的大小经过调整以填充单元格。 调整测试容器的大小,并观察到该控件随窗格一起调整了大小。

  6. 关闭测试容器。

  7. 将另一个用户控件添加到 TestContainerExample 项目。

  8. 在“Windows 窗体设计器”中,将 Button 控件从“工具箱”拖到控件的设计图面中。

  9. F5 构建项目并运行测试容器。

  10. 单击“选择用户控件ComboBox 以在两个用户控件之间进行切换。

测试来自其他项目的用户控件

可以在当前项目的测试容器中测试来自其他项目的用户控件。

  1. Visual Studio 中,创建一个 Windows 控件库项目,并将其命名为 TestContainerExample2

  2. 在“Windows 窗体设计器”中,将 RadioButton 控件从“工具箱”拖到控件的设计图面中。

  3. F5 构建项目并运行测试容器。 测试容器与 UserControl 一起显示在“预览”窗格中。

  4. 单击“加载”按钮。

  5. 在“打开”对话框中,导航到你在上一个过程中构建的 TestContainerExample.dll。 选择 TestContainerExample.dll,然后单击“打开”按钮以加载用户控件。

  6. 使用“选择用户控件ComboBox,以在 TestContainerExample 项目中的两个用户控件之间进行切换。

10、演练:使用 C# 创作复合控件

复合控件提供了一种创建和重用自定义图形界面的方法。 复合控件本质上是具有可视化表示形式的组件。 因此,它可能包含一个或多个 Windows 窗体控件、组件或代码块,它们能够通过验证用户输入、修改显示属性或执行作者所需的其他任务来扩展功能。 可以按照与其他控件相同的方式将复合控件置于 Windows 窗体中。 在本演练的第一部分,将创建一个名为 ctlClock 的简单复合控件。 在本演练的第二部分,将通过继承扩展 ctlClock 的功能。

创建项目

创建新的项目时应指定其名称,以设置根命名空间、程序集名称和项目名称,并确保默认组件将位于正确的命名空间中。

创建 CtlClockLib 控件库和 CtlClock 控件

  1. Visual Studio 中,新建一个“Windows 窗体控件库”项目,并将其命名为“CtlClockLib”。

    默认情况下,项目名称 CtlClockLib 也会分配到根命名空间中。 根命名空间用于限定程序集中的组件名。 例如,如果两个程序集都提供名为 CtlClock 的组件,则可以使用 CtlClockLib.CtlClock. 指定 ctlClock 组件

  2. 在“解决方案资源管理器”中,右键单击“UserControl1.cs”,然后单击“重命名”。 将文件名更改为 CtlClock.cs。 当系统询问是否重命名对代码元素“UserControl1”的所有引用时,单击“是”按钮。

    备注

    默认情况下,复合控件继承自系统提供的 UserControl 类。 UserControl 类提供所有复合控件所需的功能,并实现标准方法和属性。

  3. 在“文件”菜单上,单击“全部保存”保存项目。

将 Windows 控件和组件添加到复合控件

可视化界面是复合控件的基本部分。 这种可视化界面通过向设计器图面添加一个或多个 Windows 控件实现。 在下面的演示中,将向复合控件中加入 Windows 控件并编写代码实现功能。

将标签和计时器添加到复合控件

  1. 在“解决方案资源管理器”中,右键单击“CtlClock.cs”,然后单击“视图设计器”。

  2. 在“工具箱”中,展开“公共控件”节点,然后双击“标签”。

    设计器图面上的控件中添加了一个名为 label1Label 控件。

  3. 在设计器中,单击“label1”。 在“属性”窗口中,设置下列属性。

    属性更改为
    NamelblDisplay
    Text(blank space)
    TextAlignMiddleCenter
    Font.Size14
  4. 在“工具箱”中,展开“组件”节点,然后双击“计时器”。

    因为 Timer 是一个组件,所以它在运行时没有可视化表示形式。 因而它不会和控件一起出现在设计器图面上,而是出现在“组件设计器”(设计器图面底部的一栏)中。

  5. 在“组件设计器”中,单击“timer1”,然后将 Interval 属性设置为 1000,将 Enabled 属性设置为 true

    Interval 属性控制 Timer 组件计时的频率。 timer1 每走过一个刻度,它都会运行一次 timer1_Tick 事件中的代码。 间隔表示计时之间的毫秒数。

  6. 在“组件设计器”中,双击“timer1”转到 CtlClocktimer1_Tick 事件。

  7. 将代码修改为类似如下所示的代码示例。 确保将访问修饰符从 private 更改为 protected

    protected void timer1_Tick(object sender, System.EventArgs e)
    {
        // 使标签显示当前时间。
        lblDisplay.Text = DateTime.Now.ToLongTimeString();
    }

    此代码将使得当前时间显示在 lblDisplay 中。 因为 timer1 的间隔设置为 1000,所以该事件每 1000 毫秒发生一次,从而每一秒更新一次当前时间。

  8. 修改该方法使其可通过 virtual 关键字重写。 有关详细信息,请参阅下面的“从用户控件继承”一节。

    protected virtual void timer1_Tick(object sender, System.EventArgs e)
  9. 在“文件”菜单上,单击“全部保存”保存项目。

将属性添加到复合控件

现在,时钟控件封装了 Label 控件和 Timer 组件,这两者各有一组固有属性。 尽管控件的后续用户无法访问这些控件的单个属性,但可以通过编写适当的代码块来创建和公开自定义属性。 在下面的过程中,将向控件添加属性,这些属性可使用户能够更改背景和文本的颜色。

将属性添加到复合控件
  1. 在“解决方案资源管理器”中,右键单击“CtlClock.cs”,然后单击“查看代码”。

    控件的“代码编辑器”随即打开。

  2. 找到 public partial class CtlClock 语句。 在左大括号 ({) 下键入下以下代码。

    private Color colFColor;
    private Color colBColor;

    这些语句会创建私有变量,用来存储要创建的属性的值。

  3. 在步骤 2 中的变量声明下方输入或粘贴以下代码。

            /// <summary>
            /// 查看、设置前景色
            /// </summary>
            public Color ColFColor 
            { 
                get => colFColor;
                set
                {
                    colFColor = value;
                    lblDisplay.ForeColor = colFColor;
                }
            }
    ​
            /// <summary>
            /// 查看、设置背景色
            /// </summary>
            public Color ColBColor { 
                get => colBColor;
                set
                {
                    colBColor = value;
                    lblDisplay.BackColor = colBColor;
                }
            }

    上述代码使得后续用户能够使用 ClockForeColorClockBackColor 这两个自定义属性。 getset 语句提供了该属性值的存储和检索,还提供了实现适合于该属性的功能的代码。

  4. 在“文件”菜单上,单击“全部保存”保存项目。

测试控件

控件不是独立应用程序,它们必须托管在容器中。 测试控件的运行时行为,并使用“UserControl 测试容器”运用其属性。

测试控件

  1. 按“F5”生成项目并在“UserControl 测试容器”中运行控件。

  2. 在测试容器的属性网格中,找到 ClockBackColor 属性,然后选择该属性以显示调色板。

  3. 通过单击选择颜色。

    控件的背景颜色更改为你选择的颜色。

  4. 使用类似的事件序列来验证 ClockForeColor 属性的功能是否与预期的相符。

    本节和前面的几节演示了如何将组件和 Windows 控件与代码和打包相结合,以复合控件的形式提供自定义功能。 你现已了解如何在复合控件中公开属性,以及如何在完成后对控件进行测试。 下一节将介绍如何以 ctlClock 为基础构造继承的复合控件。

从复合控件继承

前面的章节中介绍了如何将 Windows 控件、组件和代码组合成可重用的复合控件。 现在即可将复合控件用作生成其他控件的基础。 从基类派生类的过程称为继承。 在本节中,将创建一个称为 ctlAlarmClock 的复合控件。 此控件将从其父控件 ctlClock 派生。 将介绍如何通过重写父级方法并添加新的方法和属性来扩展 ctlClock 的功能。

创建继承控件的第一步是从它的父控件派生。 该操作创建一个新控件,它具有父控件的所有属性、方法和图形特征,但也可以用作添加新功能或修改过的功能的基础。

创建继承的控件

  1. 在“解决方案资源管理器”中,右键单击“CtlClockLib”,指向“添加”,然后单击“用户控件”。

    此时将打开“添加新项”对话框。

  2. 选择“继承的用户控件”模板。

  3. 在“名称”框中,键入 CtlAlarmClock.cs,然后单击“添加”。

    “继承选择器”对话框将出现。

  4. 在“组件名称”下,双击“CtlClock”。

  5. 在“解决方案资源管理器”中,浏览当前项目。

    备注

    当前项目中添加了一个名为“CtlAlarmClock.cs”的文件。

添加警报属性

将属性添加到继承的控件的方法与将其添加到复合控件的方法相同。 现在将使用属性声明语法向控件中添加两个属性:AlarmTimeAlarmSet,前者将存储发出警报的日期和时间值,后者指示是否设置了警报。

将属性添加到复合控件
  1. 在“解决方案资源管理器”中,右键单击“CtlAlarmClock”,然后单击“查看代码”。

  2. 找到 public class 语句。 请注意,控件继承自 ctlClockLib.ctlClock。 在左大括号 ({) 语句下键入下以下代码。

    private DateTime dteAlarmTime;
    private bool blnAlarmSet;
    // 这些属性将被声明为公共,以允许未来的开发人员访问它们。
    public DateTime AlarmTime
    {
        get
        {
            return dteAlarmTime;
        }
        set
        {
            dteAlarmTime = value;
        }
    }
    public bool AlarmSet
    {
        get
        {
            return blnAlarmSet;
        }
        set
        {
            blnAlarmSet = value;
        }
    }

添加到控件的图形界面

继承的控件具有可视化界面,该界面与它从中继承的控件的界面完全相同。 它与其父控件拥有相同的构成控件,但除非将构成控件的属性特别公开,否则它们将不可用。 可以向继承的复合控件的图形界面进行添加,方法与向任意复合控件进行添加相同。 若要继续向警报时钟的可视化界面进行添加,请添加一个标签控件,它将在警报响起时闪烁。

添加标签控件
  1. 在“解决方案资源管理器”中,右键单击“CtlAlarmClock”,然后单击“视图设计器”。

    CtlAlarmClock 的设计器将在主窗口中打开。

  2. 单击该控件的显示部分,然后查看“属性”窗口。

    备注

    当显示所有属性时,属性是浅灰色的。 这表示这些属性是 lblDisplay 所固有的,不能在“属性”窗口中修改或访问。 默认情况下,复合控件中所包含的控件为 private,通过任何途径都无法访问其属性。

    备注

    如果希望复合控件的后续用户可以访问其内部控件,则可将其声明为 publicprotected。 如此即可使用适当的代码设置和修改复合控件内所包含控件的属性。

  3. Label 控件添加到复合控件。

  4. 使用鼠标将 Label 控件拖到显示框的下方。 在“属性”窗口中,设置下列属性。

    属性设置
    NamelblAlarm
    TextAlarm!
    TextAlignMiddleCenter
    Visiblefalse

添加警报功能

在前面的章节中,已经添加了一些属性和一个控件,它们将启用复合控件中的警报功能。 在本过程中,将添加代码以比较当前时间和警报时间,如果两者相同,则警报闪烁。 通过重写 CtlClocktimer1_Tick 方法并向其中添加其他代码,可扩展 CtlAlarmClock 的功能,同时会保留 CtlClock 的所有固有功能。

重写 CtlClocktimer1_Tick 方法
  1. 在“代码编辑器”中,找到 private bool blnAlarmSet; 语句。 在其紧下方添加下列语句。

    private bool blnColorTicker;
  2. 在“代码编辑器”中,在类的结尾找到右大括号 (})。 在大括号的前面添加以下代码。

    protected override void timer1_Tick(object sender, System.EventArgs e)
    {
        // 调用CtlClock的Timer1_Tick方法。
        base.timer1_Tick(sender, e);
        // 检查是否设置了警报。
        if (AlarmSet == false)
            return;
        else
        {   // 如果告警时间的日期、小时、分钟与当前时间一致,则闪烁告警。
            if (AlarmTime.Date == DateTime.Now.Date && AlarmTime.Hour ==
                DateTime.Now.Hour && AlarmTime.Minute == DateTime.Now.Minute)
            {
                // 将lblAlarmVisible设置为true,并根据blnColorTicker的值改变背景颜色。
                // 标签的背景颜色将闪烁一次时钟的滴答。
                lblAlarm.Visible = true;
                if (blnColorTicker == false)
                {
                    lblAlarm.BackColor = Color.Red;
                    blnColorTicker = true;
                }
                else
                {
                    lblAlarm.BackColor = Color.Blue;
                    blnColorTicker = false;
                }
            }
            else
            {
                // 一旦警报响起一分钟,标签就会再次隐形。
                lblAlarm.Visible = false;
            }
        }
    }

添加此代码将完成多项任务。 override 语句指示控件使用此方法替换从基控件继承的方法。 调用此方法时,它通过调用 base.timer1_Tick 语句来调用它重写的方法,从而确保在该控件中重现原始控件包含的所有功能。 然后,运行附加代码以合并警报功能。 警报触发时,将会出现闪烁的标签控件。

警报时钟控件已基本完成。 剩下的唯一事情是实现关闭它的方法。 为此,将向 lblAlarm_Click 方法添加代码。

实现关闭方法
  1. 在“解决方案资源管理器”中,右键单击“CtlAlarmClock.cs”,然后单击“视图设计器”。

    设计器随即打开。

  2. 将按钮添加到控件。 按如下方式设置该按钮的属性。

    属性
    NamebtnAlarmOff
    Text禁用警报
  3. 在设计器中,双击“btnAlarmOff”控件。

    “代码编辑器”随即打开并显示 private void btnAlarmOff_Click 行。

  4. 将此方法修改为类似如下所示的代码。

    private void btnAlarmOff_Click(object sender, System.EventArgs e)
    {
        // 关闭报警。
        AlarmSet = false;
        // 隐藏闪烁的标签。
        lblAlarm.Visible = false;
    }
  5. 在“文件”菜单上,单击“全部保存”保存项目。

在窗体上使用继承的控件

可按测试基类控件相同的方法测试继承的控件,ctlClock:按“F5”生成项目并在“UserControl 测试容器”中运行控件。

若要使用控件,还需要将其放入窗体中。 与标准复合控件一样,继承的复合控件不能独立存在,而必须承载在窗体或其他容器中。 由于 ctlAlarmClock 的功能更加深入,因此需要附加代码以对其进行测试。 在本过程中,将编写一个简单的程序来测试 ctlAlarmClock 的功能。 将编写代码来设置和显示 ctlAlarmClockAlarmTime 属性,并测试其固有功能。

生成测试窗体并将控件添加到该窗体
  1. 在“解决方案资源管理器”中,右键单击“CtlClockLib”,然后单击“生成”。

  2. 将一个新的“Windows 窗体应用程序”项目添加到解决方案,并将其命名为“测试”。

  3. 在“解决方案资源管理器”中,右键单击测试项目的“引用”节点。 单击“添加引用”,显示“添加引用”对话框。 单击标记为“项目”的选项卡。 “项目名称”下将列出 ctlClockLib 项目。 双击该项目将引用添加到测试项目。

  4. 在“解决方案资源管理器”中,右键单击“测试”,然后单击“生成”。

  5. 在“工具箱”中,展开“CtlClockLib 组件”节点。

  6. 双击“CtlAlarmClock”向窗体添加 CtlAlarmClock 的副本。

  7. 在“工具箱”中,找到并双击“DateTimePicker”以将 DateTimePicker 控件添加到窗体中,然后通过双击“标签”添加一个 Label 控件。

  8. 使用鼠标将这些控件放置在窗体上合适的位置。

  9. 按下述方法设置这些控件的属性。

    控制属性
    label1Text(blank space)
    NamelblTest
    dateTimePicker1NamedtpTest
    格式Time
  10. 在设计器中,双击“dtpTest”。

    “代码编辑器”随即打开并显示 private void dtpTest_ValueChanged

  11. 像如下所示修改代码。

    private void dtpTest_ValueChanged(object sender, System.EventArgs e)
    {
        ctlAlarmClock1.AlarmTime = dtpTest.Value;
        ctlAlarmClock1.AlarmSet = true;
        lblTest.Text = "Alarm Time is " + ctlAlarmClock1.AlarmTime.ToShortTimeString();
    }
  12. 在“解决方案资源管理器”中,右键单击“测试”,然后单击“设为启动项目”。

  13. “调试” 菜单中,单击 “启动调试”

    测试程序随即启动。 请注意,CtlAlarmClock 控件中的当前时间已更新,并且在 DateTimePicker 控件中显示启动时间。

  14. 单击 DateTimePicker,其中显示小时的分钟数。

  15. 使用键盘设置一个分钟值,使它比 ctlAlarmClock 显示的当前时间快一分钟。

    警报设置的时间在 lblTest 中显示。 等候显示的时间到达警报设置时间。 显示时间到达警报设置的时间时,lblAlarm 会闪烁。

  16. 单击 btnAlarmOff 关闭警报。 现在可以重置警报。

本文涵盖了许多关键概念。 现已应了解如何通过将控件和组件组合到复合控件容器中来创建复合控件。 还应了解图和将属性添加到控件,以及如何编写代码以实现自定义功能。 在最后一节中,应了解到如何通过继承来扩展给定复合控件的功能,以及如何通过重写承载方法来改变这些方法的功能。

11、演练:使用 C# 从 Windows 窗体控件继承

使用 C# 可通过继承来创建功能强大的自定义控件。 通过继承,可以创建不仅保留了标准 Windows 窗体控件的所有固有功能,而且还包含自定义功能的控件。 在本演练中,将创建一个名为 ValueButton 的简单继承控件。 此按钮将继承标准 Windows 窗体 Button 控件的功能,并将公开一个名为 ButtonValue 的自定义属性。

创建项目

创建新的项目时应指定其名称,以设置根命名空间、程序集名称和项目名称,并确保默认组件将位于正确的命名空间中。

创建 ValueButtonLib 控件库和 ValueButton 控件

  1. Visual Studio 中,创建一个新的“Windows 窗体控件库”项目,并将其命名为 ValueButtonLib

    默认情况下,项目名称 ValueButtonLib 也会分配到根命名空间中。 根命名空间用于限定程序集中的组件名。 例如,如果两个程序集都提供名为 ValueButton 的组件,则可以使用 ValueButtonLib.ValueButton 指定 ValueButton 组件。

  2. 在“解决方案资源管理器”中,右键单击“UserControl1.cs”,然后从快捷菜单中选择“重命名”。 将文件名更改为 ValueButton.cs。 询问是否希望重命名对代码元素“”的所有引用时,单击“是”UserControl1按钮。

  3. 在“解决方案资源管理器”中,右键单击“ValueButton.cs”并选择“查看代码”。

  4. 找到 class 语句行 public partial class ValueButton,并将此控件从 UserControl 继承的类型更改为 Button。 这允许所继承的控件继承 Button 控件的所有功能。

  5. 在“解决方案资源管理器”中打开“ValueButton.cs”节点,以显示设计器生成的代码文件 ValueButton.Designer.cs。 在“代码编辑器”中打开此文件。

  6. 找到 InitializeComponent 方法并删除分配 AutoScaleMode 属性的行。 Button 控件中不存在此属性。

  7. 在“文件”菜单中,选择“全部保存”以保存项目。

    备注

    可视化设计器不再可用。 由于 Button 控件自行绘制,因此无法在设计器中修改其外观。 除非在代码中进行了修改,否则它的视觉对象表示形式将与其继承的类(即 Button)的视觉对象表示形式完全相同。 但仍然可以向设计器图面添加不含 UI 元素的组件。

将属性添加到继承控件

继承的 Windows 窗体控件的可能用途之一是创建与标准 Windows 窗体控件的外观和感受相同、但公开自定义属性的控件。 在本节中,将向控件中添加名为 ButtonValue 的属性。

添加 Value 属性

  1. 在“解决方案资源管理器”中,右键单击“ValueButton.cs”,然后从快捷菜单中单击“查看代码”。

  2. 找到 class 语句。 紧接 { 之后键入以下代码:

            // 创建将存储属性值的私有变量。
            private int varValue;
    
            /// <summary>
            /// 声明属性。
            /// </summary>
            public int ButtonValue { get => varValue; set => varValue = value; }

    此代码设置存储和检索 ButtonValue 属性的方法。 get 语句将返回的值设置为存储在私有变量 varValue 中的值,而 set 语句通过使用 value 关键字设置该私有变量的值。

  3. 在“文件”菜单中,选择“全部保存”以保存项目。

测试控件

控件不是独立的项目,它们必须托管在容器中。 若要测试控件,必须提供一个测试项目,以使控件在其中运行。 还必须通过生成(编译)该控件使测试项目能够访问它。 在本节中,将生成控件并在 Windows 窗体中对其进行测试。

生成控件

在“生成”菜单中,单击“生成解决方案”。 生成应会成功,且没有任何编译器错误或警告。

创建测试项目

  1. 在“文件”菜单上,指向“添加”,然后单击“新建项目”打开“添加新项目”对话框。

  2. 在“Visual C#”节点下选择“Windows”节点,再单击“Windows 窗体应用程序”。

  3. 在“名称”框中,输入“测试”。

  4. 在“解决方案资源管理器”中,右键单击测试项目的“引用”节点,然后从快捷菜单上选择“添加引用”以显示“添加引用”对话框。

  5. 单击标记为“项目”的选项卡。 “项目名称”下将列出 ValueButtonLib 项目。 双击该项目将引用添加到测试项目。

  6. 在“解决方案资源管理器”中,右键单击“测试”,然后选择“生成”。

将控件添加到窗体

  1. 在“解决方案资源管理器”中,右键单击“Form1.cs”,然后从快捷菜单中选择“视图设计器”。

  2. 在“工具箱”中选择“ValueButtonLib 组件”。 双击“ValueButton”。

    窗体上将出现“ValueButton”。

  3. 右键单击“ValueButton”,并从快捷菜单中选择“属性”。

  4. 在“属性”窗口中,检查该控件的属性。 请注意,除增加了一个 ButtonValue 属性外,它们与标准按钮公开的属性相同。

  5. ButtonValue 属性设置为 5。

  6. 在“工具箱”的“所有 Windows 窗体”选项卡中,双击“标签”以将 Label 控件添加到窗体。

  7. 将标签重新定位到窗体的中央。

  8. 双击 valueButton1

    “代码编辑器”随即打开并显示 valueButton1_Click 事件。

  9. 插入以下代码行。

    label1.Text = valueButton1.ButtonValue.ToString();
  10. 在“解决方案资源管理器”中,右键单击“测试”,然后从快捷菜单中选择“设为启动项目”。

  11. 从“调试”菜单中选择“启动调试”。

    Form1 将出现。

  12. 单击 valueButton1

    label1 中显示数字“5”,表明继承的控件的 ButtonValue 属性已通过 valueButton1_Click 方法传递给 label1。 这样,ValueButton 控件便继承了标准 Windows 窗体按钮的所有功能,但是公开了一个附加的自定义属性。

12、演练:使用设计操作执行常规任务

为 Windows 窗体应用程序构造窗体和控件时,有许多任务需要重复执行。 下面的列表显示了一些你将遇到的常规执行任务:

  • TabControl 上添加或删除选项卡。

  • 将控件停靠到其父级。

  • 更改 SplitContainer 控件的方向。

为了加快开发速度,许多控件都提供有设计器操作,即上下文敏感的菜单,使你可以在设计时用单个手势执行此类常规任务。 这些任务称为“设计器操作谓词”。

在设计器中的生存期内,设计器操作将一直附加到控件实例,并且始终可用。

创建项目

第一步是创建项目并设置窗体。

  1. Visual Studio 中,创建一个名为 DesignerActionsExample 的基于 Windows 的应用程序项目。

  2. 在“Windows 窗体设计器”中选择窗体。

使用设计器操作

设计器操作在设计时始终在提供它们的控件上可用。

  1. TabControl 从“工具箱”拖到窗体上。

  2. 单击设计器操作字形。 在字形旁边显示的快捷菜单中,选择“添加选项卡”项。 请注意,新的选项卡页已添加到 TabControl

  3. TableLayoutPanel “工具箱” 控件拖到你的窗体上。

  4. 单击设计器操作字形。 在字形旁边显示的快捷菜单中,选择“添加列”项。 请注意,新的列已添加到 TableLayoutPanel 控件。

  5. SplitContainer “工具箱” 控件拖到你的窗体上。

  6. 单击设计器操作字形。 在字形旁边显示的快捷菜单中,选择“水平拆分器方向”项。 请注意,SplitContainer 控件的拆分器现在是水平方向。

13、演练:序列化标准类型集合

自定义控件有时会将集合作为属性公开。 此演练演示如何使用 DesignerSerializationVisibilityAttribute 类来控制集合在设计时的序列化方式。 将 Content 值应用于集合属性可确保该属性将被序列化。

注意

此内容是为.NET Framework编写的。 如果使用的是 .NET 6 或更高版本,请谨慎使用此内容。

先决条件

若要完成本演练,必须具有 Visual Studio

创建具有可序列化集合的控件

第一步是创建具有可序列化集合作为属性的控件。 可以使用集合编辑器编辑此集合的内容,该编辑器可从“属性”窗口访问。

  1. Visual Studio 中,创建一个 Windows 控件库项目,并将其命名为 SerializationDemoControlLib

  2. UserControl1 重命名为 SerializationDemoControl

  3. 在“属性”窗口中,将 Padding.All 属性的值设置为“10”。

  4. SerializationDemoControl 中放置一个 TextBox 控件。

  5. 选择 TextBox 控件。 在“属性”窗口中,设置下列属性。

    属性更改为
    multilinetrue
    DockFill
    ScrollBarsVertical
    ReadOnlytrue
  6. 在代码编辑器中,在 SerializationDemoControl 中声明一个名为 stringsValue 的字符串数组字段。

    // 这个字段支持字符串属性。
    private String[] stringsValue = new String[1];
  7. SerializationDemoControl 上定义 Strings 属性。

    备注

    Content 值用于启用集合的序列化。

    // 当DesignerSerializationVisibility属性的值为“Content”或“Visible”时,设计器将序列化该属性。
    // 也可以在设计时使用CollectionEditor编辑此属性。
    // 当DesignerSerializationVisibility属性的值为“Content”或“Visible”时,设计器将序列化该属性。
    // 也可以在设计时使用CollectionEditor编辑此属性。
    [DesignerSerializationVisibility(
        DesignerSerializationVisibility.Content )]
    public String[] Strings
    {
        get
        {
            return this.stringsValue;
        }
        set
        {
            this.stringsValue = value;
    
            // 用stringsValue数组中的值填充包含的TextBox。
            StringBuilder sb =
                new StringBuilder(this.stringsValue.Length);
    
            for (int i = 0; i < this.stringsValue.Length; i++)
            {
                sb.Append(this.stringsValue[i]);
                sb.Append("\r\n");
            }
    
            this.textBox1.Text = sb.ToString();
        }
    }
  8. F5 生成项目并在“UserControl 测试容器”中运行该控件。

  9. UserControl 测试容器的 PropertyGrid 中找到“字符串”属性。 选择“字符串”属性,然后选择“省略号”按钮属性窗口按钮中打开“字符串集合编辑器”。

  10. 在字符串集合编辑器中输入多个字符串。 在每个字符串末尾按 Enter 键将其隔开。 输入完字符串后单击“确定”。

备注

所键入的字符串随即显示在 SerializationDemoControlTextBox 中。

序列化集合属性

若要测试控件的序列化行为,需要将该控件置于一个窗体上并使用集合编辑器更改集合的内容。 可以通过查看 Windows 窗体设计器向其中发出代码的特殊设计器文件来查看序列化集合状态。

  1. 向该解决方案添加一个“Windows 应用程序”项目。 将项目命名为 SerializationDemoControlTest

  2. 在工具箱中,找到名为 SerializationDemoControlLib 组件的选项卡。 在此选项卡中,你将找到 SerializationDemoControl

  3. 在窗体上放置一个 SerializationDemoControl

  4. 在属性窗口中找到 Strings 属性。 Strings单击属性,然后单击省略号按钮打开字符串集合编辑器

  5. 在字符串集合编辑器中键入多个字符串。 在每个字符串末尾按 Enter 将其隔开。 输入完字符串后单击“确定”。

    备注

    所键入的字符串随即显示在 SerializationDemoControlTextBox 中。

  6. 在“解决方案资源管理器”中,单击“显示所有文件”按钮。

  7. 打开 Form1 节点。 其下方有一个名为 Form1.Designer.cs 。 这是 Windows 窗体设计器向其中发出代码的文件,该代码表示窗体及其子控件的设计时状态。 在“代码编辑器”中打开此文件。

  8. 打开名为“Windows 窗体设计器生成的代码”的区域,找到标有 serializationDemoControl1 的部分。 此标签下方是表示控件的序列化状态的代码。 在步骤 5 中键入的字符串随即显示在 Strings 属性的赋值中。 以下 C# 的代码示例显示的代码类似于你在键入字符串“red”、“orange”和“yellow”时所看到的代码。

    this.serializationDemoControl1.Strings = new string[] {
            "red",
            "orange",
            "yellow"};

  9. 在代码编辑器中,将 Strings 属性上的 DesignerSerializationVisibilityAttribute 的值更改为 Hidden

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
  10. 重新生成解决方案并重复步骤 3 和 4。

备注

在这种情况下,Windows 窗体设计器不会向 Strings 属性发出任何赋值。

14、演练:在设计时调试自定义 Windows 窗体控件

创建自定义控件时,你通常会发现有必要调试其设计时行为。 如果要为自定义控件创作自定义设计器,则尤其如此。

可以使用 Visual Studio 调试自定义控件,就像调试任何其他 .NET Framework 类一样。 区别在于,你将调试的是运行自定义控件代码的 Visual Studio 的单独实例。

创建项目

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

Visual Studio 中创建 Windows 应用程序项目,并将其命名为 DebuggingExample

创建控件库项目

  1. 将“Windows 控件库”项目添加到解决方案。

  2. 将新的 UserControl 项添加到 DebugControlLibrary 项目。 将其命名为 DebugControl

  3. 在“解决方案资源管理器”中,通过删除基名称为 UserControl1 的代码文件来删除项目的默认控件。

  4. 生成解决方案。

检查点

此时,你将能够在“工具箱”中看到自定义控件。

若要查看进度,请找到名为“DebugControlLibrary 组件”的新选项卡,然后单击以将其选中。 选项卡打开后,可以看到控件作为 DebugControl 列出,旁边有一个默认图标。

向自定义控件添加属性

若要演示自定义控件代码在设计时运行,需要添加属性并在实现该属性的代码中设置断点。

  1. 打开“代码编辑器”中的“DebugControl”。 在类定义中添加以下代码:

    private string demoStringValue = null;
    [Browsable(true)]
    public string DemoString
    {
        get
        {
            return this.demoStringValue;
        }
        set
        {
            demoStringValue = value;
        }
    }
  2. 生成解决方案。

将自定义控件添加到主机窗体

若要调试自定义控件的设计时行为,需要将自定义控件类的实例放置在主机窗体上。

  1. 在“DebuggingExample”项目中,打开“Windows 窗体设计器”中的“Form1”。

  2. 在“工具箱”中,打开“DebugControlLibrary 组件”选项卡,然后将“DebugControl”实例拖动到窗体上。

  3. 在“属性”窗口中找到 DemoString 自定义属性。 请注意,可以像更改任何其他属性一样更改其值。 另请注意,选中 DemoString 属性后,属性的说明字符串将显示在“属性”窗口底部。

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

若要调试自定义控件的设计时行为,需要调试运行自定义控件代码的 Visual Studio 的单独实例。

  1. 右键单击“解决方案资源管理器”中的“DebugControlLibrary”项目,然后选择“属性”。

  2. DebugControlLibrary 属性表中,选择“调试”选项卡。

    在“启动操作”部分中,选择“启动外部程序”。 你会调试 Visual Studio 的单独实例,因此请单击省略号按钮,以浏览 Visual Studio IDE。 可执行文件的名称为 devenv.exe,如果安装到默认位置,则其路径为 %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<edition>\Common7\IDE

  3. 选择“确定”关闭对话框 。

  4. 右键单击 DebugControlLibrary 项目,然后选择“设为启动项目”以启用此调试配置。

在设计时调试自定义控件

现在,自定义控件在设计模式下运行,因此可以对其进行调试。 启动调试会话后,将创建 Visual Studio 的新实例,你将使用此实例来加载“DebuggingExample”解决方案。 打开“窗体设计器”中的 Form1 后,将创建自定义控件的实例,并开始运行。

  1. 打开“代码编辑器”中的“DebugControl”源文件,并在 DemoString 属性的 Set 访问器上放置一个断点。

  2. F5 启动调试会话。 会创建 Visual Studio 的新实例。 可以通过两种方式区分实例:

    • 调试实例的标题栏中显示“正在运行”一词

    • 调试实例的“调试”工具栏上禁用了“开始”按钮

    断点在调试实例中进行设置。

  3. Visual Studio 的新实例中,打开“DebuggingExample”解决方案。 可以通过从“文件”菜单中选择“最近使用的项目”来轻松找到解决方案。 “DebuggingExample.sln”解决方案文件会列为最近使用的文件。

    重要

    如果要调试 .NET 5 (.NET Core 3.1) 或更高版本的 Windows 窗体项目,请使用 Visual Studio 的此实例将调试器附加到 DesignToolsServer.exe 进程。 选择“调试”>“附加到进程”菜单项。 在进程列表中找到 DesignToolsServer.exe,然后按“附加”。

  4. 打开“窗体设计器”中的 Form1,然后选择“DebugControl”控件。

  5. 更改 DemoString 属性的值。 提交更改时,Visual Studio 的调试实例将获得焦点并在断点处停止执行。 可以像处理任何其他代码一样单步执行属性访问器。

  6. 若要停止调试,请退出 Visual Studio 的托管实例,或选择调试实例中的“停止调试”按钮。

后续步骤

现在,你可以在设计时调试自定义控件,有多种方式可以扩展控件与 Visual Studio IDE 的交互。

  • 可以使用 Component 类的 DesignMode 属性来编写仅在设计时执行的代码。

  • 可以将多个特性应用于控件的属性,以操纵自定义控件与设计器的交互。 可以在 System.ComponentModel 命名空间中找到这些特性。

  • 可以为自定义控件编写自定义设计器。 这样就可以使用 Visual Studio 公开的可扩展设计器基础结构完全控制设计体验。

15、演练:创建利用设计时功能的控件

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

注意

此内容是为.NET Framework编写的。 如果使用的是 .NET 6 或更高版本,请谨慎使用此内容。

本文演示如何为自定义控件创建自定义设计器。 你会实现 MarqueeControl 类型和名为 MarqueeControlRootDesigner 的关联设计器类。

MarqueeControl 类型实现类似于剧院字幕的显示,带有动画灯和闪烁文本。

此控件的设计器与设计环境交互,以提供自定义设计时体验。 使用自定义设计器可以组装自定义 MarqueeControl 实现,带有多种组合的动画灯和闪烁文本。 可以与任何其他 Windows 窗体控件一样,组装的控件用于窗体。

完成本演练后,自定义控件将如下所示:

创建项目

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

Visual Studio 中创建新的 Windows 窗体应用程序项目,将其命名为 MarqueeControlTest

创建控件库项目

  1. Windows 窗体控件库项目添加到解决方案。 将项目命名为 MarqueeControlLibrary

  2. 使用“解决方案资源管理器”,根据所选语言删除名为“UserControl1.cs”的源文件,从而删除项目的默认控件。

  3. MarqueeControlLibrary 项目添加一个新 UserControl 项。 为新源文件提供基名称 MarqueeControl

  4. 使用“解决方案资源管理器”,在 MarqueeControlLibrary 项目中创建新文件夹。

  5. 右键单击“Design”文件夹并添加一个新类。 将其命名为 MarqueeControlRootDesigner

  6. 需要使用 System.Design 程序集中的类型,因此将此引用添加 MarqueeControlLibrary 项目。

引用自定义控件项目

你会使用 MarqueeControlTest 项目测试自定义控件。 添加对 MarqueeControlLibrary 程序集的项目引用后,测试项目会感知到自定义控件。

MarqueeControlTest 项目中,添加对 MarqueeControlLibrary 程序集的项目引用。 请务必在“添加引用”对话框中使用“项目”选项卡,而不是直接引用 MarqueeControlLibrary 程序集。

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

自定义控件会从 UserControl 类派生。 这使控件可以包含其他控件,并且可为控件提供了大量默认功能。

自定义控件会具有关联自定义设计器。 这使你可以创造专为自定义控件定制的独特设计体验。

使用 DesignerAttribute 类将控件与其设计器关联。 因为你在开发自定义控件的整个设计时行为,所以自定义设计器会实现 IRootDesigner 接口。

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

  1. 在“代码编辑器”中打开 MarqueeControl 源文件。 在该文件顶部导入以下命名空间:

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
  2. DesignerAttribute 添加到 MarqueeControl 类声明。 这会将自定义控件与其设计器关联。

    [Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )]
    public class MarqueeControl : UserControl
  3. 在“代码编辑器”中打开 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;
  4. 更改 MarqueeControlRootDesigner 的声明以从 DocumentDesigner 类继承。 应用 ToolboxItemFilterAttribute 以指定设计器与工具箱的交互。

    备注

    MarqueeControlRootDesigner 类的定义已包含在名为 MarqueeControlLibrary.Design 的命名空间中。 此声明将设计器置于为与设计相关的类型保留的特殊命名空间中。

    namespace MarqueeControlLibrary.Design
    {
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
        public class MarqueeControlRootDesigner : DocumentDesigner
        {
  5. 定义 MarqueeControlRootDesigner 类的构造函数。 在构造函数主体中插入 WriteLine 语句。 这对调试十分有用。

    public MarqueeControlRootDesigner()
    {
        Trace.WriteLine("MarqueeControlRootDesigner ctor");
    }

创建自定义控件的实例

  1. MarqueeControlTest 项目添加一个新 UserControl 项。 为新源文件提供基名称 DemoMarqueeControl

  2. 在“代码编辑器”中打开 DemoMarqueeControl 文件。 在该文件顶部导入 MarqueeControlLibrary 命名空间:

    using MarqueeControlLibrary;
  3. 更改 DemoMarqueeControl 的声明以从 MarqueeControl 类继承。

  4. 生成项目。

  5. Windows 窗体设计器中打开 Form1

  6. 在“工具箱”中找到“MarqueeControlTest 组件”选项卡,然后打开它。 将 DemoMarqueeControl 从“工具箱”拖到窗体上。

  7. 生成项目。

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

开发自定义设计时体验时,需要调试控件和组件。 可以通过一种简单方法设置项目,以便可以在设计时进行调试。

  1. 右键单击 MarqueeControlLibrary 项目,然后选择“属性”。

  2. 在“MarqueeControlLibrary 属性页”对话框中,选择“调试”页面。

  3. 在“启动操作”部分中,选择“启动外部程序”。 你会调试 Visual Studio 的单独实例,因此单击省略号按钮,以浏览 Visual Studio IDE。 可执行文件的名称为 devenv.exe,如果安装到默认位置,则其路径为 %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<edition>\Common7\IDE\devenv.exe

  4. 选择“确定”关闭对话框 。

  5. 右键单击 MarqueeControlLibrary 项目,然后选择“设为启动项目”以启用此调试配置。

检查点

你现在已准备好调试自定义控件的设计时行为。 确定调试环境设置正确后,便会测试自定义控件与自定义设计器之间的关联。

测试调试环境和设计器关联

  1. 在“代码编辑器”中打开 MarqueeControlRootDesigner 源文件,并在 WriteLine 语句上放置断点。

  2. F5 启动调试会话。

    会创建 Visual Studio 的新实例。

  3. Visual Studio 的新实例中,打开 MarqueeControlTest 解决方案。 可以通过从“文件”菜单中选择“最近使用的项目”来轻松找到解决方案。 MarqueeControlTest.sln 解决方案文件会列为最近使用的文件。

  4. 在设计器中打开 DemoMarqueeControl

    Visual Studio 的调试实例会获取焦点,执行会在断点处停止。 按 F5 继续调试会话。

此时,所有内容都已就位,可供你开发和调试自定义控件及其关联自定义设计器。 本文的其余部分集中讨论实现控件和设计器功能的详细信息。

实现自定义控件

MarqueeControl 是具有一点点自定义的 UserControl。 它公开两个方法:启动字幕动画的 Start,以及停止动画的 Stop。 因为 MarqueeControl 包含实现 IMarqueeWidget 接口的子控件,所以 StartStop 会枚举每个子控件并分别对实现 IMarqueeWidget 的每个子控件调用 StartMarqueeStopMarquee 方法。

MarqueeBorderMarqueeText 控件的外观取决于布局,因此 MarqueeControl 会替代 OnLayout 方法并对此类型的子控件调用 PerformLayout

这是 MarqueeControl 自定义的范围。 运行时功能由 MarqueeBorderMarqueeText 控件实现,设计时功能由 MarqueeBorderDesignerMarqueeControlRootDesigner 类实现。

实现自定义控件

  1. 在“代码编辑器”中打开 MarqueeControl 源文件。 实现 StartStop 方法。

    public void Start()
    {
        // MarqueeControl可以包含任意数量的实现IMarqueeWidget的控件,
        // 所以找到每个IMarqueeWidget子控件并调用它的StartMarquee方法。
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StartMarquee();
            }
        }
    }
    
    public void Stop()
    {
        // MarqueeControl可以包含任意数量的实现IMarqueeWidget的控件,
        // 所以找到每个IMarqueeWidget子控件并调用它的StopMarquee方法。
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StopMarquee();
            }
        }
    }
  2. 重写 OnLayout 方法。

    protected override void OnLayout(LayoutEventArgs levent)
    {
        base.OnLayout (levent);
    
        // 如果布局发生变化,请重新绘制所有IMarqueeWidget子组件。
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                Control control = cntrl as Control;
    
                control.PerformLayout();
            }
        }
    }

为自定义控件创建子控件

MarqueeControl 会承载两种类型的子控件:MarqueeBorder 控件和 MarqueeText 控件。

  • MarqueeBorder:此控件围绕其边缘绘制“灯”的边框。 灯按顺序闪烁,因此它们似乎在围绕边框移动。 灯闪烁的速度通过名为 UpdatePeriod 的属性进行控制。 其他几个自定义属性可确定控件外观的其他方面。 两个方法(名为 StartMarqueeStopMarquee)会控制动画开始和停止的时间。

  • MarqueeText:此控件绘制闪烁字符串。 与 MarqueeBorder 控件一样,文本闪烁的速度通过 UpdatePeriod 属性进行控制。 MarqueeText 控件还具有 StartMarqueeStopMarquee 方法(与 MarqueeBorder 控件相同)。

在设计时,MarqueeControlRootDesigner 允许将这两种控件类型采用任意组合添加到 MarqueeControl

两个控件的共同功能融入到名为 IMarqueeWidget 的接口中。 这使 MarqueeControl 可以发现任何与字幕相关的子控件,并对它们进行特殊处理。

若要实现定期动画功能,会使用 System.ComponentModel 命名空间中的 BackgroundWorker 对象。 可以使用 Timer 对象,但当存在许多 IMarqueeWidget 对象时,单个 UI 线程可能无法跟上动画。

为自定义控件创建子控件

  1. MarqueeControlLibrary 项目添加一个新类项。 为新源文件提供基名称“IMarqueeWidget”。

  2. 在“代码编辑器”中打开 IMarqueeWidget 源文件,并将声明从 class 更改为interface

    // 该接口定义了用于构造MarqueeControl的任何类的契约。
    public interface IMarqueeWidget
    {
  3. 将以下代码添加到 IMarqueeWidget 接口,以公开两个方法和一个操作字幕动画的属性:

        // 该接口定义了用于构造MarqueeControl的任何类的契约。
        public interface IMarqueeWidget
        {
            /// <summary>
            /// 这个方法开始动画。
            /// 如果控件可以包含其他实现IMarqueeWidget的子类,
            /// 那么控件应该在它所有的IMarqueeWidget子控件上调用StartMarquee。
            /// </summary>
            void StartMarquee();
    
            /// <summary>
            /// 这个方法停止动画。
            /// 如果控件可以包含其他将IMarqueeWidget实现为子控件的类,
            /// 则该控件应该在其所有IMarqueeWidget子控件上调用StopMarquee。
            /// </summary>
            void StopMarquee();
    
            /// <summary>
            /// 此方法指定动画的刷新率,以毫秒为单位。
            /// </summary>
            int UpdatePeriod
            {
                get;
                set;
            }
        }
  4. MarqueeControlLibrary 项目添加一个新“自定义控件”项。 为新源文件提供基名称“MarqueeText”。

  5. BackgroundWorker 组件从“工具箱”拖动到 MarqueeText 控件上。 此组件允许 MarqueeText 控件异步更新自身。

  6. 在“属性”窗口中,将 BackgroundWorker 组件的 WorkerReportsProgressWorkerSupportsCancellation 属性设置为 true。 这些设置允许 BackgroundWorker 组件定期引发 ProgressChanged 事件并取消异步更新。

  7. 在“代码编辑器”中打开 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;
  8. 更改 MarqueeText 的声明以从 Label 继承并实现 IMarqueeWidget 接口:

    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
    public partial class MarqueeText : Label, IMarqueeWidget
    {
  9. 声明与公开属性对应的实例变量,并在构造函数中初始化它们。 isLit 字段确定文本是否采用 LightColor 属性提供的颜色进行绘制。

            /// <summary>
            /// 当isLit为真时,文本被涂成浅色;
            /// 当isLit为假时,文本被涂成深色。
            /// 当BackgroundWorker组件引发ProgressChanged事件时,这个值就会改变。
            /// </summary>
            private bool isLit = true;
    
            // 这些字段支持公共属性。
            private int updatePeriodValue = 50;
            private Color lightColorValue;
            private Color darkColorValue;
    
            // 这些笔刷用于绘制文本的明暗颜色。
            private Brush lightBrush;
            private Brush darkBrush;
    
            // 该组件异步更新控件。
            private BackgroundWorker backgroundWorker1;
    
            public MarqueeText()
            {
                // 这个调用是Windows所要求的。
                InitializeComponent();
    
                // 将浅色和深色初始化为控件的默认值。
                this.lightColorValue = this.ForeColor;
                this.darkColorValue = this.BackColor;
                this.lightBrush = new SolidBrush(this.lightColorValue);
                this.darkBrush = new SolidBrush(this.darkColorValue);
            }
  10. 实现 IMarqueeWidget 接口。

    StartMarqueeStopMarquee 方法调用 BackgroundWorker 组件的 RunWorkerAsyncCancelAsync 方法以启动和停止动画。

    CategoryBrowsable 特性会应用于 UpdatePeriod 属性,使它出现在属性窗口名为“Marquee”的自定义部分中。

            public virtual void StartMarquee()
            {
                // 启动更新线程并向其传递UpdatePeriod。
                this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
            }
    
            public virtual void StopMarquee()
            {
                // 停止更新线程。
                this.backgroundWorker1.CancelAsync();
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public int UpdatePeriod
            {
                get
                {
                    return this.updatePeriodValue;
                }
    
                set
                {
                    if (value > 0)
                    {
                        this.updatePeriodValue = value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
                    }
                }
            }
  11. 实现属性访问器。 你会向客户端公开两个属性:LightColorDarkColorCategoryBrowsable 特性会应用于这些属性,使它们出现在属性窗口名为“Marquee”的自定义部分中。

           [Category("Marquee")]
            [Browsable(true)]
             public Color LightColor
            {
                get
                {
                    return this.lightColorValue;
                }
                set
                {
                    // 只有当客户端提供不同的值时,才会更改LightColor属性。
                    // 比较来自ToArgb方法的值是颜色结构之间相等性的推荐测试。
                    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
                {
                    // 只有当客户端提供不同的值时,才会更改DarkColor属性。
                    // 比较来自ToArgb方法的值是颜色结构之间相等性的推荐测试。
                    if (this.darkColorValue.ToArgb() != value.ToArgb())
                    {
                        this.darkColorValue = value;
                        this.darkBrush = new SolidBrush(value);
                    }
                }
            }
  12. BackgroundWorker 组件的 DoWorkProgressChanged 事件实现处理程序。

    DoWork 事件处理程序会按照 UpdatePeriod 指定的毫秒数进行休眠,然后引发 ProgressChanged 事件,直到代码通过调用 CancelAsync 来停止动画。

    ProgressChanged 事件处理程序会在浅色与深色状态之间切换文本,以呈现闪烁的外观。

            /// <summary>
            /// 这个方法是在工作线程的上下文中调用的,所以它不能调用MarqueeText控件。
            /// 相反,它使用ProgressChanged事件与控件通信。
            /// 在此事件处理程序中完成的唯一工作是休眠UpdatePeriod指定的毫秒数,然后引发ProgressChanged事件。
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void backgroundWorker1_DoWork(object sender,DoWorkEventArgs e)
            {
                BackgroundWorker worker = sender as BackgroundWorker;
    
                // 这个事件处理程序将一直运行,直到客户端通过调用CancelAsync来取消后台任务。
                while (!worker.CancellationPending)
                {
                    // DoWorkEventArgs对象的Argument属性保存UpdatePeriod的值,该值作为参数传递给RunWorkerAsync方法。
                    Thread.Sleep((int)e.Argument);
    
                    // DoWork事件处理程序实际上并不报告进度;ReportProgress事件用于定期提醒控件更新其状态。
                    worker.ReportProgress(0);
                }
            }
    
            /// <summary>
            /// ProgressChanged事件由DoWork方法引发。
            /// 此事件处理程序执行控件内部的工作。
            /// 在这种情况下,文本在其亮和暗状态之间切换,并告诉控件重新绘制自己。
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
            {
                this.isLit = !this.isLit;
                this.Refresh();
            }
  13. 替代 OnPaint 方法以启用动画。

    protected override void OnPaint(PaintEventArgs e)
    {
        // The text is painted in the light or dark color,
        // depending on the current value of isLit.
        this.ForeColor = this.isLit ? this.lightColorValue : this.darkColorValue;
    
        base.OnPaint(e);
    }
  14. F6 以生成解决方案。

创建 MarqueeBorder 子控件

MarqueeBorder 控件比 MarqueeText 控件稍微复杂一些。 它具有更多属性,并且 OnPaint 方法中的动画更加复杂。 原则上,它与 MarqueeText 控件非常相似。

由于 MarqueeBorder 控件可以具有子控件,因此需要注意 Layout 事件。

创建 MarqueeBorder 控件

  1. MarqueeControlLibrary 项目添加一个新“自定义控件”项。 为新源文件提供基名称“MarqueeBorder”。

  2. BackgroundWorker 组件从“工具箱”拖动到 MarqueeBorder 控件上。 此组件允许 MarqueeBorder 控件异步更新自身。

  3. 在“属性”窗口中,将 BackgroundWorker 组件的 WorkerReportsProgressWorkerSupportsCancellation 属性设置为 true。 这些设置允许 BackgroundWorker 组件定期引发 ProgressChanged 事件并取消异步更新。

  4. 在“属性”窗口中选择“事件”按钮。 为 DoWorkProgressChanged 事件附加处理程序。

  5. 在“代码编辑器”中打开 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;
  6. 更改 MarqueeBorder 的声明以从 Panel 继承并实现 IMarqueeWidget 接口。

    [Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))]
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
    public partial class MarqueeBorder : Panel, IMarqueeWidget
    {
  7. 声明用于管理 MarqueeBorder 控件状态的两个枚举:MarqueeSpinDirection用于确定灯围绕边框“旋转”的方向;以及 MarqueeLightShape,用于确定灯的形状(方形或圆形)。 将这些声明置于 MarqueeBorder 类声明之前。

        /// <summary>
        /// 这定义了MarqueeBorder控件的SpinDirection属性的可能值。
        /// </summary>
        public enum MarqueeSpinDirection
        {
            CW,
            CCW
        }
    
        /// <summary>
        /// 这定义了MarqueeBorder控件的LightShape属性的可能值。
        /// </summary>
        public enum MarqueeLightShape
        {
            Square,
            Circle
        }
  8. 声明与公开属性对应的实例变量,并在构造函数中初始化它们。

            public static int MaxLightSize = 10;
    
            // 这些字段支持公共属性。
            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;
    
            // 这些笔刷是用来画灯罩的明暗颜色的。
            private Brush lightBrush;
            private Brush darkBrush;
    
            // 这个场跟踪“first”光的进程,因为它“travels”在帐篷边界。
            private int currentOffset = 0;
    
            // 该组件异步更新控件。
            private System.ComponentModel.BackgroundWorker backgroundWorker1;
    
            public MarqueeBorder()
            {
                // 这个调用是Windows所要求的。窗体窗体设计器。
                InitializeComponent();
    
                // 将浅色和深色初始化为控件的默认值。
                this.lightColorValue = this.ForeColor;
                this.darkColorValue = this.BackColor;
                this.lightBrush = new SolidBrush(this.lightColorValue);
                this.darkBrush = new SolidBrush(this.darkColorValue);
    
                // marqueborder控件管理它自己的填充,因为它要求任何包含的控件都不能与任何marquee灯重叠。
                int pad = 2 * (this.lightSizeValue + this.lightSpacingValue);
                this.Padding = new Padding(pad, pad, pad, pad);
    
                SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            }
  9. 实现 IMarqueeWidget 接口。

    StartMarqueeStopMarquee 方法调用 BackgroundWorker 组件的 RunWorkerAsyncCancelAsync 方法以启动和停止动画。

    由于 MarqueeBorder 控件可以包含子控件,因此 StartMarquee 方法会枚举所有子控件并对实现 IMarqueeWidget 的子控件调用 StartMarqueeStopMarquee 方法具有类似的实现。

            public virtual void StartMarquee()
            {
                // MarqueeBorder控件可以包含任意数量的实现IMarqueeWidget的控件,
                // 所以找到每个IMarqueeWidget子控件并调用它的StartMarquee方法。
                foreach (Control cntrl in this.Controls)
                {
                    if (cntrl is IMarqueeWidget)
                    {
                        IMarqueeWidget widget = cntrl as IMarqueeWidget;
                        widget.StartMarquee();
                    }
                }
    
                // 启动更新线程并向其传递UpdatePeriod。
                this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
            }
    
            public virtual void StopMarquee()
            {
                // MarqueeBorder控件可以包含任意数量的实现IMarqueeWidget的控件,
                // 因此找到每个IMarqueeWidget子控件并调用其StopMarquee方法。
                foreach (Control cntrl in this.Controls)
                {
                    if (cntrl is IMarqueeWidget)
                    {
                        IMarqueeWidget widget = cntrl as IMarqueeWidget;
                        widget.StopMarquee();
                    }
                }
    
                // 停止更新线程。
                this.backgroundWorker1.CancelAsync();
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public virtual int UpdatePeriod
            {
                get
                {
                    return this.updatePeriodValue;
                }
    
                set
                {
                    if (value > 0)
                    {
                        this.updatePeriodValue = value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
                    }
                }
            }
  10. 实现属性访问器。 MarqueeBorder 控件具有多个用于控制其外观的属性。

            [Category("Marquee")]
            [Browsable(true)]
            public 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
                {
                    // 只有当客户端提供不同的值时,才会更改LightColor属性。
                    // 比较来自ToArgb方法的值是颜色结构之间相等性的推荐测试。
                    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
                {
                    // 只有当客户端提供不同的值时,才会更改DarkColor属性。
                    // 比较来自ToArgb方法的值是颜色结构之间相等性的推荐测试。
                    if (this.darkColorValue.ToArgb() != value.ToArgb())
                    {
                        this.darkColorValue = value;
                        this.darkBrush = new SolidBrush(value);
                    }
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public int LightSpacing
            {
                get
                {
                    return this.lightSpacingValue;
                }
    
                set
                {
                    if (value >= 0)
                    {
                        this.lightSpacingValue = value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0");
                    }
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            [EditorAttribute(typeof(LightShapeEditor),
                 typeof(System.Drawing.Design.UITypeEditor))]
            public MarqueeLightShape LightShape
            {
                get
                {
                    return this.lightShapeValue;
                }
    
                set
                {
                    this.lightShapeValue = value;
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public MarqueeSpinDirection SpinDirection
            {
                get
                {
                    return this.spinDirectionValue;
                }
    
                set
                {
                    this.spinDirectionValue = value;
                }
            }
  11. BackgroundWorker 组件的 DoWorkProgressChanged 事件实现处理程序。

    DoWork 事件处理程序会按照 UpdatePeriod 指定的毫秒数进行休眠,然后引发 ProgressChanged 事件,直到代码通过调用 CancelAsync 来停止动画。

    ProgressChanged 事件处理程序会递增“基本”灯(可用于确定其他灯的浅色/深色状态)的位置,并调用 Refresh 方法来使控件重新绘制自身。

            /// <summary>
            /// 这个方法是在工作线程的上下文中调用的,所以它不能对marqueborder控件进行任何调用。
            /// 相反,它使用ProgressChanged事件与控件通信。
            /// 在此事件处理程序中完成的唯一工作是休眠UpdatePeriod指定的毫秒数,然后引发ProgressChanged事件。
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
            {
                BackgroundWorker worker = sender as BackgroundWorker;
    
                // 这个事件处理程序将一直运行,直到客户端通过调用CancelAsync来取消后台任务。
                while (!worker.CancellationPending)
                {
                    // DoWorkEventArgs对象的Argument属性保存UpdatePeriod的值,该值作为参数传递给RunWorkerAsync方法。
                    Thread.Sleep((int)e.Argument);
    
                    // DoWork事件处理程序实际上并不报告进度;ReportProgress事件用于定期提醒控件更新其状态。
                    worker.ReportProgress(0);
                }
            }
    
            /// <summary>
            /// ProgressChanged事件由DoWork方法引发。
            /// 此事件处理程序执行控件内部的工作。
            /// 在这种情况下,currentOffset是递增的,并且控件被告知重新绘制自己。
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
            {
                this.currentOffset++;
                this.Refresh();
            }
  12. 实现帮助程序方法 IsLitDrawLight

    IsLit 方法可确定给定位置处灯的颜色。 “浅色”的灯会按照 LightColor 属性给定的颜色进行绘制,而“深色”的灯会按照 DarkColor 属性给定的颜色进行绘制。

    DrawLight 方法会使用适当的颜色、形状和位置绘制灯。

            /// <summary>
            /// 这个方法决定了在lightIndex处的marquee light是否应该被点亮。
            /// currentOffset字段指定了“第一个”光源的位置,lightIndex给出的光源的“位置”是相对于这个偏移量计算的。
            /// 如果这个位置对lightPeriodValue取模为零,则认为灯亮着,并且将使用控件的lightBrush对其进行绘制。
            /// </summary>
            /// <param name="lightIndex"></param>
            /// <returns></returns>
            protected virtual bool IsLit(int lightIndex)
            {
                int directionFactor =
                    (this.spinDirectionValue == MarqueeSpinDirection.CW ? -1 : 1);
    
                return (
                    (lightIndex + directionFactor * this.currentOffset) % this.lightPeriodValue == 0
                    );
            }
    
            protected virtual void DrawLight(
                Graphics g,
                Brush brush,
                int xPos,
                int yPos)
            {
                switch (this.lightShapeValue)
                {
                    case MarqueeLightShape.Square:
                        {
                            g.FillRectangle(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue);
                            break;
                        }
                    case MarqueeLightShape.Circle:
                        {
                            g.FillEllipse(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue);
                            break;
                        }
                    default:
                        {
                            Trace.Assert(false, "Unknown value for light shape.");
                            break;
                        }
                }
            }
  13. 替代 OnLayoutOnPaint 方法。

    OnPaint 方法沿 MarqueeBorder 控件边缘绘制灯。

    由于 OnPaint 方法依赖于 MarqueeBorder 控件的尺寸,因此每当布局发生更改时,都需要调用它。 若要实现此目的,请替代 OnLayout 并调用 Refresh

            protected override void OnLayout(LayoutEventArgs levent)
            {
                base.OnLayout(levent);
    
                // 当布局改变时重新绘制。
                this.Refresh();
            }
    
            /// <summary>
            /// 此方法在控件的边界周围绘制灯光。
            /// 它首先绘制顶部行,然后是右侧,底部和左侧。
            /// 每个光的颜色由IsLit方法确定,并取决于光相对于currentOffset值的位置。
            /// </summary>
            /// <param name="e"></param>
            protected override void OnPaint(PaintEventArgs e)
            {
                Graphics g = e.Graphics;
                g.Clear(this.BackColor);
    
                base.OnPaint(e);
    
                // 如果控件足够大,则绘制一些灯光。
                if (this.Width > MaxLightSize && this.Height > MaxLightSize)
                {
                    // 下一盏灯的位置将增加这个值,这个值等于灯的大小和两盏灯之间的空间之和。
                    int increment = this.lightSizeValue + this.lightSpacingValue;
    
                    // 计算要沿着控件的水平边缘绘制的灯的数量。
                    int horizontalLights = (this.Width - increment) / increment;
    
                    // 计算要沿着控件的垂直边缘绘制的灯的数量。
                    int verticalLights = (this.Height - increment) / increment;
    
                    // 这些局部变量将用于定位和绘制每个灯。
                    int xPos = 0;
                    int yPos = 0;
                    int lightCounter = 0;
                    Brush brush;
    
                    // 画最上面一排灯。
                    for (int i = 0; i < horizontalLights; i++)
                    {
                        brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                        DrawLight(g, brush, xPos, yPos);
    
                        xPos += increment;
                        lightCounter++;
                    }
    
                    // 绘制与控件右边缘齐平的灯光。
                    xPos = this.Width - this.lightSizeValue;
    
                    // 画出右边的灯柱。
                    for (int i = 0; i < verticalLights; i++)
                    {
                        brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                        DrawLight(g, brush, xPos, yPos);
    
                        yPos += increment;
                        lightCounter++;
                    }
    
                    // 绘制与控件底部边缘齐平的灯。
                    yPos = this.Height - this.lightSizeValue;
    
                    // 画下一排灯。
                    for (int i = 0; i < horizontalLights; i++)
                    {
                        brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                        DrawLight(g, brush, xPos, yPos);
    
                        xPos -= increment;
                        lightCounter++;
                    }
    
                    // 绘制与控件左边缘齐平的灯。
                    xPos = 0;
    
                    // 绘制左边的灯柱。
                    for (int i = 0; i < verticalLights; i++)
                    {
                        brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                        DrawLight(g, brush, xPos, yPos);
    
                        yPos -= increment;
                        lightCounter++;
                    }
                }
            }

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

MarqueeControlRootDesigner 类提供根设计器的实现。 除了对 MarqueeControl 控件进行操作的此设计器之外,还需要一个专门与 MarqueeBorder 控件关联的自定义设计器。 此设计器提供适用于自定义根设计器上下文的自定义行为。

具体而言,MarqueeBorderDesigner 会“隐藏”并筛选控件 MarqueeBorder 上的某些属性,从而更改它们与设计环境的交互。

截获对组件属性访问器的调用称为“隐藏”。它使设计器可以跟踪用户设置的值,并选择性地将该值传递给进行设计的组件。

对于此示例,VisibleEnabled 属性会由 MarqueeBorderDesigner 隐藏,这可防止用户在设计期间使 MarqueeBorder 控件不可见或禁用。

设计器还可以添加和移除属性。 对于此示例,Padding 属性会在设计时移除,因为 MarqueeBorder 控件以编程方式基于 LightSize 属性指定的灯大小设置填充。

MarqueeBorderDesigner 的基类是 ComponentDesigner,它具有可以在设计时更改控件公开的特性、属性和事件的方法:

  • PreFilterProperties

  • PostFilterProperties

  • PreFilterAttributes

  • PostFilterAttributes

  • PreFilterEvents

  • PostFilterEvents

使用这些方法更改组件的公共接口时,请遵循以下规则:

  • 仅在 PreFilter 方法中添加或移除项

  • 仅在 PostFilter 方法中的修改现有项

  • 始终先在 PreFilter 方法中调用基实现

  • 始终最后在 PostFilter 方法中调用基实现

遵循这些规则可确保设计时环境中的所有设计器都具有进行设计的所有组件的一致视图。

ComponentDesigner 类提供一个字典,用于管理隐藏属性的值,这可减少创建特定实例变量的需要。

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

  1. 右键单击“Design”文件夹并添加一个新类。 为源文件提供基名称“MarqueeBorderDesigner”。

  2. 在“代码编辑器”中打开 MarqueeBorderDesigner 源文件。 在该文件顶部导入以下命名空间:

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
  3. 更改 MarqueeBorderDesigner 的声明以从 ParentControlDesigner 继承。

    由于 MarqueeBorder 控件可以包含子控件,因此 MarqueeBorderDesigner 继承自 ParentControlDesigner,后者可处理父子交互。

    namespace MarqueeControlLibrary.Design
    {
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
  4. 替代 PreFilterProperties 的基实现。

    protected override void PreFilterProperties(IDictionary properties)
    {
        base.PreFilterProperties(properties);
    
        if (properties.Contains("Padding"))
        {
            properties.Remove("Padding");
        }
    
        properties["Visible"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Visible"],
            new Attribute[0]);
    
        properties["Enabled"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Enabled"],
            new Attribute[0]);
    }
  5. 实现 EnabledVisible 属性。 这些实现会隐藏控件的属性。

    public bool Visible
    {
        get
        {
            return (bool)ShadowProperties["Visible"];
        }
        set
        {
            this.ShadowProperties["Visible"] = value;
        }
    }
    
    public bool Enabled
    {
        get
        {
            return (bool)ShadowProperties["Enabled"];
        }
        set
        {
            this.ShadowProperties["Enabled"] = value;
        }
    }

处理组件更改

MarqueeControlRootDesigner 类为 MarqueeControl 实例提供自定义设计时体验。 大多数设计时功能继承自 DocumentDesigner 类。 代码会实现两个特定自定义:处理组件更改和添加设计器谓词。

当用户设计其 MarqueeControl 实例时,根设计器会跟踪对 MarqueeControl 及其子控件的更改。 设计时环境提供了一种方便服务 IComponentChangeService,用于跟踪对组件状态的更改。

可通过使用 GetService 方法查询环境来获取对此服务的引用。 如果查询成功,设计器可以附加 ComponentChanged 事件的处理程序,并执行在设计时维护一致状态所需的任何任务。

对于 MarqueeControlRootDesigner 类,你会对 MarqueeControl 包含的每个 IMarqueeWidget 对象调用 Refresh 方法。 这会使 IMarqueeWidget 对象在其父级的 Size 等属性更改时相应地重新绘制自身。

处理组件更改

  1. 在“代码编辑器”中打开 MarqueeControlRootDesigner 源文件,然后替代 Initialize 方法。 调用 Initialize 的基实现,并查询 IComponentChangeService

    base.Initialize(component);
    
    IComponentChangeService cs = GetService(typeof(IComponentChangeService)) as IComponentChangeService;
    
    if (cs != null)
    {
        cs.ComponentChanged += new ComponentChangedEventHandler(OnComponentChanged);
    }
  2. 实现 OnComponentChanged 事件处理程序。 测试发送组件的类型,如果是 IMarqueeWidget,则调用其 Refresh 方法。

    private void OnComponentChanged(
        object sender,
        ComponentChangedEventArgs e)
    {
        if (e.Component is IMarqueeWidget)
        {
            this.Control.Refresh();
        }
    }

向自定义设计器添加设计器谓词

设计器谓词是与事件处理程序链接的菜单命令。 设计器谓词会在设计时添加到组件的快捷菜单中。

你会向设计器添加两个设计器谓词:“Run Test”和“Stop Test”。 这些谓词使你可以在设计时查看 MarqueeControl 的运行时行为。 这些谓词会添加到 MarqueeControlRootDesigner 中。

调用“Run Test”时,谓词事件处理程序会对 MarqueeControl 调用 StartMarquee 方法。 调用“Stop Test”时,谓词事件处理程序会对 MarqueeControl 调用 StopMarquee 方法。 StartMarqueeStopMarquee 方法的实现会对实现 IMarqueeWidget 的包含控件调用这些方法,因此任何包含 IMarqueeWidget 的控件也会参与测试。

向自定义设计器添加设计器谓词

  1. MarqueeControlRootDesigner 类中,添加名为 OnVerbRunTestOnVerbStopTest 的事件处理程序。

    private void OnVerbRunTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Start();
    }
    
    private void OnVerbStopTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Stop();
    }
  2. 将这些事件处理程序连接到对应的设计器谓词。 MarqueeControlRootDesigner 从其基类继承 DesignerVerbCollection。 你会创建两个新 DesignerVerb 对象,并在 Initialize 方法中将它们添加到此集合中。

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

创建自定义 UITypeEditor

为用户创建自定义设计时体验时,通常需要创建与属性窗口的自定义交互。 可以通过创建 UITypeEditor 来实现此目的。

MarqueeBorder 控件在属性窗口中公开多个属性。 其中两个属性 MarqueeSpinDirectionMarqueeLightShape 由枚举表示。 为了说明 UI 类型编辑器的用法,MarqueeLightShape 属性会具有关联 UITypeEditor 类。

创建自定义 UI 类型编辑器

  1. 在“代码编辑器”中打开 MarqueeBorder 源文件。

  2. MarqueeBorder 类的定义中,声明名为 LightShapeEditor 的派生自 UITypeEditor 的类。

        /// <summary>
        /// 这个类演示了自定义UITypeEditor的使用。
        /// 它允许在设计时使用自定义UI元素更改marqueborder控件的LightShape属性,
        /// 该元素由属性窗口调用。UI是由LightShapeSelectionControl类提供的。
        /// </summary>
        internal class LightShapeEditor : UITypeEditor
        {
        }
  3. 声明名为 editorServiceIWindowsFormsEditorService 实例变量。

    private IWindowsFormsEditorService editorService = null;
  4. 重写 GetEditStyle 方法。 此实现返回 DropDown,它会告知设计环境如何显示 LightShapeEditor

    public override UITypeEditorEditStyle GetEditStyle(
    System.ComponentModel.ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
  5. 重写 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;
    }

为自定义 UITypeEditor 创建视图控件

MarqueeLightShape 属性支持两种类型的灯形状:SquareCircle。 你会创建一个仅用于在属性窗口中以图形方式显示这些值的自定义控件。 此自定义控件会由 UITypeEditor 用于与属性窗口交互。

为自定义 UI 类型编辑器创建视图控件

  1. MarqueeControlLibrary 项目添加一个新 UserControl 项。 为新源文件提供基名称 LightShapeSelectionControl

  2. 从“工具箱”将两个 Panel 控件拖动到 LightShapeSelectionControl 上。 将它们分别命名为 squarePanelcirclePanel。 并排排列它们。 将两个 Panel 控件的 Size 属性设置为 (60, 60)。 将 Location 控件的 squarePanel 属性设置为 (8, 10)。 将 Location 控件的 circlePanel 属性设置为 (80, 10)。 最后,将 LightShapeSelectionControlSize 属性设置为 (150, 80)。

  3. 在“代码编辑器”中打开 LightShapeSelectionControl 源文件。 在该文件顶部导入 System.Windows.Forms.Design 命名空间:

    using System.Windows.Forms.Design;
  4. squarePanelcirclePanel 控件实现 Click 事件处理程序。 这些方法调用 CloseDropDown 以结束自定义 UITypeEditor 编辑会话。

    private void squarePanel_Click(object sender, EventArgs e)
    {
        this.lightShapeValue = MarqueeLightShape.Square;
        
        this.Invalidate( false );
    
        this.editorService.CloseDropDown();
    }
    
    private void circlePanel_Click(object sender, EventArgs e)
    {
        this.lightShapeValue = MarqueeLightShape.Circle;
    
        this.Invalidate( false );
    
        this.editorService.CloseDropDown();
    }
  5. 声明名为 editorServiceIWindowsFormsEditorService 实例变量。

    private IWindowsFormsEditorService editorService;
  6. 声明名为 lightShapeValueMarqueeLightShape 实例变量。

    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
  7. LightShapeSelectionControl 构造函数中,将 Click 事件处理程序附加到 squarePanelcirclePanel 控件的 Click 事件。 此外,定义一个构造函数重载,该重载从设计环境中将 MarqueeLightShape 值分配给 lightShapeValue 字段。

            /// <summary>
            /// 这个构造函数从设计时环境中获取一个MarqueeLightShape值,该值将用于显示初始状态。
            /// </summary>
            /// <param name="lightShape"></param>
            /// <param name="editorService"></param>
            public LightShapeSelectionControl(
                MarqueeLightShape lightShape,
                IWindowsFormsEditorService editorService)
            {
                // 这个调用是设计者所要求的。
                InitializeComponent();
    
                // 缓存由设计时环境提供的光形状值。
                this.lightShapeValue = lightShape;
    
                // 缓存对编辑器服务的引用。
                this.editorService = editorService;
    
                // 处理两个面板的Click事件。
                this.squarePanel.Click += new EventHandler(squarePanel_Click);
                this.circlePanel.Click += new EventHandler(circlePanel_Click);
            }
  8. Dispose 方法中,拆离 Click 事件处理程序。

    protected override void Dispose( bool disposing )
    {
        if( disposing )
        {
            // Be sure to unhook event handlers
            // to prevent "lapsed listener" leaks.
            this.squarePanel.Click -=
                new EventHandler(squarePanel_Click);
            this.circlePanel.Click -=
                new EventHandler(circlePanel_Click);
    
            if(components != null)
            {
                components.Dispose();
            }
        }
        base.Dispose( disposing );
    }
  9. 在“解决方案资源管理器”中,单击“显示所有文件”按钮。 打开 LightShapeSelectionControl.Designer.cs 文件,并移除 Dispose 方法的默认定义。

  10. 实现 LightShape 属性。

            // LightShape是该控件在“属性”窗口中为其提供自定义用户界面的属性。
            public MarqueeLightShape LightShape
            {
                get
                {
                    return this.lightShapeValue;
                }
    
                set
                {
                    if (this.lightShapeValue != value)
                    {
                        this.lightShapeValue = value;
                    }
                }
            }
  11. 重写 OnPaint 方法。 此实现会绘制填充的方形和圆形。 它还会通过围绕一种形状或另一种形状绘制边框来突出显示所选值。

            protected override void OnPaint(PaintEventArgs e)
            {
                base.OnPaint(e);
    
                using (
                    Graphics gSquare = this.squarePanel.CreateGraphics(),
                    gCircle = this.circlePanel.CreateGraphics())
                {
                    // 在squarePanel控件的客户端区域绘制一个填充的正方形。
                    gSquare.FillRectangle(
                        Brushes.Red,
                        0,
                        0,
                        this.squarePanel.Width,
                        this.squarePanel.Height
                        );
    
                    // 如果选择了Square选项,则在squarePanel内部绘制边框。
                    if (this.lightShapeValue == MarqueeLightShape.Square)
                    {
                        gSquare.DrawRectangle(
                            Pens.Black,
                            0,
                            0,
                            this.squarePanel.Width - 1,
                            this.squarePanel.Height - 1);
                    }
    
                    // 在circlePanel控件的客户端区域绘制一个填充的圆。
                    gCircle.Clear(this.circlePanel.BackColor);
                    gCircle.FillEllipse(
                        Brushes.Blue,
                        0,
                        0,
                        this.circlePanel.Width,
                        this.circlePanel.Height
                        );
    
                    // 如果选择了Circle选项,则在circlePanel内部绘制边框。
                    if (this.lightShapeValue == MarqueeLightShape.Circle)
                    {
                        gCircle.DrawRectangle(
                            Pens.Black,
                            0,
                            0,
                            this.circlePanel.Width - 1,
                            this.circlePanel.Height - 1);
                    }
                }	
    		}

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

此时,可以生成 MarqueeControlLibrary 项目。 通过创建从 MarqueeControl 类继承的控件并在窗体中使用它来测试实现。

创建自定义 MarqueeControl 实现

  1. 在 Windows 窗体设计器中打开 DemoMarqueeControl。 这会创建 DemoMarqueeControl 类型的实例,并在 MarqueeControlRootDesigner 类型的实例中显示它。

  2. 在“工具箱”中,打开“MarqueeControlLibrary 组件”选项卡。你会看到可用于选择的 MarqueeBorderMarqueeText 控件。

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

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

  5. 生成解决方案。

  6. 右键单击 DemoMarqueeControl,并从快捷菜单中选择“Run Test”选项以启动动画。 单击“Stop Test”以停止动画。

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

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

  9. 为两个 Button 控件实现 Click 事件处理程序。

  10. 在“工具箱”中,打开“MarqueeControlTest 组件”选项卡。你会看到可用于选择的 DemoMarqueeControl 控件。

  11. DemoMarqueeControl 的实例拖动到“Form1”设计图面上。

  12. Click 事件处理程序中,对 DemoMarqueeControl 调用 StartStop 方法。

    private void startButton_Click(object sender, System.EventArgs e)
    {
        this.demoMarqueeControl1.Start();
    }
    
    private void stopButton_Click(object sender, System.EventArgs e)
    {
        this.demoMarqueeControl1.Stop();
    }
  13. MarqueeControlTest 项目设置为启动项目并运行它。 你会看到窗体显示你的 DemoMarqueeControl。 选择“启动”按钮启动动画。 你应该会看到文本闪烁,并且灯围绕边框移动。

后续步骤

MarqueeControlLibrary 演示自定义控件和关联设计器的简单实现。 可以通过多种方式使此示例更加复杂:

  • 在设计器中更改 DemoMarqueeControl 的属性值。 添加更多 MarqueBorder 控件并将其停靠在其父实例中,以创建嵌套效果。 对 UpdatePeriod 以及与灯相关的属性尝试不同的设置。

  • 创作自己的 IMarqueeWidget 实现。 例如,可以创建闪烁的“霓虹灯”或具有多个图像的动画符号。

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

  • 许可 MarqueeControl

  • 控制控件的序列化方式以及为其生成代码的方式。

16、Windows 窗体设计器错误页

如果 Windows 窗体设计器由于代码、第三方组件或其他位置的错误而未能加载,你会看到错误页而不是设计器。 此错误页不一定表示设计器中的 bugbug 可能位于名为 <你的窗体名称>.Designer.cs 的代码隐藏页面中的某个位置。 错误显示在可折叠的黄色栏中,其中带有用于跳转到代码页上错误位置的链接。

可以通过单击“忽略并继续”,来选择忽略错误并继续加载设计器。 此操作可能会导致意外行为,例如,控件可能不会显示在设计图面上。

此错误的实例

黄色错误栏展开后,会列出错误的每个实例。 许多错误采用以下格式包含确切位置:项目名称 行:[行号] 列:[列号]。 如果调用堆栈与错误关联,则可单击“显示调用堆栈”链接以进行查看。 检查调用堆栈可能会进一步帮助你解决错误。

设计时错误

此部分列出了可能会遇到的一些错误。

<标识符名称> 不是有效标识符

此错误指示字段、方法、事件或对象未正确命名。

“<名称>”已存在于“<项目名称>”中

错误消息:“‘<名称>’已存在于‘<项目名称>’中。 请输入唯一的名称。”

你为项目中已存在的继承窗体指定了名称。 若要更正此错误,请为继承窗体提供唯一名称。

“<工具箱选项卡名称>”不是工具箱类别

第三方设计器尝试访问工具箱上不存在的选项卡。 请联系组件供应商。

请求的语言分析器未安装

错误消息:“请求的语言分析器未安装。 语言分析器名称为‘{0}’。”

Visual Studio 尝试加载为文件类型注册的设计器,但无法加载。 这很可能是由于安装过程中发生了错误。 请联系你用于修补程序的语言的供应商。

缺少生成和分析源代码所需的服务

这是第三方组件的问题。 请联系组件供应商。

尝试创建“<对象名称>”的实例时发生异常

错误消息:“尝试创建‘<对象名称>’的实例时发生异常。 异常为‘<异常字符串>’”。

第三方设计器请求 Visual Studio 创建对象,但对象引发错误。 请联系组件供应商。

另一个编辑器以不兼容的模式打开了“<文档名称>”

错误消息:“另一个编辑器以不兼容的模式打开了‘<文档名称>’。 请关闭该编辑器,然后重试此操作。”

如果尝试打开已在另一个编辑器中打开的文件,则会出现此错误。 会显示已打开文件的编辑器。 若要更正此错误,请关闭打开文件的编辑器,然后重试。

另一个编辑器对“<文档名称>”进行了更改

关闭设计器,然后重新打开才能使更改生效。 通常,Visual Studio 会在进行更改后自动重新加载设计器。 但是,其他设计器(如第三方组件设计器)可能不支持重新加载行为。 在这种情况下,Visual Studio 会提示你手动关闭并重新打开设计器。

另一个编辑器以不兼容的模式打开了此文件

错误消息:“另一个编辑器以不兼容模式的打开了此文件。 请关闭该编辑器,然后重试此操作。”

此消息类似于“另一个编辑器以不兼容的模式打开了‘<文档名称>’”,但 Visual Studio 无法确定文件名。 若要更正此错误,请关闭打开文件的编辑器,然后重试。

数组秩“<数组中的秩>”过高

在设计器分析的代码块中,Visual Studio 仅支持单维数组。 多维数组在此区域之外有效。

无法打开程序集“<程序集名称>”

错误消息:“无法打开程序集‘<程序集名称>’。 请验证该文件是否仍存在。”

尝试打开无法打开的文件时,会出现此错误消息。 请验证该文件存在并且是有效程序集。

错误的元素类型。 此序列化程序要求元素类型为“<类型名称>”

这是第三方组件的问题。 请联系组件供应商。

此时无法访问 Visual Studio 工具箱

Visual Studio 在工具箱不可用时对其进行了调用。

“<事件名称>”事件是只读的,因此无法将事件处理程序绑定到该事件

尝试将事件连接到从基类继承的控件时,通常会出现此错误。 如果控件的成员变量是私有变量,则 Visual Studio 无法将事件连接到方法。 私有继承控件不能绑定其他事件。

请求的组件不是设计容器的成员,因此无法创建该组件的方法名

Visual Studio 尝试将事件处理程序添加到设计器中没有成员变量的组件。 请联系组件供应商。

对象“<名称>”已命名为“<名称>”,因此无法命名该对象

这是 Visual Studio 序列化程序中的内部错误。 它指示序列化程序尝试对一个对象命名两次,此操作不受支持。

无法移除或损坏继承的组件“<组件名称>”

继承的控件由其继承类所拥有。 必须在控件的起源类中更改继承的控件。 因而无法重命名或销毁它。

类别“<工具箱选项卡名称>”没有类“<>类名”的工具

设计器尝试引用特定工具箱选项卡上的类,但该类不存在。 请联系组件供应商。

类“<类名>”没有匹配的构造函数

第三方设计器要求 Visual Studio 在不存在的构造函数中创建具有特定参数的对象。 请联系组件供应商。

属性“<属性名称>”的代码生成失败

这是错误的泛型包装器。 此消息附带的错误字符串会提供有关错误消息的更多详细信息,并提供指向更具体帮助主题的链接。 若要更正此错误,请解决追加到此错误的错误消息中指定的错误。

组件“<组件名称>”未在其构造函数中调用 container.Add()

这是刚刚在窗体上加载或放置的组件中的错误。 它指示组件未将自己添加到其容器控件(无论是其他控件还是窗体)。 设计器会继续工作,但组件在运行时可能会出现问题。

若要更正错误,请联系组件供应商。 或者,如果是创建的组件,请在组件构造函数中调用 IContainer.Add 方法。

组件名称不能为空

尝试将组件重命名为空值时,会出现此错误。

未能访问变量“<变量名称>”,因为它尚未初始化

此错误可能是由于两种情形导致的。 第三方组件供应商分发的控件或组件有问题,或者你编写的代码在组件之间具有递归依赖项。

若要更正此错误,请确保代码没有递归依赖项。 如果不存在此类问题,请记下错误消息的确切文本并联系组件供应商。

找不到类型“<类型名称>”

错误消息:“找不到类型‘<类型名称>’。 请确保已引用包含此类型的程序集。 如果此类型为开发项目的一部分,请确保已成功生成该项目。”

发生此错误是因为找不到引用。 请确保引用错误消息中指示的类型,并且还引用了该类型所需的所有程序集。 通常,问题是解决方案中的控件未生成。 若要生成,请从“生成”菜单中选择“生成解决方案”。 否则,如果控件已生成,请从解决方案资源管理器中“引用”或“依赖项”文件夹的右键单击菜单中手动添加引用。

无法加载类型“<类型名称>”

错误消息:“无法加载类型‘<类型名称>’。 请确保将包含此类型的程序集添加到项目引用中。”

Visual Studio 尝试关联事件处理方法,但找不到该方法的一个或多个参数类型。 这通常是由于缺少引用导致的。 若要更正此错误,请将包含类型的引用添加到项目,然后重试。

未能找到继承的组件的项目项模板

Visual Studio 中继承表单的模板不可用。

委托类“<类名>”没有 invoke 方法。 此类是否为委托

Visual Studio 尝试创建事件处理程序,但事件类型有问题。 如果事件是通过不符合 CLS 的语言所创建,则可能会发生这种情况。 请联系组件供应商。

成员“<成员名称>”的重复声明

出现此错误的原因是成员变量已声明两次(例如,在代码中声明了两个名为 Button1 的控件)。 名称必须在继承表单间唯一。 此外,名称不能仅通过大小写来区分。

从区域性“<区域性名称>”的资源文件中读取资源时出错

如果项目中存在错误的 .resx 文件,则可能会发生此错误。

若要更正该错误,请执行以下操作:

  1. 单击解决方案资源管理器中的“显示所有文件”按钮以查看与解决方案关联的 .resx 文件。

  2. 通过右键单击 .resx 文件并选择“打开”,在 XML 编辑器中加载 .resx 文件。

  3. 手动编辑 .resx 文件以解决错误。

从默认区域性“<区域性名称>”的资源文件中读取资源时出错

如果对于默认区域性,项目中存在错误的 .resx 文件,则可能会发生此错误。

若要更正该错误,请执行以下操作:

  1. 单击解决方案资源管理器中的“显示所有文件”按钮以查看与解决方案关联的 .resx 文件。

  2. 通过右键单击 .resx 文件并选择“打开”,在 XML 编辑器中加载 .resx 文件。

  3. 手动编辑 .resx 文件以解决错误。

未能分析方法“<方法名称>”

错误消息:“未能分析方法‘<方法名称>’。 分析器报告以下错误:‘<错误字符串>’。 请查看任务列表以了解潜在的错误。”

这是针对分析过程中出现的问题的常规错误消息。 这些错误通常是由于语法错误导致的。 有关与错误相关的特定消息,请参阅任务列表。

无效的组件名称:“<组件名称>”

你尝试将组件重命名为对该语言无效的值。 若要更正此错误,请命名组件,使其符合该语言的命名规则。

类型“<类名>”由同一文件中的几个分部类构成

使用 partial 关键字在多个文件中定义类时,在每个文件中只能有一个分部定义。

若要更正此错误,请从文件中移除类的所有分部定义(只保留一个)。

未能找到程序集“<程序集名称>”

错误消息:“未能找到程序集‘<程序集名称>’。 请确保引用了该程序集。 如果该程序集是当前开发项目的一部分,请确保已生成了该项目。”

此错误类似于“找不到类型‘<类型名称>’”,但发生此错误通常是由于元数据属性。 若要更正此错误,请检查是否引用了属性使用的所有程序集。

程序集名称“<程序集名称>”无效

组件请求了特定程序集,但组件提供的名称不是有效程序集名称。 请联系组件供应商。

无法设计基类“<类名>”

Visual Studio 加载了类,但无法设计类,因为类的实现者未提供设计器。 如果类支持设计器,请确保不存在会导致设计器中的显示问题的问题(例如编译器错误)。 此外,请确保对类的所有引用都正确且所有类名都拼写正确。 否则,如果类不可设计,请在代码视图中进行编辑。

未能加载基类“<类名>”

项目中未引用类,因此 Visual Studio 无法加载它。 若要更正此错误,请在项目中添加对类的引用,然后关闭并重新打开 Windows 窗体设计器窗口。

不能在此版本的 Visual Studio 中设计“<类名>”类

此控件或组件的设计器不支持与 Visual Studio 相同的类型。 请联系组件供应商。

该类名不是此语言的有效标识符

用户创建的源代码具有对所用语言无效的类名。 若要更正此错误,请命名类,使其符合语言要求。

组件包含对“<引用名称>”的循环引用,因此无法添加该组件

无法将控件或组件添加到自身。 可能出现此问题的另一种情况是,如果窗体(例如 Form1)的 InitializeComponent 方法中有代码创建 Form1 的另一个实例。

此时无法修改设计器

当编辑器中的文件标记为只读时,会发生此错误。 确保文件未标记为只读且应用程序未在运行。

文件中的类都不能进行设计,因此未能为该文件显示设计器

当 Visual Studio 找不到满足设计器要求的基类时,会发生此错误。 窗体和控件必须派生自支持设计器的基类。 如果要从继承窗体或控件派生,请确保已生成项目。

未安装基类“<类名>”的设计器

Visual Studio 无法加载类的设计器。

设计器必须创建类型“<类型名称>”的实例,但该类型已声明为抽象,因此设计器无法创建该类型的实例

发生此错误是因为传递给设计器的对象基类是不允许的抽象类。

未能在设计器中加载该文件

此文件的基类不支持任何设计器。 解决方法是使用代码视图处理文件。 在解决方案资源管理器中右键单击文件,然后选择“查看代码”。

该文件的语言不支持必需的代码分析和生成服务

错误消息:“该文件的语言不支持必需的代码分析和生成服务。 请确保你正在打开的文件是项目的成员,然后尝试重新打开该文件。”

此错误很可能是由于打开不支持设计器的项目中的文件所导致的。

语言分析器类“<类名>”没有正确实现

错误消息:“语言分析器类‘<类名>’没有正确实现。 请和供应商联系以获得更新的分析器模块。”

所使用的语言注册了不是从正确基类派生的设计器类。 请联系你使用的语言的供应商。

名称“<名称>”已由另一个对象使用

这是 Visual Studio 序列化程序中的内部错误。

对象“对象名称”<>没有实现 IComponent 接口

Visual Studio 尝试创建组件,但创建的对象未实现 IComponent 接口。 请联系组件供应商以获取修补程序。

对象“<对象名称>”为属性“<属性名称>”返回了 null,而这是不允许的

某些 .NET 属性应始终返回对象。 例如,窗体的控件集合应始终返回对象,即使其中没有控件也是如此。

若要更正此错误,请确保错误中指定的属性不为 null。

序列化数据对象的类型不正确

序列化程序提供的数据对象不是与所使用的当前序列化程序匹配的类型实例。 请联系组件供应商。

需要服务“<服务名称>”,但未能找到它

错误消息:“需要服务‘服务名称’,但未能找到它<>。 Visual Studio 安装可能存在问题。”

Visual Studio 所需的服务不可用。 如果尝试加载不支持该设计器的项目,请使用代码编辑器进行所需更改。

服务实例必须从“<接口名称>”派生或实现它

此错误指示组件或组件设计器调用了 AddService 方法,该方法需要接口和对象,但指定的对象未实现指定接口。 请联系组件供应商。

未能修改代码窗口中的文本

错误消息:“未能修改代码窗口中的文本。 检查文件是否不是只读,并且是否有足够的磁盘空间。”

如果 Visual Studio 由于磁盘空间或内存问题而无法编辑文件,或者文件标记为只读,则会发生此错误。

工具箱枚举数对象仅支持一次检索一个项

如果看到此错误,请使用报告问题来记录问题。

未能从工具箱中检索到“<组件名称>”的工具箱项

错误消息:“未能从工具箱中检索到‘<组件名称>’的工具箱项。 请确保正确安装了包含该工具箱项的程序集。 该工具箱项引发了以下错误: <错误字符串>。”

相关组件在 Visual Studio 访问它时引发了异常。 请联系组件供应商。

未能从工具箱中检索到“<工具箱项名称>”的工具箱项

错误消息:“未能从工具箱中检索到‘<工具箱项名称>’的工具箱项。 请尝试从工具箱中移除该项,然后再将其添加回工具箱。”

如果工具箱项中的数据损坏或组件版本已更改,则会发生此错误。 请尝试从工具箱中移除该项,然后再将其添加回工具箱。

未能找到类型“<类型名称>”

错误消息:“未能找到类型‘<类型名称>’。 请确保已引用包含该类型的程序集。 如果该程序集是当前开发项目的一部分,请确保已生成了该项目。”

加载设计器时,Visual Studio 未能找到类型。 请确保已引用包含该类型的程序集。 如果该程序集是当前开发项目的一部分,请确保已生成了该项目。

只能从主应用程序线程调用类型解析服务

Visual Studio 尝试从错误的线程访问所需资源。 当用于创建设计器的代码从主应用程序线程以外的线程调用类型解析服务时,会显示此错误。 若要更正此错误,请从正确的线程调用服务或联系组件供应商。

变量“<变量名称>”未声明或从未赋值

源代码引用了未声明或赋值的变量(如 Button1)。 如果变量未赋值,则此消息显示为警告,而不是错误。

菜单命令“<菜单命令名称>”已经有一个命令处理程序

如果第三方设计器将已有处理程序的命令添加到命令表中,则会出现此错误。 请联系组件供应商。

已有一个名为“<组件名称>”的组件

错误消息:“已有一个名为‘<组件名称>’的组件。 组件的名称必须是唯一的,而且名称必须不区分大小写。 名称也不能与继承的类中的任何组件名称冲突。”

当在属性窗口中更改了组件的名称时,会出现此错误消息。 若要更正此错误,请确保所有组件名称都是唯一的,不区分大小写,并且不会与继承类中任何组件的名称冲突。

已有一个工具箱项创建者注册了格式“<格式名称>”

第三方组件对工具箱选项卡上的项进行了回调,但该项已包含回调。 请联系组件供应商。

此语言引擎不支持用于加载设计器的 CodeModel

此消息类似于“该文件的语言不支持必需的代码分析和生成服务”,但此消息涉及内部注册问题。

类型“<类型名称>”不具有带有“<参数类型名称>”类型参数的构造函数

Visual Studio 找不到具有匹配参数的构造函数。 这可能是由于为构造函数提供的类型不是所需类型所导致的。 例如,Point 构造函数可能采用两个整数。 如果提供了浮点数,则会引发此错误。

若要更正此错误,请使用其他构造函数,或是显式强制转换参数类型,使其与构造函数提供的类型匹配。

无法添加对当前应用程序的引用“<引用名称>”

错误消息:“无法添加对当前应用程序的引用‘<引用名称>’。 请检查以确保未引用不同的‘<引用名称>’版本。”

Visual Studio 无法添加引用。 若要更正此错误,请检查是否尚未引用该引用的不同版本。

无法签出当前文件

错误消息:“无法签出当前文件。 文件可能被锁定,或者可能需要手动签出文件。”

将当前签入的文件更改为源代码管理时,会出现此错误。 通常,Visual Studio 会提供文件签出对话框,以便用户可以签出文件。 这次文件未签出,可能是由于签出期间发生合并冲突。 若要更正此错误,请确保文件未锁定,然后尝试手动签出文件。

无法找到名为“<选项对话框选项卡名称>”的页

当组件设计器使用不存在的名称请求访问“选项”对话框中的页时,会出现此错误。 请联系组件供应商。

无法在页“<选项对话框选项卡名称>”上找到属性“<属性名称>”

当组件设计器请求访问“选项”对话框中某页上的特定值,但该值不存在时,会出现此错误。 请联系组件供应商。

该文件内的类不是从可进行可视化设计的类继承,因此 Visual Studio 无法为该文件打开设计器

Visual Studio 加载了类,但未能加载该类的设计器。 Visual Studio 要求设计器使用文件中的第一个类。 若要更正此错误,请移动类代码以使其成为文件中的第一个类,然后重新加载该设计器。

Visual Studio 无法保存或加载类型“<类型名称>”的实例

这是第三方组件的问题。 请联系组件供应商。

Visual Studio 无法在设计视图中打开“<文档名称>”

错误消息:“Visual Studio 无法在设计视图中打开‘<文档名称>’。 未安装该文件类型的分析器。”

此错误指示项目的语言不支持设计器,会在你尝试在“打开文件”对话框中或从解决方案资源管理器打开文件时出现。 请改为在代码视图中编辑文件。

Visual Studio 未能找到用于“<类型名称>”类型的类的设计器

Visual Studio 加载了类,但无法设计该类。 请改为通过右键单击类并选择“查看代码”,在代码视图中编辑类。

17、排除控件和组件创作故障

本主题列出了开发组件和控件时遇到的常见问题:

  • 无法将控件添加到工具箱

  • 无法调试 Windows 窗体用户控件或组件

  • 在继承的控件或组件中引发了两次事件

  • 设计时错误:“创建组件‘组件名称’失败”

  • STAThreadAttribute

  • 组件图标未出现在工具箱中

无法将控件添加到工具箱

如果要将在另一项目中创建的自定义控件或第三方控件添加到“工具箱”中,必须手动操作。 如果当前项目中包含控件或组件,它应自动显示在“工具箱”中。

将控件添加到工具箱

  1. 右键单击“工具箱”,并从快捷菜单中选择“选择项”。

  2. 在“选择工具箱项”对话框中,添加组件:

    • 如果要添加 .NET Framework 组件或控件,请单击“.NET Framework 组件”选项卡。

      - 或 -

    • 如果要添加 COM 组件或 ActiveX 控件,请单击“COM 组件”选项卡。

  3. 如果对话框中列出了该控件,请务必将它选中,然后单击“确定”。

    该控件即会添加到“工具箱”中。

  4. 如果对话框中未列出该控件,请执行以下操作:

    1. 单击“浏览”按钮。

    2. 浏览到包含 .dll 文件(它包含控件)的文件夹。

    3. 选择 .dll 文件并单击“打开”。

      控件即会出现在对话框中。

    4. 确认选中该控件,然后单击“确定”。

      控件即会添加到“工具箱”中。

无法调试 Windows 窗体用户控件或组件

如果控件派生自 UserControl 类,则可以借助测试容器对它的运行时行为进行调试。

其他自定义控件和组件不是独立的项目。 它们必须由 Windows 窗体项目这样的应用程序承载。 若要调试控件或组件,必须将其添加到 Windows 窗体项目。

调试控件或组件

  1. 在“生成”菜单中,单击“生成解决方案”来生成解决方案。

  2. 在“文件”菜单中,选择“添加”,然后选择“新建项目”,将测试项目添加到应用程序中。

  3. 在“添加新项目”对话框中选择“Windows 应用程序”作为项目类型。

  4. 在“解决方案资源管理器”中,右键单击新项目的“引用”节点。 在快捷菜单上单击“添加引用”,为包含控件或组件的项目添加引用。

  5. 在测试项目中创建控件或组件的实例。 如果组件在“工具箱”中,则可将其拖动到设计器图面,或者以编程方式创建实例,如下面的代码示例所示。

    MyNeatComponent Component1 = new MyNeatComponent();
  6. 现在即可像平常一样调试控件或组件。

在继承的控件或组件中引发了两次事件

这可能是由于重复的 Handles 子句引起的。

设计时错误:“创建组件‘组件名称’失败”

组件或控件必须提供一个不带参数的无参数构造函数。 设计环境创建组件或控件的实例时,不会尝试为使用参数的构造函数重载提供任何参数。

STAThreadAttribute

STAThreadAttribute 会向公共语言运行时 (CLR) 发出通知,指出 Windows 窗体使用了单线程单元模型。 如果没有对 Windows 窗体应用程序的 Main 方法应用此特性,则可能会出现意外的行为。 例如,ListView 等控件的背景图像可能无法显示。 某些控件也可能需要此属性才能正确地实现自动完成和拖放行为。

组件图标未出现在工具箱中

使用 ToolboxBitmapAttribute 将图标与自定义组件关联时,对于自动生成的组件,位图将不会出现在工具箱中。 若要查看位图,请使用“选择工具箱项”对话框重载控件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值