.NET Framework 开发自定义 Windows 控件

1. 使用 .NET Framework 开发自定义 Windows 窗体控件

Windows 窗体控件是可以重用的组件,可以封装用户界面功能并用于客户端基于 Windows 的应用程序。 Windows 窗体不仅可以提供许多易用的控件,而且还可以提供用于开发你自己的控件的基础结构。 你可以组合现有的控件、扩展现有的控件或创作你自己的自定义控件。 本节介绍了背景信息和示例,有助于你开发 Windows 窗体控件。

简单的 Windows 窗体应用程序

Windows 窗体应用程序至少包含以下元素:

  • 派生自 System.Windows.Forms.Form 的一个或多个类。

  • 一个 Main 方法,它调用 Run 方法并向其传递一个 Form 实例。 Run 方法处理从操作系统发送到应用程序的消息。

下面的代码示例显示 Windows 窗体应用程序的基本元素。

using System;  
using System.Windows.Forms;  
  
public class MyForm : Form {  
  
   public MyForm() 
   {  
      this.Text = "Hello World";  
   }  
   [STAThread]  
   public static void Main(string[] args) 
   {  
      MyForm aform = new MyForm();  
      // 方法处理从操作系统发送到应用程序的消息。
      // 如果注释掉下一行代码,应用程序将编译并执行,但由于它不在消息循环中,因此它将在创建表单实例后退出。
      Application.Run(aform);  
   }  
}  

在 Windows 窗体应用程序中使用控件

下面的代码示例演示了一个简单的应用程序,说明了 Windows 窗体应用程序如何使用控件和处理事件。 该示例由窗体上的三个按钮组成。单击每个按钮时,都会改变背景颜色。

using System;
using System.Drawing;
using System.Windows.Forms;
​
namespace WinForm01
{
    public class MyForm : Form
    {
        private Button red;
        private Button blue;
        private Button green;
​
        public MyForm(): base()
        {
            InitializeComponent();
        }
​
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
        }
​
        /// <summary>
        /// InitializeComponent是构造函数的辅助方法。
        /// 包含它是为了与Visual Studio中Windows窗体设计器自动生成的代码保持一致。
        /// </summary>
        private void InitializeComponent()
        {
            // 按钮单击事件的委托。构造函数的参数包含
            // 对执行事件处理逻辑的方法的引用。
            EventHandler handler = new EventHandler(button_Click);
​
            // 创建三个按钮,设置它们的属性,并为每个按钮附加一个事件处理程序。
            red = new Button();
            red.Text = "Red";
            red.Location = new Point(100, 50);
            red.Size = new Size(50, 50);
            red.Click += handler;
            Controls.Add(red);
​
            blue = new Button();
            blue.Text = "Blue";
            blue.Location = new Point(100, 100);
            blue.Size = new Size(50, 50);
            blue.Click += handler;
            Controls.Add(blue);
​
            green = new Button();
            green.Text = "Green";
            green.Location = new Point(100, 150);
            green.Size = new Size(50, 50);
            green.Click += handler;
            Controls.Add(green);
        }
​
        /// <summary>
        /// 单击事件处理程序 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button_Click(object sender, EventArgs e)
        {
            if (sender == red)
            {
                this.BackColor = Color.Red;
            }
            else if (sender == blue)
            {
                this.BackColor = Color.Blue;
            }
            else
            {
                this.BackColor = Color.Green;
            }
        }
        
        static class Program
        {
            /// <summary>
            /// 应用程序的主入口点。
            /// </summary>
            [STAThread]
            static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new MyForm());
            }
        }
    }
}

2. 各种自定义控件

使用 .NET Framework 可以开发和实现新的控件。 可以通过继承来扩展熟悉的用户控件和现有控件的功能。 还可以编写自定义控件,这些控件执行自己的绘制。

确定创建何种类型的控件可能令人困惑。 本主题重点介绍各种可继承控件之间的差异,并提供有关如何为项目选择某种特定控件的信息。

基控件类

Control 类是 Windows 窗体控件的基类。 它提供了在 Windows 窗体应用程序中进行可视显示所需的基础结构。

Control 类执行下列任务,以便在 Windows 窗体应用程序中提供可视显示:

  • 公开窗口句柄。

  • 管理消息路由。

  • 提供鼠标和键盘事件,以及许多其他用户界面事件。

  • 提供高级布局功能。

  • 包含特定于可视化显示的多个属性,如 ForeColorBackColorHeightWidth

  • Windows 窗体控件充当 Microsoft® ActiveX® 控件提供必需的安全性和线程支持。

由于基类提供了大量基础结构,因此使开发自己的 Windows 窗体控件变得相对简单。

控件种类

Windows 窗体支持三种用户定义的控件:复合、扩展和自定义。 以下各节分别介绍各种控件,并就如何选择项目中使用的控件种类提供建议。

复合控件

复合控件是封装在公共容器内的 Windows 窗体控件的集合。 这种控件有时称为用户控件。 其包含的控件称为构成控件。

复合控件包含与每个包含的 Windows 窗体控件相关联的所有固有功能,允许选择性地公开和绑定它们的属性。 复合控件还提供了大量的默认键盘处理功能,用户不需要进行任何额外的开发。

例如,可以生成复合控件,以显示来自数据库的客户地址数据。 此控件可包括用于显示数据库字段的 DataGridView 控件、用于处理到数据源的绑定的 BindingSource,以及用于在记录之间移动的 BindingNavigator 控件。 可以选择性地公开数据绑定属性,还可以将整个控件打包并在不同应用程序之间重复使用。

若要创作复合控件,请从 UserControl 类派生。 基类 UserControl 为子控件提供了键盘路由,并使子控件可以作为组进行工作。

建议

如果为以下情况,则从 UserControl 类继承:

  • 你想要将多个 Windows 窗体控件的功能组合到单个可重用单元。

扩展控件

你可以从任何现有的 Windows 窗体控件派生继承的控件。 使用此方法,你可以保留 Windows 窗体控件的所有固有功能,然后通过添加自定义属性、方法或其他功能来扩展该功能。 可以使用此选项重写基控件的绘制逻辑,然后通过更改该控件的外观来扩展其用户界面。

例如,可以创建一个由 Button 控件派生的控件,并用它来跟踪用户的单击次数。

在某些控件中,也可以通过重写基类的 OnPaint 方法为控件的图形用户界面添加自定义外观。 对于跟踪单击次数的扩展按钮,可以重写 OnPaint 方法以调用 OnPaint 的基实现,然后在 Button 控件的工作区的一角绘制单击计数。

建议

如果为以下情况,则从 Windows 窗体控件继承:

  • 大部分所需功能与现有的 Windows 窗体控件相同。

  • 不需要自定义图形用户界面,或者想为现有控件设计一个新的图形用户界面。

自定义控件

创建控件的另一种方法是通过从 Control 继承,从头开始充分创建一个控件。 Control 类提供控件所需的所有基本功能(包括鼠标和键盘处理事件),但不提供特定于控件的功能或图形界面。

相比从 UserControl 或现有 Windows 窗体控件继承来说,通过从 Control 类继承来创建控件需要花费更多心思和精力。 由于用户还需执行大量的实现,因此,控件可以具有比复合控件或扩展控件更好的灵活性,而且可以使控件完全满足自己的需要。

若要实现自定义控件,必须编写该控件的 OnPaint 事件的代码,以及所需的任何功能特定的代码。 还可以重写 WndProc 方法并直接处理窗口消息。 这是创建控件的最强大的方法,但若要有效地使用此技术,需熟悉 Microsoft Win32® API

时钟控件即是一个自定义控件,它复制模拟时钟的外观和行为。 调用自定义绘制来使指针移动,以响应来自内部 Timer 组件的 Tick 事件。

建议

如果为以下情况,则从 Control 类继承:

  • 你想要提供控件的自定义图形表示形式。

  • 你需要实现不能通过标准控件实现的自定义功能。

ActiveX 控件

尽管 Windows 窗体基础结构已为承载 Windows 窗体控件进行了优化,但仍可以使用 ActiveX 控件。 Visual Studio 中对此任务提供支持。

自定义设计体验

如果需要实现自定义设计时体验,可以创作自己的设计器。 对于复合控件,从 ParentControlDesignerDocumentDesigner 类派生自定义设计器类。 对于扩展控件和自定义控件,从 ControlDesigner 类派生自定义设计器类。

使用 DesignerAttribute 将控件与设计器关联。

控件类型建议

.NET Framework 使你能够开发和实现新控件。 除了已熟悉的用户控件,现在你会发现你能够编写自定义控件用于执行其自己的绘制,甚至能够通过继承扩展现有控件的功能。 决定创建哪种类型的控件可能令人困惑。 本节重点介绍可以从其继承的各类控件之间的区别,并提供要为你的项目选择的类型的相关注意事项。

Windows 窗体控件继承

你可以从任何现有的 Windows 窗体控件派生继承的控件。 此方法使你可以保留 Windows 窗体控件的所有固有功能,然后通过添加自定义属性、方法或其他功能来扩展该功能。 例如,可以创建一个派生自 TextBox 的控件,它只能接受数字并自动将输入转换为一个值。 此类控件可能包含验证代码,该代码每当文本框中的文本更改时即被调用,而且可能具有附加属性,即“值”。 在某些控件中,也可以通过重写基类的 OnPaint 方法为控件的图形界面添加自定义外观。

如果为以下情况,则从 Windows 窗体控件继承:

  • 大部分所需功能与现有的 Windows 窗体控件相同。

  • 不需要自定义图形界面,或者你想要为现有控件设计一个新图形前端。

UserControl 类继承

用户控件是封装到一个公共容器中的 Windows 窗体控件的集合。 容器保有与每个 Windows 窗体控件关联的所有固有功能,并允许你选择性地公开和绑定它们的属性。 用户控件的一个示例可能是这样一个控件,其生成的目的是显示来自数据库的客户地址数据。 该控件将包括多个用以显示每个字段的文本框以及用以在记录中导航的按钮控件。 可以选择性地公开数据绑定属性,并可以打包整个控件,并将其重复应用于不同的应用程序。

如果为以下情况,则从 UserControl 类继承:

  • 你想要将多个 Windows 窗体控件的功能组合到单个可重用单元。

从 Control 类继承

创建控件的另一种方法是通过从 Control 继承,从头开始充分创建一个控件。 Control 类提供控件(例如,事件)所需的所有基本功能,但没有特定于控件的功能或图形界面。 相比从用户控件或现有 Windows 窗体控件继承来说,通过从 Control 类继承来创建控件需要更多的心思和精力。 作者必须为控件的 OnPaint 事件以及任何所需的功能特定的代码编写代码。 但是允许更大的灵活性,并且可以自定义定制控件以满足你的具体需求。 自定义控件的一个示例是一个时钟控件,它复制了模拟时钟的外观和操作。 将调用自定义绘制来使指针移动,以响应来自内部计时器组件的 Tick 事件。

如果为以下情况,则从 Control 类继承:

  • 你想要提供控件的自定义图形表示形式。

  • 你需要实现不能通过标准控件实现的自定义功能。

3. Windows 窗体控件开发基础知识

Windows 窗体控件是直接或间接派生自 System.Windows.Forms.Control 的类。 以下列表介绍了开发 Windows 窗体控件的常见方案:

  • 组合现有控件以创作复合控件。

    复合控件封装可重复用作控件的用户界面。 复合控件的一个示例是由文本框和重置按钮组成的控件。 可视化设计器为创建复合控件提供了丰富的支持。 若要创作复合控件,请从 System.Windows.Forms.UserControl 派生。 基类 UserControl 为子控件提供了键盘路由,并使子控件可以作为组进行工作。

  • 扩展现有控件以对其自定义或将其添加到其功能。

    无法更改其颜色的按钮以及具有一个附加属性来跟踪它被单击多少次的按钮都是扩展控件的示例。 可从任何 Windows 窗体控件派生并重写或添加属性、方法和事件来自定义该控件。

  • 创作不合并或扩展现有控件的控件。

    在此方案中,从基类 Control 派生控件。 可以添加和替换基类的属性、方法和事件。

Windows 窗体控件的基类 Control 提供在基于客户端 Windows 的应用程序中进行进行可视化显示所需的管道。 Control 提供窗口句柄,处理消息路由并提供鼠标和键盘事件以及其他许多用户界面事件。 它提供高级布局,并且具有特定于可视化显示的属性,例如 ForeColorBackColorHeightWidth 等等。 此外,它还提供安全性、线程支持以及与 ActiveX 控件的互操作性。 由于基类提供了大量基础结构,因此使开发自己的 Windows 窗体控件变得相对简单。

如何:开发简单的 Windows 窗体控件

本部分演示创建自定义 Windows 窗体控件的关键步骤。 本演练中开发的简单控件允许更改 Text 属性的对齐方式。 它不会引发或处理事件。

创建简单的自定义控件

  1. 定义一个从 System.Windows.Forms.Control 派生的类。

    public class FirstControl:Control {}
  2. 定义属性。 (不需要定义属性,因为控件继承了 Control 类中的许多属性,但大多数自定义控件通常都会定义其他属性。)以下代码片段定义名为 TextAlignment 的属性,FirstControl 使用该属性设置继承自 ControlText 属性的显示格式。

    // ContentAlignment是一个在System中定义的枚举。绘图命名空间,指定绘图表面上内容的对齐方式。
    private ContentAlignment alignmentValue = ContentAlignment.MiddleLeft;

设置用于更改控件的视觉显示的属性时,必须调用 Invalidate 方法来重绘控件。 在基类 Control 中定义 Invalidate

  1. 重写从 Control 继承的受保护的 OnPaint 方法,以便为控件提供呈现逻辑。 如果不重写 OnPaint,则控件将无法自行自身。 在下面的代码片段中,OnPaint 方法显示从 Control 继承的 Text 属性,其对齐方式由 alignmentValue 字段指定。

            protected override void OnPaint(PaintEventArgs e)
            {
                base.OnPaint(e);
                StringFormat style = new StringFormat();
                // StringAlignment.Near:指定文本对齐靠近布局。 在从左到右布局中,保留近的位置。 在从右到左布局中,近的位置是右
                style.Alignment = StringAlignment.Near;
                switch (alignmentValue)
                {
                    case ContentAlignment.MiddleLeft:   // 内容是在中间,垂直对齐和水平方向上左对齐
                        // 指定文本对齐靠近布局。 在从左到右布局中,保留近的位置。
                        // 在从右到左布局中,近的位置是右。
                        style.Alignment = StringAlignment.Near;
                        break;
                    case ContentAlignment.MiddleRight:  // 内容是在中间,垂直对齐和水平方向上右对齐
                        // 指定文本对齐与相差甚远的布局矩形的来源位置。
                        // 在从左到右布局中,远的位置是右。 在从右到左布局中,保留远的位置。
                        style.Alignment = StringAlignment.Far;
                        break;
                    case ContentAlignment.MiddleCenter: // 内容是在中间,垂直对齐和水平方向上居中对齐。
                        // 指定文本在布局矩形的中心对齐。
                        style.Alignment = StringAlignment.Center;
                        break;
                }
                // 调用系统的DrawString方法。绘图写文字。Text和ClientRectangle是从Control继承的属性。
                e.Graphics.DrawString(
                    Text,
                    Font,
                    new SolidBrush(ForeColor),
                    ClientRectangle, style);
            }
  2. 为控件提供属性。 特性让视觉设计器能够在设计时适当显示控件及其属性和事件。 以下代码片段将特性应用于 TextAlignment 属性。 在诸如 Visual Studio 的设计器中,Category 特性(在代码片段中显示)会让属性显示在逻辑类别下。 如果选择了 TextAlignment 属性,Description 特性会使描述性字符串显示在“属性”窗口的底部。

    [
    Category("Alignment"),
    Description("指定文本的对齐方式。")
    ]

此示例演示了 FirstControl 的代码。 控件包含在命名空间 CustomWinControls 中。 命名空间提供相关类型的逻辑分组。 可在新的或现有命名空间中创建控件。 在 C# 中,using 声明允许从命名空间访问类型,而无需使用类型的完全限定名称。 在下面的示例中,using 声明允许代码直接作为 Control 访问来自 System.Windows.Forms 的类 Control,而不必使用完全限定的名称 System.Windows.Forms.Control

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
​
namespace WinForm01
{
    public class FirstControl:Control
    {
        // ContentAlignment是一个在System中定义的枚举。绘图命名空间,指定绘图表面上内容的对齐方式。
        private ContentAlignment alignmentValue = ContentAlignment.MiddleLeft;
​
        [
        Category("Alignment"),
        Description("指定文本的对齐方式。")
        ]
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            StringFormat style = new StringFormat();
            // StringAlignment.Near:指定文本对齐靠近布局。 在从左到右布局中,保留近的位置。 在从右到左布局中,近的位置是右
            style.Alignment = StringAlignment.Near;
            switch (alignmentValue)
            {
                case ContentAlignment.MiddleLeft:   // 内容是在中间,垂直对齐和水平方向上左对齐
                    // 指定文本对齐靠近布局。 在从左到右布局中,保留近的位置。
                    // 在从右到左布局中,近的位置是右。
                    style.Alignment = StringAlignment.Near;
                    break;
                case ContentAlignment.MiddleRight:  // 内容是在中间,垂直对齐和水平方向上右对齐
                    // 指定文本对齐与相差甚远的布局矩形的来源位置。
                    // 在从左到右布局中,远的位置是右。 在从右到左布局中,保留远的位置。
                    style.Alignment = StringAlignment.Far;
                    break;
                case ContentAlignment.MiddleCenter: // 内容是在中间,垂直对齐和水平方向上居中对齐。
                    // 指定文本在布局矩形的中心对齐。
                    style.Alignment = StringAlignment.Center;
                    break;
            }
            // 调用系统的DrawString方法。绘图写文字。Text和ClientRectangle是从Control继承的属性。
            e.Graphics.DrawString(
                Text,
                Font,
                new SolidBrush(ForeColor),
                ClientRectangle, style);
        }
    }
}
 

在窗体上使用自定义控件

以下示例显示使用 FirstControl 的简单窗体。 它会创建 FirstControl 的三个实例,每个实例具有一个不同的 TextAlignment 属性值。

编译和运行此示例
  1. 将以下示例中的代码保存到源文件(SimpleForm.cs)。

  2. 使用以下命令执行 SimpleForm.exe

    SimpleForm

    using System;
    using System.Drawing;
    using System.Collections;
    using System.ComponentModel;
    using System.Windows.Forms;
    ​
    namespace CustomWinControls
    {
    ​
        public class SimpleForm : System.Windows.Forms.Form
        {
            private FirstControl firstControl1;
    ​
            private System.ComponentModel.Container components = null;
    ​
            public SimpleForm()
            {
                InitializeComponent();
            }
    ​
            protected override void Dispose( bool disposing )
            {
                if( disposing )
                {
                    if (components != null)
                    {
                        components.Dispose();
                    }
                }
                base.Dispose( disposing );
            }
    ​
            private void InitializeComponent()
            {
                this.firstControl1 = new FirstControl();
                this.SuspendLayout();
    ​
                // firstControl1
                this.firstControl1.BackColor = System.Drawing.SystemColors.ControlDark;
                this.firstControl1.Location = new System.Drawing.Point(96, 104);
                this.firstControl1.Name = "firstControl1";
                this.firstControl1.Size = new System.Drawing.Size(75, 16);
                this.firstControl1.TabIndex = 0;
                this.firstControl1.Text = "Hello World";
                this.firstControl1.TextAlignment = System.Drawing.ContentAlignment.MiddleCenter;
    ​
                // SimpleForm
                this.ClientSize = new System.Drawing.Size(292, 266);
                this.Controls.Add(this.firstControl1);
                this.Name = "SimpleForm";
                this.Text = "SimpleForm";
                this.ResumeLayout(false);
            }
    ​
            [STAThread]
            static void Main()
            {
                Application.Run(new SimpleForm());
            }
        }
    }

如何:创建显示进度的 Windows 窗体控件

以下代码示例显示了一个名为 FlashTrackBar 的自定义控件,可用于向用户显示应用程序的级别或进度。 它使用渐变来直观地表示进度。

FlashTrackBar 控件阐释了以下概念:

  • 定义自定义属性。

  • 定义自定义事件。 (FlashTrackBar 定义 ValueChanged 事件。)

  • 重写 OnPaint 方法以提供绘制控件的逻辑。

  • 使用控件的 ClientRectangle 属性计算可用于绘制控件的区域。 FlashTrackBarOptimizedInvalidate 方法中执行此操作。

  • Windows 窗体设计器中更改属性时,实现其序列化或持久性。 FlashTrackBar 定义用于序列化其 StartColorEndColor 属性的 ShouldSerializeStartColorShouldSerializeEndColor 方法。

下表显示了由 FlashTrackBar 定义的自定义属性。

属性说明
AllowUserEdit指示用户是否可以通过单击和拖动来更改闪存跟踪条的值。
EndColor指定跟踪条的结束颜色。
DarkenBy指定相对于前景渐变加深背景颜色的程度。
Max指定跟踪条的最大值。
Min指定跟踪条的最小值。
StartColor指定渐变的开始颜色。
ShowPercentage指示是否在渐变上显示百分比。
ShowValue指示是否在渐变上显示当前值。
ShowGradient指示跟踪条是否应显示有当前值的颜色渐变。
Value指定跟踪条的当前值。

下表显示了 FlashTrackBar: 定义的其他成员:property-changed 事件和引发该事件的方法。

成员说明
ValueChanged当跟踪条的 Value 属性更改时引发的事件。
OnValueChanged引发 ValueChanged 事件的方法。

备注

FlashTrackBarEventArgs 类用于事件数据,将 EventHandler 类用于事件委托。

为了处理相应的 EventName 事件,FlashTrackBar 将替代从 System.Windows.Forms.Control 继承的以下方法:

  • OnPaint

  • OnMouseDown

  • OnMouseMove

  • OnMouseUp

  • OnResize

为了处理相应的属性更改事件,FlashTrackBar 将替代从 System.Windows.Forms.Control 继承的以下方法:

  • OnBackColorChanged

  • OnBackgroundImageChanged

  • OnTextChanged

示例

FlashTrackBar 控件定义了两个 UI 类型编辑器(FlashTrackBarValueEditorFlashTrackBarDarkenByEditor),它们显示在以下代码列表中。 HostApp 类在 Windows 窗体上使用 FlashTrackBar 控件。

namespace Microsoft.Samples.WinForms.Cs.FlashTrackBar
{
    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Drawing.Design;
    using System.Windows.Forms;
    using System.Diagnostics;

    public class FlashTrackBar : Control
    {
        /// <summary>
        ///    必需的设计变量。
        /// </summary>
        private Container components;

        /// <summary>
        /// 左右边距
        /// </summary>
        private const int LeftRightBorder = 10;
        /// <summary>
        /// 指定跟踪条的当前值。
        /// </summary>
        private int value = 0;
        /// <summary>
        /// 指定跟踪条的最小值。
        /// </summary>
        private int min = 0;
        /// <summary>
        /// 指定跟踪条的最大值。
        /// </summary>
        private int max = 100;
        /// <summary>
        /// 指示是否在渐变上显示百分比。
        /// </summary>
        private bool showPercentage = false;
        /// <summary>
        /// 指示是否在渐变上显示当前值。
        /// </summary>
        private bool showValue = false;
        /// <summary>
        /// 指示用户是否可以通过单击和拖动来更改闪存跟踪条的值。
        /// </summary>
        private bool allowUserEdit = true;
        /// <summary>
        /// 指示跟踪条是否应显示有当前值的颜色渐变。
        /// </summary>
        private bool showGradient = true;
        /// <summary>
        /// 指定相对于前景渐变加深背景颜色的程度。
        /// </summary>
        private int dragValue = 0;
        /// <summary>
        /// 是否拖拽
        /// </summary>
        private bool dragging = false;
        /// <summary>
        /// 指定渐变的开始颜色。
        /// </summary>
        private Color startColor = Color.Red;
        /// <summary>
        /// 指定跟踪条的结束颜色。
        /// </summary>
        private Color endColor = Color.LimeGreen;
        /// <summary>
        /// 指定相对于前景渐变加深背景颜色的程度。
        /// </summary>
        private byte darkenBy = 200;

        /// <summary>
        /// 引发 `ValueChanged` 事件的方法。
        /// </summary>
        private EventHandler onValueChanged;
        private Brush baseBackground = null;
        private Brush backgroundDim = null;

        public FlashTrackBar()
        {
            // Windows窗体设计器支持所需
            InitializeComponent();

            // 将指定的 System.Windows.Forms.ControlStyles 标志设置为 true 或 false。
            SetStyle(ControlStyles.Opaque, true);// 如果为 true,则控件会绘制为不透明,且不绘制背景。
            SetStyle(ControlStyles.ResizeRedraw, true); // 如果为 true,则控件会在调整大小时进行重绘。
            Debug.Assert(GetStyle(ControlStyles.ResizeRedraw), "Should be redraw!");
        }

        /// <summary>
        /// 清理所有正在使用的资源。
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (components != null)
                {
                    components.Dispose();
                }
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// 设计器所需的方法
        /// </summary>
        void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.ForeColor = System.Drawing.Color.White;
            this.BackColor = System.Drawing.Color.Black;
            this.Size = new System.Drawing.Size(100, 23);
            this.Text = "FlashTrackBar";
        }

        [
            Category("Alignment"),
            DefaultValue(true),Description("指示用户是否可以通过单击和拖动来更改闪存跟踪条的值。")
        ]
        public bool AllowUserEdit
        {
            get
            {
                return allowUserEdit;
            }
            set
            {
                if (value != allowUserEdit)
                {
                    allowUserEdit = value;
                    if (!allowUserEdit)
                    {
                        Capture = false;
                        dragging = false;
                    }
                }
            }
        }

        [Category("Flash"),Description("指定跟踪条的结束颜色。")]
        public Color EndColor
        {
            get
            {
                return endColor;
            }
            set
            {
                endColor = value;
                if (baseBackground != null && showGradient)
                {
                    baseBackground.Dispose();
                    baseBackground = null;
                }
                Invalidate();
            }
        }

        public bool ShouldSerializeEndColor()
        {
            return !(endColor == Color.LimeGreen);
        }

        [
            Category("Flash"),
            Editor(typeof(FlashTrackBarDarkenByEditor), typeof(UITypeEditor)),
            DefaultValue((byte)200),
            Description("指定相对于前景渐变加深背景颜色的程度。")
        ]
        public byte DarkenBy
        {
            get
            {
                return darkenBy;
            }
            set
            {
                if (value != darkenBy)
                {
                    darkenBy = value;
                    if (backgroundDim != null)
                    {
                        backgroundDim.Dispose();
                        backgroundDim = null;
                    }
                    OptimizedInvalidate(Value, max);
                }
            }
        }

        [Category("Flash"),DefaultValue(100),Description("指定跟踪条的最大值。")]
        public int Max
        {
            get
            {
                return max;
            }
            set
            {
                if (max != value)
                {
                    max = value;
                    Invalidate();
                }
            }
        }

        [Category("Flash"),DefaultValue(0),Description("指定跟踪条的最小值。")]
        public int Min
        {
            get
            {
                return min;
            }
            set
            {
                if (min != value)
                {
                    min = value;
                    Invalidate();
                }
            }
        }

        [Category("Flash"),Description("指定渐变的开始颜色。")]
        public Color StartColor
        {
            get
            {
                return startColor;
            }
            set
            {
                startColor = value;
                if (baseBackground != null && showGradient)
                {
                    baseBackground.Dispose();
                    baseBackground = null;
                }
                Invalidate();
            }
        }

        public bool ShouldSerializeStartColor()
        {
            return !(startColor == Color.Red);
        }

        [
            Category("Flash"),
            RefreshProperties(RefreshProperties.Repaint),
            DefaultValue(false),
            Description("指示是否在渐变上显示百分比。")
        ]
        public bool ShowPercentage
        {
            get
            {
                return showPercentage;
            }
            set
            {
                if (value != showPercentage)
                {
                    showPercentage = value;
                    if (showPercentage)
                    {
                        showValue = false;
                    }
                    Invalidate();
                }
            }
        }

        [
            Category("Flash"),
            RefreshProperties(RefreshProperties.Repaint),
            DefaultValue(false),
            Description("指示是否在渐变上显示当前值。")
        ]
        public bool ShowValue
        {
            get
            {
                return showValue;
            }
            set
            {
                if (value != showValue)
                {
                    showValue = value;
                    if (showValue)
                    {
                        showPercentage = false;
                    }
                    Invalidate();
                }
            }
        }

        [
            Category("Flash"),
            DefaultValue(true),
            Description("指示跟踪条是否应显示有当前值的颜色渐变。")
        ]
        public bool ShowGradient
        {
            get
            {
                return showGradient;
            }
            set
            {
                if (value != showGradient)
                {
                    showGradient = value;
                    if (baseBackground != null)
                    {
                        baseBackground.Dispose();
                        baseBackground = null;
                    }
                    Invalidate();
                }
            }
        }

        [
            Category("Flash"),
            Editor(typeof(FlashTrackBarValueEditor), 
            typeof(UITypeEditor)),DefaultValue(0),
            Description("指定跟踪条的当前值。")
        ]
        public int Value
        {
            get
            {
                if (dragging)
                {
                    return dragValue;
                }
                return value;
            }
            set
            {
                if (value != this.value)
                {
                    int old = this.value;
                    this.value = value;
                    OnValueChanged(EventArgs.Empty);
                    OptimizedInvalidate(old, this.value);
                }
            }
        }


        // ValueChanged事件
        [Description("当显示的值改变时引发")]
        public event EventHandler ValueChanged
        {
            add
            {
                onValueChanged += value;
            }
            remove
            {
                onValueChanged -= value;
            }
        }

        protected virtual void OnValueChanged(EventArgs e)
        {
            if (onValueChanged != null)
            {
                onValueChanged.Invoke(this, e);
            }
        }

        private void SetDragValue(Point mouseLocation)
        {

            Rectangle client = ClientRectangle;

            if (client.Contains(mouseLocation))
            {
                float percentage = (float)mouseLocation.X / (float)ClientRectangle.Width;
                int newDragValue = (int)(percentage * (float)(max - min));
                if (newDragValue != dragValue)
                {
                    int old = dragValue;
                    dragValue = newDragValue;
                    OptimizedInvalidate(old, dragValue);
                }
            }
            else
            {
                if (client.Y <= mouseLocation.Y && mouseLocation.Y <= client.Y + client.Height)
                {
                    if (mouseLocation.X <= client.X && mouseLocation.X > client.X - LeftRightBorder)
                    {
                        int newDragValue = min;
                        if (newDragValue != dragValue)
                        {
                            int old = dragValue;
                            dragValue = newDragValue;
                            OptimizedInvalidate(old, dragValue);
                        }
                    }
                    else if (mouseLocation.X >= client.X + client.Width && mouseLocation.X < client.X + client.Width + LeftRightBorder)
                    {
                        int newDragValue = max;
                        if (newDragValue != dragValue)
                        {
                            int old = dragValue;
                            dragValue = newDragValue;
                            OptimizedInvalidate(old, dragValue);
                        }
                    }
                }
                else
                {
                    if (dragValue != value)
                    {
                        int old = dragValue;
                        dragValue = value;
                        OptimizedInvalidate(old, dragValue);
                    }
                }
            }
        }

        private void OptimizedInvalidate(int oldValue, int newValue)
        {
            // 获取表示控件的工作区的矩形。
            Rectangle client = ClientRectangle;

            // 计算oldValue占了工作区的长度
            float oldPercentValue = ((float)oldValue / ((float)Max - (float)Min));
            int oldNonDimLength = (int)(oldPercentValue * (float)client.Width);

            // 计算工作区总长度
            float newPercentValue = ((float)newValue / ((float)Max - (float)Min));
            int newNonDimLength = (int)(newPercentValue * (float)client.Width);

            int min = Math.Min(oldNonDimLength, newNonDimLength);
            int max = Math.Max(oldNonDimLength, newNonDimLength);

            // 计算进度条占用区域
            Rectangle invalid = new Rectangle(
                client.X + min,
                client.Y,
                max - min,
                client.Height);

            Invalidate(invalid);

            string oldToDisplay;
            string newToDisplay;

            if (ShowPercentage) // 计算百分比
            {
                oldToDisplay = Convert.ToString((int)(oldPercentValue * 100f)) + "%";
                newToDisplay = Convert.ToString((int)(newPercentValue * 100f)) + "%";
            }
            else if (ShowValue) // 计算当前值
            {
                oldToDisplay = Convert.ToString(oldValue);
                newToDisplay = Convert.ToString(newValue);
            }
            else
            {
                oldToDisplay = null;
                newToDisplay = null;
            }

            if (oldToDisplay != null && newToDisplay != null)
            {
                // 为控件创建 System.Drawing.Graphics:封装一个 GDI+ 绘图图面。 此类不能被继承。
                Graphics g = CreateGraphics();
                // MeasureString:测量用指定的 System.Drawing.Font 绘制的指定字符串。
                SizeF oldFontSize = g.MeasureString(oldToDisplay, Font);
                SizeF newFontSize = g.MeasureString(newToDisplay, Font);
                RectangleF oldFontRect = new RectangleF(new PointF(0, 0), oldFontSize);
                RectangleF newFontRect = new RectangleF(new PointF(0, 0), newFontSize);
                oldFontRect.X = (client.Width - oldFontRect.Width) / 2;
                oldFontRect.Y = (client.Height - oldFontRect.Height) / 2;
                newFontRect.X = (client.Width - newFontRect.Width) / 2;
                newFontRect.Y = (client.Height - newFontRect.Height) / 2;

                Invalidate(new Rectangle((int)oldFontRect.X, (int)oldFontRect.Y, (int)oldFontRect.Width, (int)oldFontRect.Height));
                Invalidate(new Rectangle((int)newFontRect.X, (int)newFontRect.Y, (int)newFontRect.Width, (int)newFontRect.Height));
            }
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            if (!allowUserEdit)
            {
                return;
            }
            Capture = true;
            dragging = true;
            SetDragValue(new Point(e.X, e.Y));
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            if (!allowUserEdit || !dragging)
            {
                return;
            }
            SetDragValue(new Point(e.X, e.Y));
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            base.OnMouseUp(e);
            if (!allowUserEdit || !dragging)
            {
                return;
            }
            Capture = false;
            dragging = false;
            value = dragValue;
            OnValueChanged(EventArgs.Empty);
        }

        protected override void OnPaint(PaintEventArgs e)
        {

            base.OnPaint(e);
            if (baseBackground == null)
            {
                if (showGradient)
                {
                    baseBackground = new LinearGradientBrush(new Point(0, 0),
                                                             new Point(ClientSize.Width, 0),
                                                             StartColor,
                                                             EndColor);
                }
                else if (BackgroundImage != null)
                {
                    baseBackground = new TextureBrush(BackgroundImage);
                }
                else
                {
                    baseBackground = new SolidBrush(BackColor);
                }
            }

            backgroundDim = new SolidBrush(Color.FromArgb(DarkenBy, Color.Black));

            Rectangle toDim = ClientRectangle;
            float percentValue = ((float)Value / ((float)Max - (float)Min));
            int nonDimLength = (int)(percentValue * (float)toDim.Width);
            toDim.X += nonDimLength;
            toDim.Width -= nonDimLength;

            string text = Text;
            string toDisplay = null;
            RectangleF textRect = new RectangleF();

            if (ShowPercentage || ShowValue || text.Length > 0)
            {

                if (ShowPercentage)
                {
                    toDisplay = Convert.ToString((int)(percentValue * 100f)) + "%";
                }
                else if (ShowValue)
                {
                    toDisplay = Convert.ToString(Value);
                }
                else
                {
                    toDisplay = text;
                }

                SizeF textSize = e.Graphics.MeasureString(toDisplay, Font);
                textRect.Width = textSize.Width;
                textRect.Height = textSize.Height;
                textRect.X = (ClientRectangle.Width - textRect.Width) / 2;
                textRect.Y = (ClientRectangle.Height - textRect.Height) / 2;
            }

            e.Graphics.FillRectangle(baseBackground, ClientRectangle);
            e.Graphics.FillRectangle(backgroundDim, toDim);
            e.Graphics.Flush();
            if (toDisplay != null && toDisplay.Length > 0)
            {
                e.Graphics.DrawString(toDisplay, Font, new SolidBrush(ForeColor), textRect);
            }
        }

        protected override void OnTextChanged(EventArgs e)
        {
            base.OnTextChanged(e);
            Invalidate();
        }

        protected override void OnBackColorChanged(EventArgs e)
        {
            base.OnBackColorChanged(e);
            if ((baseBackground != null) && (!showGradient))
            {
                baseBackground.Dispose();
                baseBackground = null;
            }
        }

        protected override void OnBackgroundImageChanged(EventArgs e)
        {
            base.OnTextChanged(e);
            if ((baseBackground != null) && (!showGradient))
            {
                baseBackground.Dispose();
                baseBackground = null;
            }
        }

        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            if (baseBackground != null)
            {
                baseBackground.Dispose();
                baseBackground = null;
            }
        }

    }
}
namespace Microsoft.Samples.WinForms.Cs.FlashTrackBar {
   using System;
   using System.ComponentModel;
   using System.ComponentModel.Design;
   using System.Diagnostics;
   using System.Drawing;
   using System.Drawing.Drawing2D;
   using System.Drawing.Design;
   using System.Windows.Forms;
   using System.Windows.Forms.ComponentModel;
   using System.Windows.Forms.Design;

   public class FlashTrackBarDarkenByEditor : FlashTrackBarValueEditor {
       protected override void SetEditorProps(FlashTrackBar editingInstance, FlashTrackBar editor) {
           base.SetEditorProps(editingInstance, editor);
           editor.Min = 0;
           editor.Max = byte.MaxValue;
       }
   }
}
namespace Microsoft.Samples.WinForms.Cs.FlashTrackBar
{
    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Design;
    using System.Windows.Forms.Design;

    public class FlashTrackBarValueEditor : UITypeEditor
    {

        private IWindowsFormsEditorService edSvc = null;

        protected virtual void SetEditorProps(FlashTrackBar editingInstance, FlashTrackBar editor)
        {
            editor.ShowValue = true;
            editor.StartColor = Color.Navy;
            editor.EndColor = Color.White;
            editor.ForeColor = Color.White;
            editor.Min = editingInstance.Min;
            editor.Max = editingInstance.Max;
        }

        public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
        {

            if (context != null
                && context.Instance != null
                && provider != null)
            {

                edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));

                if (edSvc != null)
                {
                    FlashTrackBar trackBar = new FlashTrackBar();
                    trackBar.ValueChanged += new EventHandler(this.ValueChanged);
                    SetEditorProps((FlashTrackBar)context.Instance, trackBar);
                    bool asInt = true;
                    if (value is int)
                    {
                        trackBar.Value = (int)value;
                    }
                    else if (value is byte)
                    {
                        asInt = false;
                        trackBar.Value = (byte)value;
                    }
                    edSvc.DropDownControl(trackBar);
                    if (asInt)
                    {
                        value = trackBar.Value;
                    }
                    else
                    {
                        value = (byte)trackBar.Value;
                    }
                }
            }

            return value;
        }

        public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
        {
            if (context != null && context.Instance != null)
            {
                return UITypeEditorEditStyle.DropDown;
            }
            return base.GetEditStyle(context);
        }

        private void ValueChanged(object sender, EventArgs e)
        {
            if (edSvc != null)
            {
                edSvc.CloseDropDown();
            }
        }
    }
}
namespace Microsoft.Samples.WinForms.Cs.HostApp
{
    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.Samples.WinForms.Cs.FlashTrackBar;

    public class HostApp : System.Windows.Forms.Form
    {
        /// <summary>
        /// 必需的设计变量。
        /// </summary>
        private Container components;
        protected internal FlashTrackBar flashTrackBar1;

        public HostApp()
        {
            // Windows窗体设计器支持所需
            InitializeComponent();
        }

        /// <summary>
        /// 清理所有正在使用的资源。
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (components != null)
                {
                    components.Dispose();
                }
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// 设计器支持所需的方法-不要使用代码编辑器修改此方法的内容。
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new Container();
            this.flashTrackBar1 = new FlashTrackBar();
            this.Text = "Control Example";
            this.ClientSize = new Size(600, 450);
            flashTrackBar1.BackColor = Color.Black;
            flashTrackBar1.Dock = DockStyle.Fill;
            flashTrackBar1.TabIndex = 0;
            flashTrackBar1.ForeColor = Color.White;
            flashTrackBar1.Text = "拖动鼠标!";
            flashTrackBar1.Value = 73;
            flashTrackBar1.Size = new Size(200, 200);
            this.Controls.Add(this.flashTrackBar1);
        }

        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        public static void Main(string[] args)
        {
            Application.Run(new HostApp());
        }
    }
}

Windows 窗体控件中的属性

一个 Windows 窗体控件从基类 System.Windows.Forms.Control 中继承了许多属性。 这些属性包括 FontForeColorBackColorBoundsClientRectangleDisplayRectangleEnabledFocusedHeightWidthVisibleAutoSize 等等。

可以在控件中重写继承的属性和定义新属性。

在 Windows 窗体控件中定义属性

定义属性时需考虑以下重要注意事项:

  • 必须将特性应用于定义的属性。 特性指定设计器应如何显示属性。

  • 如果更改属性会影响控件的视觉显示,请从 set 访问器调用 Invalidate 方法,此方法是从 Control 继承的。 Invalidate 又会调用 OnPaint 方法,以重绘控件。 对 Invalidate 的多次调用会生成对 OnPaint 的一次调用,以提高效率。

  • .NET Framework 类库为常见数据类型(如整数、小数、布尔值和其他数据类型)提供了类型转换器。 类型转换器的用途通常是提供字符串到数值的转换(从字符串数据转换为其他数据类型)。 常见数据类型与默认类型转换器相关联,默认类型转换器可将数值转换为字符串,并将字符串转换为相应数据类型。 如果定义了自定义(即非标准)数据类型的属性,则应用的特性必须将类型转换器指定为与该属性相关联。 还可使用特性将自定义 UI 类型编辑器与某个属性相关联。 UI 类型编辑器提供一个用于编辑属性或数据类型的用户界面。 UI 类型编辑器的一个例子是颜色选取器。 本主题末尾给出了特性的示例。

    备注

    如果没有类型转换器或 UI 类型编辑器可供自定义属性使用,则可根据扩展设计时支持中所述来实现一个。

以下代码片段为自定义控件 FlashTrackBar 定义了一个名为 EndColor 的自定义属性。

public class FlashTrackBar : Control {  
   ...  
   // 支持EndColor属性的私有数据成员。 
   private Color endColor = Color.LimeGreen;  
   // Category属性告诉设计人员将其显示在Flash分组中。
   //Description属性提供属性的描述。
   [  
   Category("Flash"),  
   Description("进度条的结束颜色。")  
   ]  
   // 公共属性EndColor访问EndColor。
   public Color EndColor {  
      get {  
         return endColor;  
      }  
      set {  
         endColor = value;  
         if (baseBackground != null && showGradient) {  
            baseBackground.Dispose();  
            baseBackground = null;  
         }  
         // Invalidate方法调用OnPaint方法,该方法将重新绘制控件。 
         Invalidate();  
      }  
   }  
   ...  
}  

以下代码片段将类型转换器和 UI 类型编辑器与属性 Value 相关联。 在这种情况下,Value 是一个整数,且具有默认的类型转换器,但 TypeConverterAttribute 特性应用一个自定义类型转换器 (FlashTrackBarValueConverter),该类型转换器允许设计器按百分比形式显示特性。 UI 类型编辑器 FlashTrackBarValueEditor 允许以可视化方式显示百分比。 此示例还演示由 TypeConverterAttributeEditorAttribute 特性指定的类型转换器或编辑器如何重写默认转换器。

[  
Category("Flash"),
// 应用一个自定义类型转换器
TypeConverter(typeof(FlashTrackBarValueConverter)),  
// 指定要使用的属性进行更改编辑器
Editor(typeof(FlashTrackBarValueEditor), typeof(UITypeEditor)),  
Description("跟踪条的当前值。您可以输入实际值或百分比。")  
]  
public int Value {  
...  
}  

使用 ShouldSerialize 和 Reset 方法定义默认值

如果属性没有简单的默认值,ShouldSerializeReset 是可以为属性提供的可选方法。 如果属性有一个简单的默认值,则应改为应用 DefaultValueAttribute,并将默认值提供给属性类构造函数。 这两种机制都支持设计器中的以下功能:

  • 如果已将属性的值修改为非默认值,则该属性会在属性浏览器中提供视觉指示。

  • 用户可以右键单击该属性,然后选择“重置”,将属性还原为其默认值。

  • 设计器生成更高效的代码。

备注

应用 DefaultValueAttribute 或提供 ResetPropertyNameShouldSerializePropertyName 方法。 请勿同时使用两者。

声明 ShouldSerializeReset 方法时,请使用 private 访问修饰符。 这些方法通常由设计器调用,而不是由用户代码调用。

ResetPropertyName 方法将属性设置为其默认值,如以下代码片段所示。

private void ResetMyFont()
{
   MyFont = null;
}

备注

如果属性没有 Reset 方法,没有用 DefaultValueAttribute 标记,并且没有在其声明中提供默认值,则该属性的 Reset 选项在 Visual StudioWindows 窗体设计器的“属性”窗口的快捷菜单中禁用。

诸如 Visual Studio 之类的设计器可使用属性名 ShouldSerialize 方法来检查属性是否已不再使用其默认值,并仅在属性发生改变的情况下将代码写入窗体中,从而更有效地生成代码。 例如:

// 如果字体改变,返回true;否则,返回false。
// 只有当返回true时,设计器才会将代码写入表单。
private bool ShouldSerializeMyFont()
{
   return thefont != null;
}

提示

如果要永久阻止设计器序列化属性,请添加值为 HiddenDesignerSerializationVisibility 特性。

以下是完整的代码示例。

using System;
using System.Drawing;
using System.Windows.Forms;

public class MyControl : Control {
   // 声明Font类的实例并将其默认值设置为null。
   private Font thefont = null;

   // MyFont属性。
   public Font MyFont {
      // 注意,MyFont属性永远不会返回null。
      get {
         if (thefont != null) return thefont;
         // Parent:获取设置控件父容器
         if (Parent != null) return Parent.Font;
         return Control.DefaultFont;
      }
      set {
         thefont = value;
      }
   }

   private bool ShouldSerializeMyFont()
   {
      return thefont != null;
   }

   private void ResetMyFont()
   {
      MyFont = null;
   }
}

在这种情况下,即使 MyFont 属性访问的专用变量值为 null,属性浏览器也不会显示 null;而是显示父级的 Font 属性(如果它不是 nullControl 中定义的默认 Font 值)。 因此,不能简单地设置 MyFont 的默认值,并且不能将 DefaultValueAttribute 应用于此属性。 取而代之的是,必须为 MyFont 属性实现 ShouldSerializeReset 方法。

属性更改事件

如果希望控件在名为 PropertyName 的属性更改时发送通知,请定义一个名为 PropertyNameChanged 的事件和一个引发该事件的名为 OnPropertyNameChanged 的方法。 Windows 窗体中的命名约定是将单词 Changed 附加到属性的名称中。 属性更改事件的关联事件委托类型是 EventHandler,该事件数据类型是 EventArgs。 基类 Control 定义了许多属性更改事件,例如 BackColorChangedBackgroundImageChangedFontChangedLocationChanged 等。

属性更改事件很有用,因为它们允许控件的使用者附加响应更改的事件处理程序。 如果控件需要响应它引发的属性更改事件,请重写相应的 OnPropertyNameChanged 方法,而不是将委托附加到该事件。 控件通常通过更新其他属性或重绘其部分或全部绘图图面来响应属性更改事件。

以下示例显示 FlashTrackBar 自定义控件如何响应其继承自 Control 的一些属性更改事件。

protected override void OnTextChanged(EventArgs e) {
    base.OnTextChanged(e);
    Invalidate();
}

protected override void OnBackColorChanged(EventArgs e) {
    base.OnBackColorChanged(e);
    if ((baseBackground != null) && (!showGradient)) {
                baseBackground.Dispose();
                baseBackground = null;
    }
}

如何:公开构成控件的属性

构成复合控件的控件称为构成控件。 这些控件通常声明为私有,因此开发人员无法访问。 如果要使这些控件的属性可供将来的用户使用,则必须向用户公开它们。 通过在用户控件中创建属性,并使用该属性的 getset 访问器来影响构成控件的私有属性,可以公开构成控件的属性。

考虑一个假设的用户控件,其构成按钮名为 MyButton。 在本例中,当用户请求 ConstituentButtonBackColor 属性时,将传递存储在 MyButtonBackColor 属性中的值。 当用户向此属性赋值时,该值会自动传递给 MyButtonBackColor 属性,然后执行 set 代码,改变 MyButton 的颜色。

以下示例演示如何公开构成按钮的 BackColor 属性:

public Color ButtonColor
{
   get
   {
      return(myButton.BackColor);
   }
   set
   {
      myButton.BackColor = value;
   }
}

公开构成控件的属性
  1. 为用户控件创建公共属性。

  2. 在属性的 get 部分,编写用于检索要公开的属性的值的代码。

  3. 在属性的 set 部分中,编写代码,将属性的值传递给构成控件的公开属性。

自定义控件中的方法实现

控件中方法的实现与任何其他组件中方法的实现方式相同。

C# 不区分函数和过程。 方法返回值或返回 void。 声明 C# 公共方法的语法是:

public int ConvertMatterToEnergy(int matter)  
{  
   // 转换代码在这里。
}  

在声明方法时,应尽可能将它的所有参数都声明为显式数据类型。 接受对象引用的参数应被声明为特定的类类型,例如 As Widget 而不是 As Object

键入的参数允许编译器捕获由开发人员犯的很多错误,而不是在运行时才捕获。 编译器总是捕获错误,而运行时测试只相当于测试套件。

重载方法

如果想允许控件的用户为某个方法提供参数的不同组合,则应使用显式数据类型提供该方法的多个重载。 应避免创建声明 As Object 的参数,这样声明的参数可以包含任何数据类型,从而会产生测试时无法捕获的错误。

备注

公共语言运行时中的通用数据类型是 Object 而不是 Variant

例如,有一个假想的 Spin 控件,其 Widget 方法可能允许直接指定旋转方向和速度,或者允许指定另一个 Widget 对象作为角动力的来源:

public void Spin(SpinDirectionsEnum spinDirection, double revolutionsPerSecond)  
{  
   // 这里是实现代码。
}  
  
public void Spin(Widget driver)  
{  
   // 这里是实现代码。
} 

4. Windows 窗体控件中的事件

Windows 窗体控件从 System.Windows.Forms.Control 继承 60 多个事件。 其中包括 Paint 事件(引发控件绘制)、与显示窗口相关的事件(如 ResizeLayout 事件)以及低级别的鼠标和键盘事件。 某些低级别事件通过 Control 合成为语义事件,例如 ClickDoubleClick

如果自定义控件需要重写继承事件功能,请重写继承的 OnEventName 方法,而不附加委托。

重写 OnPaint 方法

重写 .NET Framework 中定义的任何事件的基本步骤是相同的,以下列表对其进行了总结。

重写继承的事件
  1. 重写受保护的 OnEventName 方法。

  2. 从重写的 OnEventName 方法调用基类的 OnEventName 方法,以便注册的委托接收事件。

此处详细介绍了 Paint 事件,因为每个 Windows 窗体控件都必须重写它从 Control 继承的 Paint 事件。 Control 基类不知道需要如何绘制派生控件,并且未在 OnPaint 方法中提供任何绘制逻辑。 ControlOnPaint 方法只是将 Paint 事件分派给已注册的事件接收器。

public class FirstControl : Control {  
   public FirstControl() {}  
   protected override void OnPaint(PaintEventArgs e) {  
      // 调用基类的OnPaint方法。 
      base.OnPaint(e);  
      // 调用System.Drawing.Graphics对象的方法。 
      e.Graphics.DrawString(Text, Font, new SolidBrush(ForeColor), ClientRectangle);  
   }
}

PaintEventArgs 类包含 Paint 事件的数据。 它具有两个属性,如以下代码所示。

public class PaintEventArgs : EventArgs {  
...  
    public System.Drawing.Rectangle ClipRectangle {}  
    public System.Drawing.Graphics Graphics {}  
...  
} 

ClipRectangle 是要绘制的矩形,Graphics 属性引用 Graphics 对象。 System.Drawing 命名空间中的类是托管类,它们提供对新 Windows 图形库 GDI+ 的功能的访问。 Graphics 对象具有绘制点、字符串、线条、弧线、椭圆形和许多其他形状的方法。

每当控件需要更改其视觉对象显示时,该控件都会调用其 OnPaint 方法。 此方法又会引发 Paint 事件。

处理用户输入

本主题介绍 System.Windows.Forms.Control 提供的主键盘和鼠标事件。 处理事件时,控件作者应重写受保护的 OnEventName 方法,而不是向事件附加委托。

备注

如果没有与事件关联的数据,则会将基类 EventArgs 的一个实例作为参数传递给 OnEventName 方法。

键盘事件

控件可以处理的常见键盘事件包括:KeyDownKeyPressKeyUp

事件名称要重写的方法事件说明
KeyDownvoid OnKeyDown(KeyEventArgs)仅在最初按下键时引发。
KeyPressvoid OnKeyPress (KeyPressEventArgs)每次按键时引发。 如果一直按住某个键,则按操作系统定义的重复速率引发 KeyPress 事件。
KeyUpvoid OnKeyUp(KeyEventArgs)在松开某个键时引发。

备注

处理键盘输入比重写上表中的事件要复杂得多,这超出了本主题的讨论范围。

鼠标事件

控件可以处理的鼠标事件包括:MouseDownMouseEnterMouseHoverMouseLeaveMouseMoveMouseUp

事件名称要重写的方法事件说明
MouseDownvoid OnMouseDown(MouseEventArgs)指针位于控件上时,按下鼠标按钮会引发该事件。
MouseEntervoid OnMouseEnter(EventArgs)指针首次进入控件区域时引发。
MouseHovervoid OnMouseHover(EventArgs)指针悬停在控件上时引发。
MouseLeavevoid OnMouseLeave(EventArgs)指针离开控件区域时引发。
MouseMovevoid OnMouseMove(MouseEventArgs)指针在控件区域中移动时引发。
MouseUpvoid OnMouseUp(MouseEventArgs)指针位于控件上或指针离开控件区域时,松开鼠标按钮会引发该事件。

以下代码片段演示了一个重写 MouseDown 事件的示例。

protected override void OnMouseDown(MouseEventArgs e) {
    base.OnMouseDown(e);
    if (!allowUserEdit) {
        return;
    }
    Capture = true;
    dragging = true;
    SetDragValue(new Point(e.X, e.Y));
}

以下代码片段演示了一个重写 MouseMove 事件的示例。

protected override void OnMouseMove(MouseEventArgs e) {
    base.OnMouseMove(e);
    if (!allowUserEdit || !dragging) {
        return;
    }
    SetDragValue(new Point(e.X, e.Y));
}

以下代码片段演示了一个重写 MouseUp 事件的示例。

protected override void OnMouseUp(MouseEventArgs e) {
    base.OnMouseUp(e);
    if (!allowUserEdit || !dragging) {
        return;
    }
    Capture = false;
    dragging = false;
    value = dragValue;
    OnValueChanged(EventArgs.Empty);
}

在 Windows 窗体控件中定义事件

如果你定义的事件没有任何关联的数据,则使用事件数据的基类型 EventArgs,并使用 EventHandler 作为事件委托。 剩下的工作就是定义一个事件成员和一个引发该事件的受保护的 On EventName 方法。

以下代码段显示了 FlashTrackBar 自定义控件如何定义自定义事件 ValueChanged

using System;  
using System.Windows.Forms;  
using System.Drawing;  
  
public class FlashTrackBar : Control {  
   // 事件没有任何数据,因此EventHandler作为事件委托就足够了。
   private EventHandler onValueChanged;  
   // 使用event关键字定义事件成员。
   // 在本例中,为了提高效率,使用事件属性构造定义事件。
   public event EventHandler ValueChanged {  
            add {  
                onValueChanged += value;  
            }  
            remove {  
                onValueChanged -= value;  
            }  
        }  
   // 当值实际发生更改时引发ValueChanged事件的受保护方法。派生控件可以重写此方法。
   protected virtual void OnValueChanged(EventArgs e)
   {  
       onValueChanged?.Invoke(this, e);  
   }  
}  

5. Windows 窗体控件中的特性

.NET Framework 提供了多种可应用于自定义控件和组件的成员的特性。 其中的一些特性会影响类的运行时行为,另一些会影响设计时行为。

注意

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

控件和组件属性的特性

下表显示了可应用于自定义控件和组件的属性或其他成员的特性。

属性说明
AmbientValueAttribute指定要传递给属性的值,以使该属性从另一个源中获取其值。 这称为“环境”。
BrowsableAttribute指定属性或事件是否应在“属性”窗口中显示。
CategoryAttribute指定当属性或事件显示在一个设置为 Categorized 模式的 PropertyGrid 控件中时,用于对属性或事件分组的类别的名称。
DefaultValueAttribute指定属性的默认值。
DescriptionAttribute指定属性或事件的说明。
DisplayNameAttribute为属性、事件或不采用参数的 public void 方法指定显示名称。
EditorAttribute指定用于更改属性的编辑器。
EditorBrowsableAttribute指定可在编辑器中查看的属性或方法。
HelpKeywordAttribute为类或成员指定上下文关键字。
LocalizableAttribute指定是否应本地化某一属性。
PasswordPropertyTextAttribute指示对象的文本表示形式被星号等字符隐匿。
ReadOnlyAttribute指定此特性绑定到的属性在设计时是只读还是可读/写。
RefreshPropertiesAttribute指示关联的属性值更改时应刷新属性网格。
TypeConverterAttribute指定对于此属性绑定到的对象要使用哪种类型作为转换器。

数据绑定属性的特性

下表显示了可用于指定自定义控件和组件如何与数据绑定之间进行交互的特性。

属性说明
BindableAttribute指定属性是否通常用于绑定。
ComplexBindingPropertiesAttribute指定组件的数据源和数据成员属性。
DefaultBindingPropertyAttribute指定组件的默认绑定属性。
LookupBindingPropertiesAttribute指定组件的数据源和数据成员属性。
AttributeProviderAttribute启用特性重定向。

类的特性

下表显示了可用于在设计时指定自定义控件和组件的行为的特性。

属性说明
DefaultEventAttribute指定组件的默认事件。
DefaultPropertyAttribute指定组件的默认属性。
DesignerAttribute指定用于为组件实现设计时服务的类。
DesignerCategoryAttribute指定类设计器属于某一类别。
ToolboxItemAttribute表示工具箱项的特性。
ToolboxItemFilterAttribute指定要用于工具箱项的筛选器字符串和筛选器类型。

如何:应用 Windows 窗体控件中的特性

若要开发与设计环境正确交互并在运行时正确执行的组件和控件,需要将特性正确应用于类和成员。

注意

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

示例

下面的代码示例演示如何对自定义控件使用多个特性。 此控件演示了简单的日志记录功能。 当控件绑定到数据源时,它会在 DataGridView 控件中显示数据源发送的值。 如果值超出 Threshold 属性指定的值,则会引发 ThresholdExceeded 事件。

AttributesDemoControl 使用 LogEntry 类记录值。 LogEntry 类是模板类,这意味着它在所记录的类型上参数化。 例如,如果 AttributesDemoControl 记录类型为 float 的值,则会按如下所示声明并使用每个 LogEntry 实例。

// 这个方法处理计时器的Elapsed事件。
// 它查询下一个值的性能计数器,将该值打包到一个LogEntry对象中,
// 并将新的LogEntry添加到由BindingSource管理的列表中。
private void timer1_Elapsed(
    object sender,
    System.Timers.ElapsedEventArgs e)
{	
    // 从性能计数器获取最新值。
    float val = this.performanceCounter1.NextValue();

    // 性能计数器返回float类型的值,但任何实现IComparable接口的类型都可以工作。
    LogEntry<float> entry = new LogEntry<float>(val, DateTime.Now);

    // 将新的LogEntry添加到BindingSource列表中。
    this.bindingSource1.Add(entry);
}

备注

由于 LogEntry 是由任意类型参数化的,因此,它必须使用反射对参数类型进行操作。 若要使阈值功能正常工作,参数类型 T 必须实现 IComparable 接口。

托管 AttributesDemoControl 的窗体定期查询性能计数器。 每个值都打包在适当类型的 LogEntry 中,并添加到窗体的 BindingSource 中。 AttributesDemoControl 通过它的数据绑定接收值,并在 DataGridView 控件中显示值。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Drawing;
using System.Data;
using System.Reflection;
using System.Text;
using System.Windows.Forms;

// 此示例演示了创建控件时使用的各种属性。
namespace AttributesDemoControlLibrary
{
    // 这是thresholdexceded事件的事件处理程序委托。
    public delegate void ThresholdExceededEventHandler(ThresholdExceededEventArgs e);

    // 这个控件演示了一个简单的日志记录功能。
    [ComplexBindingProperties("DataSource", "DataMember")]
    [DefaultBindingProperty("TitleText")]
    [DefaultEvent("ThresholdExceeded")]
    [DefaultProperty("Threshold")]
    [HelpKeywordAttribute(typeof(UserControl))]
    [ToolboxItem("System.Windows.Forms.Design.AutoSizeToolboxItem,System.Design")]
    public class AttributesDemoControl : UserControl
    {

        // 这支持Threshold属性。
        private object thresholdValue;

        // 包含值超过阈值的DataGridView单元格的默认前置颜色值。
        private static Color defaultAlertForeColorValue = Color.White;

        // 包含值超过阈值的DataGridView单元格的默认背景颜色值。
        private static Color defaultAlertBackColorValue = Color.Red;

        // 环境颜色值。
        private static Color ambientColorValue = Color.Empty;

        // 包含值超过阈值的DataGridView单元格的前置颜色值。
        private Color alertForeColorValue = defaultAlertForeColorValue;

        // 包含值超过阈值的DataGridView单元格的背景颜色值。
        private Color alertBackColorValue = defaultAlertBackColorValue;

        // 包含此UserControl的子控件。
        private TableLayoutPanel tableLayoutPanel1;
        private DataGridView dataGridView1;
        private Label label1;

        // 需要设计器的支持。
        private System.ComponentModel.IContainer components = null;

        // 无参构造函数
        public AttributesDemoControl()
        {
            InitializeComponent();
        }

        [Category("Appearance")]
        [Description("日志数据的标题。")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        [Localizable(true)]
        [HelpKeywordAttribute("AttributesDemoControlLibrary.AttributesDemoControl.TitleText")]
        public string TitleText
        {
            get
            {
                return this.label1.Text;
            }

            set
            {
                this.label1.Text = value;
            }
        }

        // 继承的Text属性在设计时隐藏,并在运行时引发异常。这强制要求客户端代码使用TitleText属性。
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public override string Text
        {
            get
            {
                throw new NotSupportedException();
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        [AmbientValue(typeof(Color), "Empty")]
        [Category("Appearance")]
        [DefaultValue(typeof(Color), "White")]
        [Description("用于绘制警告文本的颜色。")]
        public Color AlertForeColor
        {
            get
            {
                if (this.alertForeColorValue == Color.Empty &&
                    this.Parent != null)
                {
                    return Parent.ForeColor;
                }

                return this.alertForeColorValue;
            }

            set
            {
                this.alertForeColorValue = value;
            }
        }

        // 设计人员使用此方法将属性重置为默认值。
        public void ResetAlertForeColor()
        {
            this.AlertForeColor = AttributesDemoControl.defaultAlertForeColorValue;
        }

        // 此方法向设计器指示属性值是否与环境值不同,在这种情况下,设计器应保留该值。
        private bool ShouldSerializeAlertForeColor()
        {
            return (this.alertForeColorValue != AttributesDemoControl.ambientColorValue);
        }

        [AmbientValue(typeof(Color), "Empty")]
        [Category("Appearance")]
        [DefaultValue(typeof(Color), "Red")]
        [Description("绘制警告文本的背景颜色。")]
        public Color AlertBackColor
        {
            get
            {
                if (this.alertBackColorValue == Color.Empty &&
                    this.Parent != null)
                {
                    return Parent.BackColor;
                }

                return this.alertBackColorValue;
            }

            set
            {
                this.alertBackColorValue = value;
            }
        }

        // 设计人员使用此方法将属性重置为默认值。
        public void ResetAlertBackColor()
        {
            this.AlertBackColor = AttributesDemoControl.defaultAlertBackColorValue;
        }

        // 此方法向设计器指示属性值是否与环境值不同,在这种情况下,设计器应保留该值。
        private bool ShouldSerializeAlertBackColor()
        {
            return (this.alertBackColorValue != AttributesDemoControl.ambientColorValue);
        }

        [Category("Data")]
        [Description("指示控件的数据源。")]
        [RefreshProperties(RefreshProperties.Repaint)]
        [AttributeProvider(typeof(IListSource))]
        public object DataSource
        {
            get
            {
                return this.dataGridView1.DataSource;
            }

            set
            {
                this.dataGridView1.DataSource = value;
            }
        }

        [Category("Data")]
        [Description("指示要在控件中显示的数据源的子列表。")]
        public string DataMember
        {
            get
            {
                return this.dataGridView1.DataMember;
            }

            set
            {
                this.dataGridView1.DataMember = value;
            }
        }

        // 该属性通常将其BrowsableAttribute设置为false,
        // 但此代码演示使用ReadOnlyAttribute,因此BrowsableAttribute为true以在任何附加的PropertyGrid控件中显示它。
        [Browsable(true)]
        [Category("Behavior")]
        [Description("最新条目的时间戳。")]
        [ReadOnly(true)]
        public DateTime CurrentLogTime
        {
            get
            {
                int lastRowIndex =
                    this.dataGridView1.Rows.GetLastRow(
                    DataGridViewElementStates.Visible);

                if (lastRowIndex > -1)
                {
                    DataGridViewRow lastRow = this.dataGridView1.Rows[lastRowIndex];
                    DataGridViewCell lastCell = lastRow.Cells["EntryTime"];
                    return ((DateTime)lastCell.Value);
                }
                else
                {
                    return DateTime.MinValue;
                }
            }

            set
            {
            }
        }

        [Category("Behavior")]
        [Description("thresholdexceded事件将被引发的值。")]
        public object Threshold
        {
            get
            {
                return this.thresholdValue;
            }

            set
            {
                this.thresholdValue = value;
            }
        }

        // 此属性仅用于演示PasswordPropertyText属性。
        // 当此控件附加到PropertyGrid控件时,返回的字符串将显示为模糊字符,如星号。此属性没有其他作用。
        [Category("Security")]
        [Description("Demonstrates PasswordPropertyTextAttribute.")]
        [PasswordPropertyText(true)]
        public string Password
        {
            get
            {
                return "这是一个演示密码。";
            }
        }

        // 此属性仅用于演示DisplayName属性。
        // 当此控件附加到PropertyGrid控件时,属性将显示为“rename property”而不是“MisnamedProperty”。
        [Description("Demonstrates DisplayNameAttribute.")]
        [DisplayName("RenamedProperty")]
        public bool MisnamedProperty
        {
            get
            {
                return true;
            }
        }

        // 这是thresholdexceded事件的声明。
        public event ThresholdExceededEventHandler ThresholdExceeded;

        #region Implementation

        // 这是DataGridView控件的CellFormatting事件的事件处理程序。
        // 处理此事件允许AttributesDemoControl在数据源传入的日志项到达时检查它们。
		// 如果引发此事件的单元格包含日志条目的时间戳,则该单元格值将使用完整的日期/时间模式进行格式化。
		// 否则,假定单元格的值保存日志条目值。
        // 如果该值超过阈值,则用AlertForeColor和AlertBackColor属性指定的颜色绘制单元格,然后引发thresholdexceded。
        // 要使这种比较成功,日志条目的类型必须实现IComparable接口。
        private void dataGridView1_CellFormatting(
            object sender,
            DataGridViewCellFormattingEventArgs e)
        {
            try
            {
                if (e.Value != null)
                {
                    if (e.Value is DateTime)
                    {
                        // 以完整的日期/时间模式(长时间)显示日志记录时间。
                        e.CellStyle.Format = "F";
                    }
                    else
                    {
                        // 滚动到最近的条目。
                        DataGridViewRow row = this.dataGridView1.Rows[e.RowIndex];
                        DataGridViewCell cell = row.Cells[e.ColumnIndex];
                        this.dataGridView1.FirstDisplayedCell = cell;

                        if (this.thresholdValue != null)
                        {
                            // 获取日志条目的类型。
                            object val = e.Value;
                            Type paramType = val.GetType();

                            // 将日志条目值与阈值进行比较。使用反射在模板参数的类型上调用CompareTo方法。
                            int compareVal = (int)paramType.InvokeMember(
                                "CompareTo",
                                BindingFlags.Default | BindingFlags.InvokeMethod,
                                null,
                                e.Value,
                                new object[] { this.thresholdValue },
                                System.Globalization.CultureInfo.InvariantCulture);

                            // 如果日志条目值超过阈值,则设置单元格的前颜色和后颜色属性,并引发thresholdexceded事件。
                            if (compareVal > 0)
                            {
                                e.CellStyle.BackColor = this.alertBackColorValue;
                                e.CellStyle.ForeColor = this.alertForeColorValue;

                                ThresholdExceededEventArgs teea =
                                    new ThresholdExceededEventArgs(
                                    this.thresholdValue,
                                    e.Value);
                                this.ThresholdExceeded(teea);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
            this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
            this.dataGridView1 = new System.Windows.Forms.DataGridView();
            this.label1 = new System.Windows.Forms.Label();
            this.tableLayoutPanel1.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
            this.SuspendLayout();
            //
            // tableLayoutPanel1
            //
            this.tableLayoutPanel1.AutoSize = true;
            this.tableLayoutPanel1.ColumnCount = 1;
            this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 100F));
            this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 100F));
            this.tableLayoutPanel1.Controls.Add(this.dataGridView1, 0, 1);
            this.tableLayoutPanel1.Controls.Add(this.label1, 0, 0);
            this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.tableLayoutPanel1.Location = new System.Drawing.Point(10, 10);
            this.tableLayoutPanel1.Name = "tableLayoutPanel1";
            this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(10);
            this.tableLayoutPanel1.RowCount = 2;
            this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
            this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 80F));
            this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
            this.tableLayoutPanel1.Size = new System.Drawing.Size(425, 424);
            this.tableLayoutPanel1.TabIndex = 0;
            //
            // dataGridView1
            //
            this.dataGridView1.AllowUserToAddRows = false;
            this.dataGridView1.AllowUserToDeleteRows = false;
            this.dataGridView1.AllowUserToOrderColumns = true;
            this.dataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
            this.dataGridView1.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells;
            dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
            dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Control;
            dataGridViewCellStyle1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.WindowText;
            dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
            dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
            dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
            this.dataGridView1.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1;
            this.dataGridView1.ColumnHeadersHeight = 4;
            this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.dataGridView1.Location = new System.Drawing.Point(13, 57);
            this.dataGridView1.Name = "dataGridView1";
            this.dataGridView1.ReadOnly = true;
            this.dataGridView1.RowHeadersVisible = false;
            this.dataGridView1.Size = new System.Drawing.Size(399, 354);
            this.dataGridView1.TabIndex = 1;
            this.dataGridView1.CellFormatting += new System.Windows.Forms.DataGridViewCellFormattingEventHandler(this.dataGridView1_CellFormatting);
            //
            // label1
            //
            this.label1.AutoSize = true;
            this.label1.BackColor = System.Drawing.SystemColors.Control;
            this.label1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.label1.Location = new System.Drawing.Point(13, 13);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(399, 38);
            this.label1.TabIndex = 2;
            this.label1.Text = "label1";
            this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
            //
            // AttributesDemoControl
            //
            this.Controls.Add(this.tableLayoutPanel1);
            this.Name = "AttributesDemoControl";
            this.Padding = new System.Windows.Forms.Padding(10);
            this.Size = new System.Drawing.Size(445, 444);
            this.tableLayoutPanel1.ResumeLayout(false);
            this.tableLayoutPanel1.PerformLayout();
            ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
            this.ResumeLayout(false);
            this.PerformLayout();
        }

        #endregion
    }

    // 这是thresholdexceded事件的EventArgs类。
    public class ThresholdExceededEventArgs : EventArgs
    {
        private object thresholdValue = null;
        private object exceedingValue = null;

        public ThresholdExceededEventArgs(
            object thresholdValue,
            object exceedingValue)
        {
            this.thresholdValue = thresholdValue;
            this.exceedingValue = exceedingValue;
        }

        public object ThresholdValue
        {
            get
            {
                return this.thresholdValue;
            }
        }

        public object ExceedingValue
        {
            get
            {
                return this.exceedingValue;
            }
        }
    }

    // 这个类封装了一个日志条目。它是一个参数化类型(也称为模板类)。
    // 参数类型T定义了记录的数据类型。要使阈值检测工作,该类型必须实现IComparable接口。
    [TypeConverter("LogEntryTypeConverter")]
    public class LogEntry<T> where T : IComparable
    {
        private T entryValue;
        private DateTime entryTimeValue;

        public LogEntry(
            T value,
            DateTime time)
        {
            this.entryValue = value;
            this.entryTimeValue = time;
        }

        public T Entry
        {
            get
            {
                return this.entryValue;
            }
        }

        public DateTime EntryTime
        {
            get
            {
                return this.entryTimeValue;
            }
        }

        // 这是LogEntry类的TypeConverter。
        public class LogEntryTypeConverter : TypeConverter
        {
            public override bool CanConvertFrom(
                ITypeDescriptorContext context,
                Type sourceType)
            {
                if (sourceType == typeof(string))
                {
                    return true;
                }

                return base.CanConvertFrom(context, sourceType);
            }

            public override object ConvertFrom(
                ITypeDescriptorContext context,
                System.Globalization.CultureInfo culture,
                object value)
            {
                if (value is string)
                {
                    string[] v = ((string)value).Split(new char[] { '|' });

                    Type paramType = typeof(T);
                    T entryValue = (T)paramType.InvokeMember(
                        "Parse",
                        BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod,
                        null,
                        null,
                        new string[] { v[0] },
                        culture);

                    return new LogEntry<T>(
                        entryValue,
                        DateTime.Parse(v[2]));
                }

                return base.ConvertFrom(context, culture, value);
            }

            public override object ConvertTo(
                ITypeDescriptorContext context,
                System.Globalization.CultureInfo culture,
                object value,
                Type destinationType)
            {
                if (destinationType == typeof(string))
                {
                    LogEntry<T> le = value as LogEntry<T>;

                    string stringRepresentation =
                        String.Format("{0} | {1}",
                        le.Entry,
                        le.EntryTime);

                    return stringRepresentation;
                }

                return base.ConvertTo(context, culture, value, destinationType);
            }
        }
    }
}

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
using AttributesDemoControlLibrary;

// 这个示例演示了如何使用AttributesDemoControl来记录来自数据源的数据。
namespace AttributesDemoControlTest
{
    public class Form1 : Form
    {	
        private BindingSource bindingSource1;
        private System.Diagnostics.PerformanceCounter performanceCounter1;
        private Button startButton;
        private Button stopButton;
        private System.Timers.Timer timer1;
        private ToolStripStatusLabel statusStripPanel1;
        private NumericUpDown numericUpDown1;
        private GroupBox groupBox1;
        private GroupBox groupBox2;
        private TableLayoutPanel tableLayoutPanel1;
        private AttributesDemoControl attributesDemoControl1;
        private System.ComponentModel.IContainer components = null;

        // 这个表单使用AttributesDemoControl来显示一个LogEntry对象流。
        // 数据流是通过轮询性能计数器并通过数据绑定将计数器值传递给控件来生成的。
        public Form1()
        {
            InitializeComponent();

            // 将阈值上下控件的初始值设置为该控件的阈值。
            this.numericUpDown1.Value =
                (decimal)(float)this.attributesDemoControl1.Threshold;

            // 将性能计数器的名称分配给控件的标题文本。
            this.attributesDemoControl1.TitleText =
                this.performanceCounter1.CounterName;
        }

        // 这个方法处理thresholdexceded事件。它将超出阈值的值发布到状态条。
        private void attributesDemoControl1_ThresholdExceeded(
            ThresholdExceededEventArgs e)
        {
            string msg = String.Format(
                "{0}: Value {1} exceeded threshold {2}",
                this.attributesDemoControl1.CurrentLogTime,
                e.ExceedingValue,
                e.ThresholdValue);

            this.ReportStatus( msg );
        }

        // 这个方法处理计时器的Elapsed事件。
        // 它查询下一个值的性能计数器,将该值打包到一个LogEntry对象中,并将新的LogEntry添加到由BindingSource管理的列表中。
        private void timer1_Elapsed(
            object sender,
            System.Timers.ElapsedEventArgs e)
        {	
            // 从性能计数器获取最新值。
            float val = this.performanceCounter1.NextValue();

            // 性能计数器返回float类型的值,但任何实现IComparable接口的类型都可以工作。
            LogEntry<float> entry = new LogEntry<float>(val, DateTime.Now);

            // 将新的LogEntry添加到BindingSource列表中。
            this.bindingSource1.Add(entry);
        }

        private void numericUpDown1_ValueChanged(object sender, EventArgs e)
        {
            this.attributesDemoControl1.Threshold =
                (float)this.numericUpDown1.Value;

            string msg = String.Format(
                "Threshold changed to {0}",
                this.attributesDemoControl1.Threshold);

            this.ReportStatus(msg);
        }

        private void startButton_Click(object sender, EventArgs e)
        {
            this.ReportStatus(DateTime.Now + ": Starting");

            this.timer1.Start();
        }

        private void stopButton_Click(object sender, EventArgs e)
        {
            this.ReportStatus(DateTime.Now + ": Stopping");

            this.timer1.Stop();
        }

        private void ReportStatus(string msg)
        {
            if (msg != null)
            {
                this.statusStripPanel1.Text = msg;
            }
        }

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.Run(new Form1());
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.bindingSource1 = new System.Windows.Forms.BindingSource(this.components);
            this.performanceCounter1 = new System.Diagnostics.PerformanceCounter();
            this.startButton = new System.Windows.Forms.Button();
            this.stopButton = new System.Windows.Forms.Button();
            this.timer1 = new System.Timers.Timer();
            this.statusStripPanel1 = new System.Windows.Forms.ToolStripStatusLabel();
            this.numericUpDown1 = new System.Windows.Forms.NumericUpDown();
            this.groupBox1 = new System.Windows.Forms.GroupBox();
            this.groupBox2 = new System.Windows.Forms.GroupBox();
            this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
            this.attributesDemoControl1 = new AttributesDemoControlLibrary.AttributesDemoControl();
            ((System.ComponentModel.ISupportInitialize)(this.bindingSource1)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.performanceCounter1)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.timer1)).BeginInit();

            ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).BeginInit();
            this.groupBox1.SuspendLayout();
            this.groupBox2.SuspendLayout();
            this.tableLayoutPanel1.SuspendLayout();
            this.SuspendLayout();
            //
            // performanceCounter1
            //
            this.performanceCounter1.CategoryName = ".NET CLR Memory";
            this.performanceCounter1.CounterName = "Gen 0 heap size";
            this.performanceCounter1.InstanceName = "_Global_";
            //
            // startButton
            //
            this.startButton.Location = new System.Drawing.Point(31, 25);
            this.startButton.Name = "startButton";
            this.startButton.TabIndex = 1;
            this.startButton.Text = "Start";
            this.startButton.Click += new System.EventHandler(this.startButton_Click);
            //
            // stopButton
            //
            this.stopButton.Location = new System.Drawing.Point(112, 25);
            this.stopButton.Name = "stopButton";
            this.stopButton.TabIndex = 2;
            this.stopButton.Text = "Stop";
            this.stopButton.Click += new System.EventHandler(this.stopButton_Click);
            //
            // timer1
            //
            this.timer1.Interval = 1000;
            this.timer1.SynchronizingObject = this;
            this.timer1.Elapsed += new System.Timers.ElapsedEventHandler(this.timer1_Elapsed);
            //
            // statusStripPanel1
            //
            this.statusStripPanel1.BorderStyle = System.Windows.Forms.Border3DStyle.SunkenOuter;
            this.statusStripPanel1.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
            this.statusStripPanel1.Name = "statusStripPanel1";
            this.statusStripPanel1.Text = "Ready";
            //
            // numericUpDown1
            //
            this.numericUpDown1.Location = new System.Drawing.Point(37, 29);
            this.numericUpDown1.Maximum = new decimal(new int[] {
            1410065408,
            2,
            0,
            0});
            this.numericUpDown1.Name = "numericUpDown1";
            this.numericUpDown1.TabIndex = 7;
            this.numericUpDown1.ValueChanged += new System.EventHandler(this.numericUpDown1_ValueChanged);
            //
            // groupBox1
            //
            this.groupBox1.Anchor = System.Windows.Forms.AnchorStyles.None;
            this.groupBox1.Controls.Add(this.numericUpDown1);
            this.groupBox1.Location = new System.Drawing.Point(280, 326);
            this.groupBox1.Name = "groupBox1";
            this.groupBox1.Size = new System.Drawing.Size(200, 70);
            this.groupBox1.TabIndex = 13;
            this.groupBox1.TabStop = false;
            this.groupBox1.Text = "Threshold Value";
            //
            // groupBox2
            //
            this.groupBox2.Anchor = System.Windows.Forms.AnchorStyles.None;
            this.groupBox2.Controls.Add(this.startButton);
            this.groupBox2.Controls.Add(this.stopButton);
            this.groupBox2.Location = new System.Drawing.Point(26, 327);
            this.groupBox2.Name = "groupBox2";
            this.groupBox2.Size = new System.Drawing.Size(214, 68);
            this.groupBox2.TabIndex = 14;
            this.groupBox2.TabStop = false;
            this.groupBox2.Text = "Logging";
            //
            // tableLayoutPanel1
            //
            this.tableLayoutPanel1.ColumnCount = 2;
            this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
            this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
            this.tableLayoutPanel1.Controls.Add(this.groupBox2, 0, 1);
            this.tableLayoutPanel1.Controls.Add(this.groupBox1, 1, 1);
            this.tableLayoutPanel1.Controls.Add(this.attributesDemoControl1, 0, 0);
            this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
            this.tableLayoutPanel1.Name = "tableLayoutPanel1";
            this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(10);
            this.tableLayoutPanel1.RowCount = 2;
            this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 80F));
            this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F));
            this.tableLayoutPanel1.Size = new System.Drawing.Size(514, 411);
            this.tableLayoutPanel1.TabIndex = 15;
            //
            // attributesDemoControl1
            //
            this.tableLayoutPanel1.SetColumnSpan(this.attributesDemoControl1, 2);
            this.attributesDemoControl1.DataMember = "";
            this.attributesDemoControl1.DataSource = this.bindingSource1;
            this.attributesDemoControl1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.attributesDemoControl1.Location = new System.Drawing.Point(13, 13);
            this.attributesDemoControl1.Name = "attributesDemoControl1";
            this.attributesDemoControl1.Padding = new System.Windows.Forms.Padding(10);
            this.attributesDemoControl1.Size = new System.Drawing.Size(488, 306);
            this.attributesDemoControl1.TabIndex = 0;
            this.attributesDemoControl1.Threshold = 200000F;
            this.attributesDemoControl1.TitleText = "TITLE";
            this.attributesDemoControl1.ThresholdExceeded += new AttributesDemoControlLibrary.ThresholdExceededEventHandler(this.attributesDemoControl1_ThresholdExceeded);
            //
            // Form1
            //
            this.BackColor = System.Drawing.SystemColors.Control;
            this.ClientSize = new System.Drawing.Size(514, 430);
            this.Controls.Add(this.tableLayoutPanel1);
            this.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.Name = "Form1";
            this.Text = "Form1";
            ((System.ComponentModel.ISupportInitialize)(this.bindingSource1)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.performanceCounter1)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.timer1)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.numericUpDown1)).EndInit();
            this.groupBox1.ResumeLayout(false);
            this.groupBox2.ResumeLayout(false);
            this.tableLayoutPanel1.ResumeLayout(false);
            this.ResumeLayout(false);
            this.PerformLayout();
        }
    }
}

第一个代码示例是 AttributesDemoControl 实现。 第二个代码示例演示了使用 AttributesDemoControl 的一个窗体。

类级别特性

某些特性在类级别应用。 下面的代码示例演示通常应用于 Windows 窗体控件的特性。

// 这个控件演示了一个简单的日志记录功能。
[ComplexBindingProperties("DataSource", "DataMember")]
[DefaultBindingProperty("TitleText")]
[DefaultEvent("ThresholdExceeded")]
[DefaultProperty("Threshold")]
[HelpKeywordAttribute(typeof(UserControl))]
[ToolboxItem("System.Windows.Forms.Design.AutoSizeToolboxItem,System.Design")]
public class AttributesDemoControl : UserControl
{

TypeConverter 特性

TypeConverterAttribute 是另一个常用的类级别特性。 下面的代码示例显示它针对 LogEntry 类的使用。 此示例还显示了针对 LogEntry 类型的 TypeConverter 实现,称为 LogEntryTypeConverter

// 这个类封装了一个日志条目。它是一个参数化类型(也称为模板类)。参数类型T定义了记录的数据类型。
// 要使阈值检测工作,该类型必须实现IComparable接口。
[TypeConverter("LogEntryTypeConverter")]
public class LogEntry<T> where T : IComparable
{
    private T entryValue;
    private DateTime entryTimeValue;

    public LogEntry(
        T value,
        DateTime time)
    {
        this.entryValue = value;
        this.entryTimeValue = time;
    }

    public T Entry
    {
        get
        {
            return this.entryValue;
        }
    }

    public DateTime EntryTime
    {
        get
        {
            return this.entryTimeValue;
        }
    }

    // 这是LogEntry类的TypeConverter。
    public class LogEntryTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(
            ITypeDescriptorContext context,
            Type sourceType)
        {
            if (sourceType == typeof(string))
            {
                return true;
            }

            return base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(
            ITypeDescriptorContext context,
            System.Globalization.CultureInfo culture,
            object value)
        {
            if (value is string)
            {
                string[] v = ((string)value).Split(new char[] { '|' });

                Type paramType = typeof(T);
                T entryValue = (T)paramType.InvokeMember(
                    "Parse",
                    BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod,
                    null,
                    null,
                    new string[] { v[0] },
                    culture);

                return new LogEntry<T>(
                    entryValue,
                    DateTime.Parse(v[2]));
            }

            return base.ConvertFrom(context, culture, value);
        }

        public override object ConvertTo(
            ITypeDescriptorContext context,
            System.Globalization.CultureInfo culture,
            object value,
            Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                LogEntry<T> le = value as LogEntry<T>;

                string stringRepresentation =
                    String.Format("{0} | {1}",
                    le.Entry,
                    le.EntryTime);

                return stringRepresentation;
            }

            return base.ConvertTo(context, culture, value, destinationType);
        }
    }
}

成员级别特性

某些特性在成员级别应用。 下面的代码示例显示了通常应用于 Windows 窗体控件的属性的某些特性。

[Category("Appearance")]
[Description("The title of the log data.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Localizable(true)]
[HelpKeywordAttribute("AttributesDemoControlLibrary.AttributesDemoControl.TitleText")]
public string TitleText
{
    get
    {
        return this.label1.Text;
    }

    set
    {
        this.label1.Text = value;
    }
}

AmbientValue 特性

以下示例展示了 AmbientValueAttribute 并显示了支持它与设计环境的交互的代码。 这种交互称为“氛围”。

[AmbientValue(typeof(Color), "Empty")]
[Category("Appearance")]
[DefaultValue(typeof(Color), "White")]
[Description("用于绘制警告文本的颜色。")]
public Color AlertForeColor
{
    get
    {
        if (this.alertForeColorValue == Color.Empty &&
            this.Parent != null)
        {
            return Parent.ForeColor;
        }

        return this.alertForeColorValue;
    }

    set
    {
        this.alertForeColorValue = value;
    }
}

// 设计人员使用此方法将属性重置为默认值。
public void ResetAlertForeColor()
{
    this.AlertForeColor = AttributesDemoControl.defaultAlertForeColorValue;
}

// 此方法向设计器指示属性值是否与环境值不同,在这种情况下,设计器应保留该值。
private bool ShouldSerializeAlertForeColor()
{
    return (this.alertForeColorValue != AttributesDemoControl.ambientColorValue);
}

数据绑定特性

以下示例展示了复杂数据绑定的实现。 前面显示的类级别 ComplexBindingPropertiesAttribute 指定要用于数据绑定的 DataSourceDataMember 属性。 AttributeProviderAttribute 指定 DataSource 属性将绑定到的类型。

[Category("Data")]
[Description("指示控件的数据源。")]
[RefreshProperties(RefreshProperties.Repaint)]
[AttributeProvider(typeof(IListSource))]
public object DataSource
{
    get
    {
        return this.dataGridView1.DataSource;
    }

    set
    {
        this.dataGridView1.DataSource = value;
    }
}


[Category("Data")]
[Description("指示要在控件中显示的数据源的子列表。")]
public string DataMember
{
    get
    {
        return this.dataGridView1.DataMember;
    }

    set
    {
        this.dataGridView1.DataMember = value;
    }
}

编译代码
  • 托管 AttributesDemoControl 的窗体需要引用 AttributesDemoControl 程序集才能生成。

6. 自定义控件的绘制和呈现

控件的自定义绘制是 .NET Framework 可以轻松完成的众多复杂任务之一。 创作自定义控件时,有许多关于控件图形外观的选项。 如果要创作从 Control 继承的控件,则必须提供代码,让控件可以呈现其图形表示形式。 如果通过从 UserControl 继承来创建用户控件,或者从 Windows 窗体控件之一继承,则可以替代标准图形表示形式并提供自己的图形代码。 如果要为所创作的 UserControl 构成控件提供自定义呈现,选项会变得更为有限,但仍允许各种控件和应用程序的图形表示形式。

呈现 Windows 窗体控件

呈现是指在用户屏幕上创建视觉对象表示形式的过程。 Windows 窗体使用 GDI(新的 Windows 图形库)进行呈现。 提供 GDI 访问权限的托管类位于 System.Drawing 命名空间及其子命名空间中。

控件呈现涉及以下元素:

  • 基类 System.Windows.Forms.Control 提供的绘图功能。

  • GDI 图形库的基本元素。

  • 绘图区域的几何图形。

  • 释放图形资源的过程。

控件提供的绘图功能

基类 Control 通过其 Paint 事件提供绘图功能。 每当控件需要更新其显示时,它都会引发 Paint 事件。

Paint 事件的事件数据类 PaintEventArgs 包含绘制控件所需的数据,即图形对象的句柄和表示绘制区域的矩形对象。 这些对象在以下代码片段中以粗体显示。

public class PaintEventArgs : EventArgs, IDisposable {  
public System.Drawing.Rectangle ClipRectangle {get;}  
public System.Drawing.Graphics Graphics {get;}  
// 其他属性和方法。
...  
}  

Graphics 是一个可封装绘图功能的托管类,如本主题后面的 GDI 讨论中所述。 ClipRectangleRectangle 结构的实例,它定义了可在其中绘制控件的可用区域。 控件开发人员可以使用控件的 ClipRectangle 属性来计算 ClipRectangle,如本主题后面的几何图形讨论中所述。

控件必须通过重写它从 Control 继承的 OnPaint 方法来提供呈现逻辑。 OnPaint 可访问图形对象和矩形,以通过传递给它的 PaintEventArgs 实例的 GraphicsClipRectangle 属性进行绘制。

protected virtual void OnPaint(PaintEventArgs pe);  

Control 基类的 OnPaint 方法不实现任何绘图功能,只是调用注册到 Paint 事件的事件委托。 重写 OnPaint 时,应确保调用基类的 OnPaint 方法,以便注册的委托可接收 Paint 事件。 但是,绘制整个表面的控件不应调用基类的 OnPaint,因为这会引起闪烁。

备注

请勿直接从控件调用 OnPaint;请改为调用 Invalidate 方法(从 Control 继承)或调用 Invalidate 的其他方法。 Invalidate 方法又会调用 OnPaint。 重载 Invalidate 方法,并且根据提供给 Invalidate 的参数,控件将重绘其部分屏幕区域或整个屏幕区域。

Control 基类定义了另一种可用于绘图的方法,即 OnPaintBackground 方法。

protected virtual void OnPaintBackground(PaintEventArgs pevent);  

OnPaintBackground 绘制窗口的背景(并以相同方式绘制形状),并保证速度较快,而 OnPaint 绘制详细信息,速度可能较慢,因为单个绘制请求会合并为一个 Paint 事件,其中涵盖了所有需要重新绘制的区域。 例如,如果想要为控件绘制颜色渐变的背景,则可能需要调用 OnPaintBackground

虽然 OnPaintBackground 具有类似事件的命名法并采用与 OnPaint 方法相同的参数,但 OnPaintBackground 不是真正的事件方法。 不存在 PaintBackground 事件,并且 OnPaintBackground 不调用事件委托。 重写 OnPaintBackground 方法时,无需使用派生类即可调用其基类的 OnPaintBackground 方法。

GDI+ 基础知识

Graphics 类提供用于绘制各种形状(如圆形、三角形、弧形和椭圆形)的方法,以及用于显示文本的方法。 System.Drawing 命名空间及其子命名空间包含类,可用于封装图形元素,如形状(圆形、矩形、弧形等)、颜色、字体、画笔等。

绘图区域的几何图形

控件的 ClientRectangle 属性指定可用于用户屏幕上的控件的矩形区域,而 PaintEventArgsClipRectangle 属性指定实际绘制的区域。 (请记住,绘制是在将 PaintEventArgs 实例作为其参数的 Paint 事件方法中完成的)。 当控件的一小部分显示发生变化时,控件可能只需要绘制部分可用区域。 在这些情况下,控件开发人员必须计算要在其中进行绘制的实际矩形,并将其传递到 Invalidate。 采用 RectangleRegion 作为参数的 Invalidate 的重载版本使用该参数生成 PaintEventArgsClipRectangle 属性。

以下代码片段显示了 FlashTrackBar 自定义控件如何计算要绘制的矩形区域。 client 变量表示 ClipRectangle 属性。

Rectangle invalid = new Rectangle(
    client.X + min,
    client.Y,
    max - min,
    client.Height);
​
Invalidate(invalid);

释放图形资源

图形对象成本昂贵,因为它们使用系统资源。 此类对象包括 System.Drawing.Graphics 类的实例以及 System.Drawing.BrushSystem.Drawing.Pen 和其他图形类的实例。 请务必仅在需要时才创建图形资源,并在使用完之后立即释放。 如果创建实现 IDisposable 接口的类型,请在完成后调用其 Dispose 方法以释放资源。

以下代码片段显示了 FlashTrackBar 自定义控件如何创建和释放 Brush 资源。

private Brush baseBackground = null;

base.OnPaint(e);
if (baseBackground == null) {
    if (showGradient) {
        baseBackground = new LinearGradientBrush(new Point(0, 0),
                                                 new Point(ClientSize.Width, 0),
                                                 StartColor,
                                                 EndColor);
    }
    else if (BackgroundImage != null) {
        baseBackground = new TextureBrush(BackgroundImage);
    }
    else {
        baseBackground = new SolidBrush(BackColor);
    }
}

protected override void OnResize(EventArgs e) {
    base.OnResize(e);
    if (baseBackground != null) {
        baseBackground.Dispose();
        baseBackground = null;
    }
}

用户描述的控件

.NET Framework 使你能够轻松开发自己的控件。 可以创建一个用户控件,这是一组由代码绑定在一起的标准控件,也可以从头开始设计自己的控件。 甚至可以使用继承来创建从现有控件继承的控件并添加到其固有功能。 无论使用哪种方法,.NET Framework 都提供为创建的任何控件绘制自定义图形界面的功能。

控件的绘制是通过执行控件的 OnPaint 方法中的代码来完成的。 OnPaint 方法的单个参数是一个 PaintEventArgs 对象,它提供呈现控件所需的所有信息和功能。 PaintEventArgs 提供了两个将用于呈现控件的主要对象作为属性:

  • ClipRectangle 对象 - 表示将要绘制的控件部分的矩形。 这可以是整个控件,也可以是控件的一部分,具体取决于控件的绘制方式。

  • Graphics 对象 - 封装了几个提供绘制控件所需功能的面向图形的对象和方法。

每当在屏幕上绘制或刷新控件时都会触发 OnPaint 事件,并且 ClipRectangle 对象表示将在其中进行绘制的矩形。 如果需要刷新整个控件,ClipRectangle 将代表整个控件的大小。 但是,如果只需要刷新控件的一部分,则 ClipRectangle 对象将仅表示需要重绘的区域。 这种情况的一个例子是当一个控件被用户界面中的另一个控件或窗体部分遮挡时。

Control 类继承时,必须重写 OnPaint 方法并在其中提供图形呈现代码。 如果要为用户控件或继承的控件提供自定义图形界面,也可以通过重写 OnPaint 方法来实现。 下面显示了一个示例:

protected override void OnPaint(PaintEventArgs e)  
{  
   // 调用基类的OnPaint方法。
   base.OnPaint(e);  
  
   // 声明并实例化一个Pen。 
   using (System.Drawing.Pen myPen = new System.Drawing.Pen(Color.Aqua))  
   {
      // 在控件表示的矩形中绘制水色矩形。 
      e.Graphics.DrawRectangle(myPen, new Rectangle(this.Location,
         this.Size));  
   }
}  

前面的示例演示了如何使用非常简单的图形表示形式来呈现控件。 它调用基类的 OnPaint 方法,创建一个用于绘制的 Pen 对象,最后在由控件的 LocationSize 确定的矩形中绘制一个椭圆。 尽管大多数呈现代码会比这复杂得多,但此示例演示了如何使用 PaintEventArgs 对象中包含的 Graphics 对象。 请注意,如果从已具有图形表示形式的类(例如 UserControlButton)继承,并且不希望将该表示形式合并到呈现中,则不应调用基类的 OnPaint 方法。

控件的 OnPaint 方法中的代码将在第一次绘制控件以及每次刷新该控件时执行。 若要确保在每次调整控件大小时都重新进行绘制,请将下面的行添加到控件的构造函数中:

SetStyle(ControlStyles.ResizeRedraw, true);  

备注

使用 Control.Region 属性实现非矩形控件。

构成控件

组成用户控件的控件(也称作“构成控件”)在自定义图形呈现方面的灵活性相对较差。 所有 Windows 窗体控件都通过各自的 OnPaint 方法处理自己的呈现。 由于此方法受到保护,开发人员无法对其进行访问,因此在绘制控件时无法阻止其执行。 然而,这并不意味着不能添加影响构成控件外观的代码。 附加呈现可通过添加事件处理程序来完成。 例如,假设正在创作一个 UserControl 控件,该控件有一个名为 MyButton 的按钮。 如果想要获取 Button 提供的附加呈现,则需将类似如下的代码添加到用户控件中:

// 将事件处理程序添加到按钮的Paint事件。
MyButton.Paint +=
   new System.Windows.Forms.PaintEventHandler (this.MyPaint);  
// 创建自定义绘画方法。 
protected void MyPaint (object sender,
System.Windows.Forms.PaintEventArgs e)  
{  
   // 其他渲染代码在这里。
}  

备注

某些 Windows 窗体控件(如 TextBox)由 Windows 直接绘制。 在这些情况下,永远不会调用 OnPaint 方法,因此永远不会调用上面的示例。

上例创建一个每次执行 MyButton.Paint 事件时都会执行的方法,用于将附加的图形化表示形式添加到控件中。 请注意,这并不妨碍 MyButton.OnPaint 的执行,因此,除了自定义绘制外,仍会执行通常由某个按钮执行的所有绘制操作。 如果希望控件具有唯一的表示形式,则最好创建一个继承的控件,为其编写自定义呈现代码。

如何:使控件在运行时不可见

有时,你可能想要创建一个在运行时不可见的用户控件。 例如,一个除了警报响起时、其他时间不可见的闹钟控件。 只需要设置 Visible 属性即可轻松实现。 如果 Visible 属性是 true,则控件将正常显示。 如果为 false,则将隐藏控件。 尽管控件中的代码仍然可以在不可见的情况下运行,但将无法通过用户界面与控件进行交互。 如果要创建仍可响应用户输入(例如,单击鼠标)的不可见控件,则应创建透明控件。

使控件在运行时不可见

Visible 属性设置为 false

// 在对象自己的代码中设置可见属性。
this.Visible = false;  
// 从另一个对象设置可见属性。 
myControl1.Visible = false;  

如何:使控件拥有透明背景

在早期版本的.NET Framework 中,如果事先未在窗体的构造函数中设置 SetStyle 方法,控件将不支持设置透明背景色。 在当前的框架版本中,可以在设计时在“属性” Transparent窗口中或在窗体构造函数的代码中将背景色设置为

备注

Windows 窗体控件不支持真正的透明。 透明 Windows 窗体控件的背景由其父级绘制。

备注

即使将 Button 属性设置为 BackColorTransparent控件也不支持透明背景色。

使控件拥有透明背景色

  • 在“属性”窗口中,选择 BackColor 属性并将其设置为 Transparent

使用视觉样式呈现控件

.NET Framework 使用操作系统中受支持的视觉样式为呈现控件和其他 Windows 用户界面 (UI) 元素提供支持。 本主题讨论 .NET Framework 中对使用操作系统当前视觉样式呈现控件和其他 UI 元素提供的多种级别的支持。

公共控件的呈现类

呈现控件是指绘制控件的用户界面。 System.Windows.Forms 命名空间提供了用来呈现某些公共 Windows 窗体控件的 ControlPaint 类。 但是,此类以经典 Windows 样式绘制的控件,当在启用了视觉样式的应用程序中绘制自定义控件时,难以维护 UI 体验的一致性。

.NET Framework 2.0 包括 System.Windows.Forms 命名空间中使用视觉样式呈现部件和常见控件状态的类。 每个这样的类都包括使用操作系统当前视觉样式绘制控件和特定状态控件部件的 static 方法。

其中一些类旨在绘制相关控件,而不考虑视觉样式是否可用。 如果启用了视觉样式,类成员将使用视觉样式绘制相关控件;如果禁用了视觉样式,类成员将以经典 Windows 样式绘制控件。 这些类包括:

  • ButtonRenderer

  • CheckBoxRenderer

  • GroupBoxRenderer

  • RadioButtonRenderer

视觉样式可用时,其他类才能绘制相关控件,如果禁用了视觉样式,则类成员会引发异常。 这些类包括:

  • ComboBoxRenderer

  • ProgressBarRenderer

  • ScrollBarRenderer

  • TabRenderer

  • TextBoxRenderer

  • TrackBarRenderer

视觉样式元素和呈现类

System.Windows.Forms.VisualStyles 命名空间包含用于绘制和获取视觉样式支持的任何控件或 UI 元素信息的类。 支持的控件包括在 System.Windows.Forms 命名空间中具有呈现类的公共控件(请参阅上一节)以及诸如选项卡控件和 rebar 控件的其他控件。 其他受支持的 UI 元素包括“开始” 菜单、任务栏和 Windows 非工作区的各部分。

System.Windows.Forms.VisualStyles 命名空间的主要类为 VisualStyleElementVisualStyleRendererVisualStyleElement 是一个基础类,用于标识视觉样式支持的任何控件或用户界面元素。 除了 VisualStyleElement 本身, System.Windows.Forms.VisualStyles 命名空间包含许多 VisualStyleElement 嵌套类,这些类具有为视觉样式支持的控件、控件部件或其他 UI 元素的状态返回 staticVisualStyleElement 属性。

VisualStyleRenderer 提供一些方法,这些方法可以绘制和获取由操作系统当前视觉样式定义的每个 VisualStyleElement 的信息。 可以检索的元素信息包括其默认大小、背景类型和颜色定义。 VisualStyleRenderer 包装来自 Windows Platform SDKWindows Shell 部分的视觉样式 (UxTheme) API 的功能。

启用视觉样式

若要为针对 NET Framework 版本 1.0 编写的应用程序启用视觉样式,程序员必须将应用清单包含进来。该清单指定使用 ComCtl32.dll 版本 6 或更高版本绘制控件。 使用 .NET Framework 版本 1.1 或更高版本生成的应用程序可以使用 Application 类的 Application.EnableVisualStyles 方法。

检查视觉样式支持

RenderWithVisualStyles 类的 Application 属性指示当前应用程序是否正在使用视觉样式绘制控件。 绘制自定义控件时,可以检查 RenderWithVisualStyles 的值来确定是否应使用视觉样式呈现控件。 下表列出了 RenderWithVisualStyles 返回 true必须存在的四个条件。

条件备注
操作系统支持视觉样式。若要单独验证这种情况,请使用 IsSupportedByOS 类的 VisualStyleInformation 属性。
用户已在操作系统中启用视觉样式。若要单独验证这种情况,请使用 IsEnabledByUser 类的 VisualStyleInformation 属性。
应用程序中已启用视觉样式。可以通过调用 Application.EnableVisualStyles 方法或使用指定用 ComCtl32.dll 版本 6 或更高版本绘制控件的应用程序清单来启用应用程序中的视觉样式。
正在使用视觉样式来绘制应用程序窗口的工作区。若要单独验证这种情况,请使用 VisualStyleState 类的 Application 属性,验证它是否具有 VisualStyleState.ClientAreaEnabledVisualStyleState.ClientAndNonClientAreasEnabled值。

若要确定用户何时启用或禁用视觉样式,或何时从一种视觉样式切换到另种,请检查 UserPreferenceCategory.VisualStyleSystemEvents.UserPreferenceChanging 事件处理程序中的 SystemEvents.UserPreferenceChanged 值。

重要

如果想要在用户启用或切换视觉样式时使用 VisualStyleRenderer 来呈现控件或 UI 元素,请确保在处理 UserPreferenceChanged 事件而非 UserPreferenceChanging 事件时这样做。 处理 VisualStyleRenderer 时如果使用 UserPreferenceChanging类,则会引发异常。

如何:使用控件呈现类

此示例演示如何使用 ComboBoxRenderer 类呈现组合框控件的下拉箭头。 该示例包含一个简单自定义控件的 OnPaint 方法。 ComboBoxRenderer.IsSupported 属性用于确定是否在应用程序窗口的工作区启用视觉样式。 如果视觉样式处于活动状态,则 ComboBoxRenderer.DrawDropDownButton 方法将呈现带有视觉样式的下拉箭头;否则,ControlPaint.DrawComboButton 方法将以经典 Windows 样式呈现下拉箭头。

示例
// 呈现带有或不带有视觉样式的下拉箭头。
protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);

    if (!ComboBoxRenderer.IsSupported)
    {
        ControlPaint.DrawComboButton(e.Graphics,
            this.ClientRectangle, ButtonState.Normal);
    }
    else
    {
        ComboBoxRenderer.DrawDropDownButton(e.Graphics,
            this.ClientRectangle, ComboBoxState.Normal);
    }
}

编译代码

此示例需要:

  • 派生自 Control 类的自定义控件。

  • 托管自定义控件的 Form

  • SystemSystem.DrawingSystem.Windows.FormsSystem.Windows.Forms.VisualStyles 命名空间的引用。

如何:呈现视觉样式元素

System.Windows.Forms.VisualStyles 命名空间公开 VisualStyleElement 对象,这些对象表示视觉样式支持的 Windows 用户界面 (UI) 元素。 本主题演示如何使用 VisualStyleRenderer 类呈现表示“开始”菜单的“注销”和“关闭”按钮的 VisualStyleElement

呈现视觉样式元素

创建 VisualStyleRenderer 并将其设置为要绘制的元素。 请注意 Application.RenderWithVisualStyles 属性和 VisualStyleRenderer.IsElementDefined 方法的使用;如果禁用了视觉样式或未定义元素,则 VisualStyleRenderer 构造函数将引发异常。

private VisualStyleRenderer renderer = null;
private readonly VisualStyleElement element =
    VisualStyleElement.StartPanel.LogOffButtons.Normal;

public CustomControl()
{
    this.Location = new Point(50, 50);
    this.Size = new Size(200, 200);
    this.BackColor = SystemColors.ActiveBorder;

    if (Application.RenderWithVisualStyles &&
        VisualStyleRenderer.IsElementDefined(element))
    {
        renderer = new VisualStyleRenderer(element);
    }
}

调用 DrawBackground 方法来呈现 VisualStyleRenderer 当前表示的元素。

protected override void OnPaint(PaintEventArgs e)
{
    // 如果设置了渲染器,则绘制元素。
    if (renderer != null)
    {
        renderer.DrawBackground(e.Graphics, this.ClientRectangle);
    }

    // 视觉样式被禁用或元素未定义,因此只需绘制消息。
    else
    {
        this.Text = "Visual styles are disabled.";
        TextRenderer.DrawText(e.Graphics, this.Text, this.Font,
            new Point(0, 0), this.ForeColor);
    }
}

编译代码

此示例需要:

  • 派生自 Control 类的自定义控件。

  • 托管自定义控件的 Form

  • SystemSystem.DrawingSystem.Windows.FormsSystem.Windows.Forms.VisualStyles 命名空间的引用。

7. Windows 窗体控件的布局

在窗体上精确地放置控件对于许多应用程序而言是高优先级。 System.Windows.Forms 命名空间给你提供许多布局工具来实现此目的。

AutoSize 属性概述

必要情况下,可使用 AutoSize 属性更改控件的大小,以获取 PreferredSize 属性指定的值。 可通过设置 AutoSizeMode 属性来调整特定控件的调整大小行为。

AutoSize 行为

仅某些控件支持 AutoSize 属性。 此外,支持 AutoSize 属性的某些控件也支持 AutoSizeMode 属性。

AutoSize 属性会生成一些不同的行为,具体取决于特定的控件类型和 AutoSizeMode 属性的值(如果属性存在)。 下表描述了始终为 true 的行为,并简要介绍了每个行为:

Always true 行为描述
自动调整大小是一种运行时间功能。这意味着它永远不会增大或缩小控件,因此不会产生进一步的影响。
即使控件更改大小,它的 Location 属性的值也始终保持不变。控件的内容导致控件增大时,控件将向右和向下扩展。 控件不会向左侧扩展。
AutoSizetrue 时,采用 DockAnchor 属性。控件的 Location 属性的值将调整为正确值。 注意Label 控件是此项规则的例外情况。 将停靠的 Label 控件的 AutoSize 属性的值设置为 true 时,Label 控件将不会拉伸。
无论控件的 AutoSize 属性的值如何,始终采用其 MaximumSizeMinimumSize 属性。MaximumSizeMinimumSize 属性不受 AutoSize 属性的影响。
默认情况下,不设置最小大小。这意味着,如果在 AutoSize 下方将控件设置为缩小,且不包含任何内容,则其 Size 属性的值为 0,0。 在这种情况下,控件将缩小为点,且不明显可见。
如果控件未实现 GetPreferredSize 方法,则 GetPreferredSize 方法将返回分配给 Size 属性的最后一个值。这意味着将 AutoSize 设置为 true 无效。
TableLayoutPanel 单元格中的控件始终会缩小以适应单元格,直到达到其 MinimumSize此大小强制作为最大大小。 如果单元格属于 AutoSize 行或列,则不会出现这种情况。

AutoSizeMode 属性

AutoSizeMode 属性提供对默认 AutoSize 行为更精细的控制。 AutoSizeMode 属性指定控件自行调整大小以适应其内容的方式。 例如,内容可以是 Button 控件的文本或容器的子控件。

下表显示了 AutoSizeMode 设置以及每个设置效果的说明。

AutoSizeMode 设置行为
GrowAndShrink控件根据它的内容增大或缩小。 将接受 MinimumSizeMaximumSize 值,但会忽略 Size 属性的当前值。 这与具备 AutoSize 属性但没有 AutoSizeMode 属性的控件的行为相同。
GrowOnly控件会根据需要增大以包含其内容,但不会缩小到小于其 Size 属性指定的值。 这是 AutoSizeMode 的默认值。

支持 AutoSize 属性的控件

下表列出了支持 AutoSize 属性和 AutoSizeMode 属性的控件。

AutoSize 支持控件类型
- 支持的 AutoSize 属性。 - 无 AutoSizeMode 属性。CheckBox DomainUpDown Label LinkLabel MaskedTextBoxTextBox 基) NumericUpDown RadioButton TextBox TrackBar
- 支持的 AutoSize 属性。 - 支持的 AutoSizeMode 属性。Button CheckedListBox FlowLayoutPanel Form GroupBox Panel TableLayoutPanel
- 无 AutoSize 属性。CheckedListBox ComboBox DataGridView DateTimePicker ListBox ListView MaskedTextBox MonthCalendar ProgressBar PropertyGrid RichTextBox SplitContainer TabControl TabPage TreeView WebBrowser ScrollBar

设计环境中的 AutoSize

下表描述了在设计时基于控件的 AutoSizeAutoSizeMode 属性的值调整其大小的行为。

重写 SelectionRules 属性以确定给定控件是否处于用户可调整大小的状态。 在下表中,“不能”表示仅限 Moveable,“能”表示 AllSizeableMoveable

AutoSize 设置设计时调整大小手势
- AutoSize = true - 无 AutoSizeMode 属性。用户无法在设计时调整控件的大小,以下控件除外: - TextBox - MaskedTextBox - RichTextBox - TrackBar
- AutoSize = true - AutoSizeMode = GrowAndShrink用户无法在设计时调整控件的大小。
- AutoSize = true - AutoSizeMode = GrowOnly用户可以在设计时调整控件的大小。 设置 Size 属性后,用户只能增加控件的大小。
- AutoSize = falseAutoSize 属性处于隐藏状态。用户可以在设计时调整控件的大小。

备注

为了最大限度地提高工作效率,Visual Studio 中的 Windows 窗体设计器将隐藏 Form 类的 AutoSize 属性。 在设计时,窗体的行为如同 AutoSize 属性设置为 false,而不考虑其实际设置如何。 在运行时,不会进行特殊调整,并且会按照属性设置指定的方式应用 AutoSize 属性。

如何:使控件与窗体边缘对齐

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

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

Visual Studio 中具有对此功能的设计时支持。

在运行时设置控件的 Dock 属性

在代码中将 Dock 属性设置为适当的值。

// 在内部设置Dock属性。
this.Dock = DockStyle.Top;
// 从另一个对象设置Dock属性。
UserControl1.Dock = DockStyle.Top;

Windows 窗体控件中的边距和填充

在窗体上精确地放置控件对于许多应用程序而言是高优先级。 System.Windows.Forms 命名空间给你提供许多布局功能来实现此目的。 其中两个最重要的功能是 MarginPadding 属性。

Margin 属性定义控件周围的空间,该空间使其他控件与该控件的边框保持指定的距离。

Padding 属性定义控件内部的空间,该空间使控件的内容(例如,其 Text 属性的值)与该控件的边框保持指定的距离。

下图显示控件上的 PaddingMargin 属性。

Visual Studio 中具有对此功能的设计时支持。

如何:使用填充在 Windows 窗体控件周围创建边框

下面的代码示例演示如何在 RichTextBox 控件周围创建边框或轮廓。 该示例将 Panel 控件的 Padding 属性的值设置为 5,并将子 RichTextBox 控件的 Dock 属性设置为 FillPanel 控件的 BackColor 设置为 Blue,这将在 RichTextBox 控件周围创建一个蓝色边框。

示例
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace MarginAndPadding
{
    public class Form1 : Form
    {
        private Panel panel1;
        private RichTextBox richTextBox1;
        /// <summary>
        /// 必需的设计变量。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        // 这个代码示例演示了如何使用Padding属性在RichTextBox控件周围创建边框。
        public Form1()
        {
            InitializeComponent();

            this.panel1.BackColor = System.Drawing.Color.Blue;
            this.panel1.Padding = new System.Windows.Forms.Padding(5);
            this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;

            this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
            this.richTextBox1.Dock = System.Windows.Forms.DockStyle.Fill;
        }

        /// <summary>
        /// 清理所有正在使用的资源。
        /// </summary>
        /// <param name="disposing">如果应处置托管资源,则为True;否则false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// 设计器支持所需的方法-不要使用代码编辑器修改此方法的内容。
        /// </summary>
        private void InitializeComponent()
        {
            this.panel1 = new System.Windows.Forms.Panel();
            this.richTextBox1 = new System.Windows.Forms.RichTextBox();
            this.panel1.SuspendLayout();
            this.SuspendLayout();
            //
            // panel1
            //
            this.panel1.Controls.Add(this.richTextBox1);
            this.panel1.Location = new System.Drawing.Point(20, 20);
            this.panel1.Name = "panel1";
            this.panel1.Size = new System.Drawing.Size(491, 313);
            this.panel1.TabIndex = 0;
            //
            // richTextBox1
            //
            this.richTextBox1.Location = new System.Drawing.Point(5, 5);
            this.richTextBox1.Name = "richTextBox1";
            this.richTextBox1.Size = new System.Drawing.Size(481, 303);
            this.richTextBox1.TabIndex = 0;
            this.richTextBox1.Text = "";
            //
            // Form1
            //
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(531, 353);
            this.Controls.Add(this.panel1);
            this.Name = "Form1";
            this.Padding = new System.Windows.Forms.Padding(20);
            this.Text = "Form1";
            this.panel1.ResumeLayout(false);
            this.ResumeLayout(false);
        }

        #endregion
    }

    static class Program
    {
        /// <summary>
        /// 应用程序的主要入口点。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.Run(new Form1());
        }
    }
}

如何:实现自定义布局引擎

下面的代码示例演示如何创建执行简单流布局的自定义布局引擎。 它实现了一个名为 DemoFlowPanel 的面板控件,该控件重写 LayoutEngine 属性以提供 DemoFlowLayout 类的实例。

示例
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.Layout;

// 这个类演示了一个简单的自定义布局面板。
// 它覆盖Panel控件的layoutenengine属性以提供自定义布局引擎。
public class DemoFlowPanel : Panel
{
    private DemoFlowLayout layoutEngine;

    public DemoFlowPanel()
    {
    }

    public override LayoutEngine LayoutEngine
    {
        get
        {
            layoutEngine ??= new DemoFlowLayout();

            return layoutEngine;
        }
    }
}

// 这个类演示了一个简单的自定义布局引擎。
public class DemoFlowLayout : LayoutEngine
{
    public override bool Layout(
        object container,
        LayoutEventArgs layoutEventArgs)
    {
        Control parent = container as Control;

        // 使用DisplayRectangle,来显示parent.Padding。
        Rectangle parentDisplayRectangle = parent.DisplayRectangle;
        Point nextControlLocation = parentDisplayRectangle.Location;

        foreach (Control c in parent.Controls)
        {
            // 只对可见控件应用布局。
            if (!c.Visible)
            {
                continue;
            }

            // 尊从控件的边距:
			// 移到左边和上面。
            nextControlLocation.Offset(c.Margin.Left, c.Margin.Top);

            // 设置控件的位置。
            c.Location = nextControlLocation;

            // 将自动调整大小的控件设置为其自动调整大小的高度。
            if (c.AutoSize)
            {
                c.Size = c.GetPreferredSize(parentDisplayRectangle.Size);
            }

            // 将X移回显示矩形原点。
            nextControlLocation.X = parentDisplayRectangle.X;

            // 将Y增加控件的高度和底部边距。
            nextControlLocation.Y += c.Height + c.Margin.Bottom;
        }

        // 可选:返回该容器的父类是否应根据此布局执行布局。一些布局引擎返回容器的AutoSize属性的值。
        return false;
    }
}

8. Windows 窗体控件中的多线程处理

在许多应用程序中,可以通过在另一个线程上执行耗时的操作来使用户界面 (UI) 更具响应性。 许多工具可用于多线程处理 Windows 窗体控件,包括 System.Threading 命名空间、Control.BeginInvoke 方法和 BackgroundWorker 组件。

备注

BackgroundWorker 组件取代了 System.Threading 命名空间和 Control.BeginInvoke 方法并向其添加功能;但是,可以选择保留这些项以实现向后兼容并供将来使用。

如何:对 Windows 窗体控件进行线程安全调用

多线程处理可以改进 Windows 窗体应用的性能,但对 Windows 窗体控件的访问本质上不是线程安全的。 多线程处理可将代码公开到极为严重和复杂的 bug。 有两个或两个以上线程操作控件可能会迫使该控件处于不一致状态并导致争用条件、死锁和冻结或挂起。 如果要在应用中实现多线程处理,请务必以线程安全的方式调用跨线程控件。

可通过两种方法从未创建 Windows 窗体控件的线程安全地调用该控件。 可以使用 System.Windows.Forms.Control.Invoke 方法调用在主线程中创建的委托,进而调用控件。 或者,可以实现一个 System.ComponentModel.BackgroundWorker,它使用事件驱动模型将后台线程中完成的工作与结果报告分开。

不安全的跨线程调用

直接从未创建控件的线程调用该控件是不安全的。 以下代码片段演示了对 System.Windows.Forms.TextBox 控件的不安全调用。 Button1_Click 事件处理程序创建一个新的 WriteTextUnsafe 线程,该线程直接设置主线程的 TextBox.Text 属性。

private void Button1_Click(object sender, EventArgs e)
{
    thread2 = new Thread(new ThreadStart(WriteTextUnsafe));
    thread2.Start();
}
private void WriteTextUnsafe()
{
    textBox1.Text = "This text was set unsafely.";
}

Visual Studio 调试器通过引发 InvalidOperationException 检测这些不安全线程调用,并显示消息“跨线程操作无效。控件 "" 从创建它的线程以外的线程访问。”在 Visual Studio 调试期间,对于不安全的跨线程调用总是会发生 InvalidOperationException,并且可能在应用运行时发生。 应解决此问题,但也可以通过将 Control.CheckForIllegalCrossThreadCalls 属性设置为 false 来禁用该异常。

安全的跨线程调用

以下代码示例演示了两种从未创建 Windows 窗体控件的线程安全调用该窗体的方法:

  1. System.Windows.Forms.Control.Invoke 方法,它从主线程调用委托以调用控件。

  2. System.ComponentModel.BackgroundWorker 组件,它提供事件驱动模型。

在这两个示例中,后台线程都会休眠一秒钟以模拟该线程中正在完成的工作。

可以通过 C# 命令行将这些示例作为 .NET Framework 应用生成和运行。

.NET Core 3.0 开始,你还可以从具有 .NET Core Windows 窗体 <folder name>.csproj 项目文件的文件夹将这些示例作为 Windows .NET Core 应用生成和运行。

示例:将 Invoke 方法与委托配合使用

下面的示例演示了一种用于确保对 Windows 窗体控件进行线程安全调用的模式。 它查询 System.Windows.Forms.Control.InvokeRequired 属性,该属性将控件的创建线程 ID 与调用线程 ID 进行比较。 如果线程 ID 相同,则直接调用控件。 如果线程 ID 不同,它会使用来自主线程的委托调用 Control.Invoke 方法,从而实际调用控件。

SafeCallDelegate 允许设置 TextBox 控件的 Text 属性。 WriteTextSafe 方法查询 InvokeRequired。 如果 InvokeRequired 返回 true,则 WriteTextSafeSafeCallDelegate 传递给 Invoke 方法以对控件进行实际调用。 如果 InvokeRequired 返回 false,则 WriteTextSafe 直接设置 TextBox.TextButton1_Click 事件处理程序创建新线程并运行 WriteTextSafe 方法。

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class InvokeThreadSafeForm : Form
{
    private delegate void SafeCallDelegate(string text);
    private Button button1;
    private TextBox textBox1;
    private Thread thread2 = null;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new InvokeThreadSafeForm());
    }
    public InvokeThreadSafeForm()
    {
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        thread2 = new Thread(new ThreadStart(SetText));
        thread2.Start();
        Thread.Sleep(1000);
    }

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;
        }
    }

    private void SetText()
    {
        WriteTextSafe("这段文字设置得很安全。");
    }
}

示例:使用 BackgroundWorker 事件处理程序

实现多线程处理的一种简单方法是使用 System.ComponentModel.BackgroundWorker 组件,该组件使用事件驱动模型。 后台线程运行不与主线程交互的 BackgroundWorker.DoWork 事件。 主线程运行 BackgroundWorker.ProgressChangedBackgroundWorker.RunWorkerCompleted 事件处理程序,它们可以调用主线程的控件。

若要使用 BackgroundWorker 进行线程安全调用,请在后台线程中创建一个方法来完成这项工作,并将其绑定到 DoWork 事件。 在主线程中创建另一个方法来报告后台工作的结果,并将其绑定到 ProgressChangedRunWorkerCompleted 事件。 若要启动后台线程,请调用 BackgroundWorker.RunWorkerAsync

该示例使用 RunWorkerCompleted 事件处理程序来设置 TextBox 控件的 Text 属性。

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class BackgroundWorkerForm : Form
{
    private BackgroundWorker backgroundWorker1;
    private Button button1;
    private TextBox textBox1;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new BackgroundWorkerForm());
    }
    public BackgroundWorkerForm()
    {
        backgroundWorker1 = new BackgroundWorker();
        backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
        backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "使用BackgroundWorker安全地设置文本"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }
    private void Button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // 休眠2秒以模拟获取数据。
        Thread.Sleep(2000);
        e.Result = "这个文本是由BackgroundWorker安全设置的。";
    }

    private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = e.Result.ToString();
    }
}

如何:使用后台线程搜索文件

BackgroundWorker 组件取代了 System.Threading 组件并向其添加了功能;但是,可以选择保留 System.Threading 组件以实现向后兼容并供将来使用。

Windows 窗体使用单线程单元 (STA) 模型,因为 Windows 窗体基于固有单元线程的本机 Win32 窗口。 STA 模型意味着可以在任何线程上创建窗口,但是窗口一旦创建就不能切换线程,并且对该窗口的所有函数调用都必须发生在其创建线程上。 在 Windows 窗体之外,.NET Framework 中的类使用自由线程模型。

STA 模型要求控件上需要从控件的创建线程外部调用的任何方法都必须封送到(在其上执行)控件的创建线程。 为此,基类 Control 提供了多种方法(InvokeBeginInvokeEndInvoke)。 Invoke 进行同步方法调用;BeginInvoke 进行异步方法调用。

如果在控件中使用多线程处理资源密集型任务,则用户界面可以在后台线程执行资源密集型计算时保持响应。

以下示例 (DirectorySearcher) 显示了一个多线程 Windows 窗体控件,该控件使用后台线程递归搜索目录,以查找与指定搜索字符串匹配的文件,然后使用搜索结果填充列表框。 该示例说明的关键概念如下:

  • DirectorySearcher 启动新线程来执行搜索。 线程执行 ThreadProcedure 方法,该方法又调用帮助程序 RecurseDirectory 方法来执行实际搜索并填充列表框。 但是,填充列表框需要跨线程调用,如以下两个项目符号中所述。

  • DirectorySearcher 定义了将文件添加到列表框的 AddFiles 方法;但是,RecurseDirectory 不能直接调用 AddFiles,因为 AddFiles 只能在创建 DirectorySearcher 的 STA 线程中执行。

  • RecurseDirectory 调用 AddFiles 的唯一方法是通过跨线程调用,即通过调用 InvokeBeginInvokeAddFiles 封送到 DirectorySearcher 的创建线程。 RecurseDirectory 使用 BeginInvoke 以便可以异步进行调用。

  • 封送方法需要函数指针或回调的等效项。 这是使用 .NET Framework 中的委托来完成的。 BeginInvoke 将委托作为参数。 因此,DirectorySearcher 定义了一个委托 (FileListDelegate),将 AddFiles 绑定到其构造函数中的 FileListDelegate 实例,并将此委托实例传递给 BeginInvokeDirectorySearcher 还定义了一个在搜索完成时封送的事件委托。

namespace Microsoft.Samples.DirectorySearcher
{
   using System;
   using System.IO;
   using System.Threading;
   using System.Windows.Forms;

   /// <summary>
   ///      该类是实现简单目录搜索器的Windows窗体控件。
   ///通过代码提供一个搜索字符串,它将在后台线程中搜索目录,用匹配项填充其列表框。
   /// </summary>
   public class DirectorySearcher : Control
   {
      // 定义一个特殊委托,用于处理从后台目录搜索线程到包含列表框的线程的文件名封送处理列表。
      private delegate void FileListDelegate(string[] files, int startIndex, int count);

      private ListBox listBox;
      private string  searchCriteria;
      private bool searching;
      private bool deferSearch;
      private Thread searchThread;
      private FileListDelegate fileListDelegate;
      private EventHandler onSearchComplete;

      public DirectorySearcher()
      {
         listBox = new ListBox();
         listBox.Dock = DockStyle.Fill;

         Controls.Add(listBox);

         fileListDelegate = new FileListDelegate(AddFiles);
         onSearchComplete = new EventHandler(OnSearchComplete);
      }

      public string SearchCriteria
      {
         get
         {
            return searchCriteria;
         }
         set
         {
            // 如果当前正在搜索,请中止搜索,并在设置新条件后重新启动搜索。
            bool wasSearching = Searching;

            if (wasSearching)
            {
               StopSearch();
            }

            listBox.Items.Clear();
            searchCriteria = value;

            if (wasSearching)
            {
               BeginSearch();
            }
         }
      }

      public bool Searching
      {
         get
         {
            return searching;
         }
      }

      public event EventHandler SearchComplete;

      /// <summary>
      /// 这个方法是从后台线程调用的。它是通过BeginInvoke调用来调用的,所以它总是被封送到拥有列表框控件的线程。
      /// </summary>
      /// <param name="files"></param>
      /// <param name="startIndex"></param>
      /// <param name="count"></param>
      private void AddFiles(string[] files, int startIndex, int count)
      {
         while(count-- > 0)
         {
            listBox.Items.Add(files[startIndex + count]);
         }
      }

      public void BeginSearch()
      {
         // 创建搜索线程,它将开始搜索。如果已经在搜索,什么也不做。
         if (Searching)
         {
            return;
         }

         // 如果句柄已经创建,则开始搜索。否则,将其推迟到创建句柄为止。
         if (IsHandleCreated)
         {
            searchThread = new Thread(new ThreadStart(ThreadProcedure));
            searching = true;
            searchThread.Start();
         }
         else
         {
            deferSearch = true;
         }
      }

      protected override void OnHandleDestroyed(EventArgs e)
      {
         // 如果句柄正在被销毁,而您没有重新创建它,则中止搜索。
         if (!RecreatingHandle)
         {
            StopSearch();
         }
         base.OnHandleDestroyed(e);
      }

      protected override void OnHandleCreated(EventArgs e)
      {
         base.OnHandleCreated(e);
         if (deferSearch)
         {
            deferSearch = false;
            BeginSearch();
         }
      }

      /// <summary>
      /// 此方法由后台线程在完成搜索时调用。
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void OnSearchComplete(object sender, EventArgs e)
      {
         if (SearchComplete != null)
         {
            SearchComplete(sender, e);
         }
      }

      public void StopSearch()
      {
         if (!searching)
         {
            return;
         }

         if (searchThread.IsAlive)
         {
            searchThread.Abort();
            searchThread.Join();
         }

         searchThread = null;
         searching = false;
      }

      /// <summary>
      /// 递归给定的路径,将该路径上的所有文件添加到列表框中。在完成文件处理后,它为路径上的每个目录调用自己一次。
      /// </summary>
      /// <param name="searchPath"></param>
      private void RecurseDirectory(string searchPath)
      {
         // 将searchPath拆分为一个目录和一个通配符规范。
         string directory = Path.GetDirectoryName(searchPath);
         string search = Path.GetFileName(searchPath);

         // 如果未指定目录或搜索条件,则返回。
         if (directory == null || search == null)
         {
            return;
         }

         string[] files;

         // 像NTFS这样具有访问权限的文件系统在没有权限的情况下查找目录时可能会导致异常。捕获这些异常并返回。
         try
         {
            files = Directory.GetFiles(directory, search);
         }
         catch(UnauthorizedAccessException)
         {
            return;
         }
         catch(DirectoryNotFoundException)
         {
            return;
         }

         // 对列表框执行BeginInvoke调用,以便封送到正确的线程。
         // 对每个文件执行一次封送并不是很有效,所以将多个文件调用批处理为一次封送调用。
         int startingIndex = 0;

         while(startingIndex < files.Length)
         {
            // 一次批量处理20个文件,除非最后。
            int count = 20;
            if (count + startingIndex >= files.Length)
            {
               count = files.Length - startingIndex;
            }

            // 开始跨线程调用。因为要将不可变对象传递给这个调用方法,所以不必等待它完成。
            // 如果这些对象是复杂的对象,则必须创建它们的新实例,或者在修改对象之前等待线程处理此调用。
            IAsyncResult r = BeginInvoke(fileListDelegate, new object[] {files, startingIndex, count});
            startingIndex += count;
         }

         // 现在您已经完成了该目录中的文件,对每个子目录进行递归。
         string[] directories = Directory.GetDirectories(directory);
         foreach(string d in directories)
         {
            RecurseDirectory(Path.Combine(d, search));
         }
      }

      /// <summary>
      /// 这是实际的线程过程。此方法在后台线程中运行以扫描目录。完成后,它就退出。
      /// </summary>
      private void ThreadProcedure()
      {
         // 获取搜索字符串。在.net中,单个字段的分配是原子的,因此您不需要使用任何线程同步来获取这里的字符串值。
         try
         {
            string localSearch = SearchCriteria;

            // 现在,搜索文件系统。
            RecurseDirectory(localSearch);
         }
         finally
         {
            // 您已经完成了搜索,所以更新。
            //
            searching = false;

            // 引发一个事件,通知用户搜索已终止。
			// 您不必通过封送调用来执行此操作,但建议出于以下原因进行封送:
			// 此控件的用户不知道它是多线程的,因此他们期望其事件返回到与控件相同的线程中。
            BeginInvoke(onSearchComplete, new object[] {this, EventArgs.Empty});
         }
      }
   }
}

在窗体上使用多线程控件

以下示例显示了如何在窗体上使用多线程 DirectorySearcher 控件。

namespace SampleUsage
{
   using System;
   using System.Collections;
   using System.ComponentModel;
   using System.Data;
   using System.Drawing;
   using System.Windows.Forms;
   using Microsoft.Samples.DirectorySearcher;

   /// <summary>
   ///      Form1的概要描述。
   /// </summary>
   public class Form1 : System.Windows.Forms.Form
   {
      private DirectorySearcher directorySearcher;
      private System.Windows.Forms.TextBox searchText;
      private System.Windows.Forms.Label searchLabel;
      private System.Windows.Forms.Button searchButton;

      public Form1()
      {
         //
         // Windows窗体设计器支持所需。
         //
         InitializeComponent();

         //
         // 在InitializeComponent调用之后添加任何构造函数代码。
         //
      }

      #region Windows Form Designer generated code
      /// <summary>
      ///      设计人员支持所需的方法。不要使用代码编辑器修改此方法的内容。
      /// </summary>
      private void InitializeComponent()
      {
         this.directorySearcher = new Microsoft.Samples.DirectorySearcher.DirectorySearcher();
         this.searchButton = new System.Windows.Forms.Button();
         this.searchText = new System.Windows.Forms.TextBox();
         this.searchLabel = new System.Windows.Forms.Label();
         this.directorySearcher.Anchor = (((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
            | System.Windows.Forms.AnchorStyles.Left)
            | System.Windows.Forms.AnchorStyles.Right);
         this.directorySearcher.Location = new System.Drawing.Point(8, 72);
         this.directorySearcher.SearchCriteria = null;
         this.directorySearcher.Size = new System.Drawing.Size(271, 173);
         this.directorySearcher.TabIndex = 2;
         this.directorySearcher.SearchComplete += new System.EventHandler(this.directorySearcher_SearchComplete);
         this.searchButton.Location = new System.Drawing.Point(8, 16);
         this.searchButton.Size = new System.Drawing.Size(88, 40);
         this.searchButton.TabIndex = 0;
         this.searchButton.Text = "&Search";
         this.searchButton.Click += new System.EventHandler(this.searchButton_Click);
         this.searchText.Anchor = ((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
            | System.Windows.Forms.AnchorStyles.Right);
         this.searchText.Location = new System.Drawing.Point(104, 24);
         this.searchText.Size = new System.Drawing.Size(175, 20);
         this.searchText.TabIndex = 1;
         this.searchText.Text = "c:\\*.cs";
         this.searchLabel.ForeColor = System.Drawing.Color.Red;
         this.searchLabel.Location = new System.Drawing.Point(104, 48);
         this.searchLabel.Size = new System.Drawing.Size(176, 16);
         this.searchLabel.TabIndex = 3;
         this.ClientSize = new System.Drawing.Size(291, 264);
         this.Controls.AddRange(new System.Windows.Forms.Control[] {this.searchLabel,
                                                        this.directorySearcher,
                                                        this.searchText,
                                                        this.searchButton});
         this.Text = "Search Directories";

      }
      #endregion

      /// <summary>
      ///    应用程序的主要入口点。
      /// </summary>
      [STAThread]
      static void Main()
      {
         Application.Run(new Form1());
      }

      private void searchButton_Click(object sender, System.EventArgs e)
      {
         directorySearcher.SearchCriteria = searchText.Text;
         searchLabel.Text = "搜索...";
         directorySearcher.BeginSearch();
      }

      private void directorySearcher_SearchComplete(object sender, System.EventArgs e)
      {
         searchLabel.Text = string.Empty;
      }
   }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值