WinForm控件开发总结


2008-10-21 00:47 我本人不是专业的控件开发人员,只是在平常的工作中,需要自己 开发一些控件。在自己开发 WinForm 控件的时候,没有太多可以借鉴的资料,只能盯着 MSDN 使劲看,还好总算有些收获。现在我会把这些经验陆陆续续的总结出来,写成一系 列方章,希望对看到的朋友有所帮助。今天我来开个头。 其实开发 WinForm 控件并不是很复杂,.NET 为我们提供了丰富的底层支持。如果 你有 MFC 或者 API 图形界面的开发经验,那么学会 WinForm 控件可能只需要很短的时间 就够了。 自己开发的 WinForm 控件通常有三种类型:复合控件( Composite Controls),扩展 控件(Extended Controls),自定义控件(Custom Controls)。 复合控件:将现有的各种控件组合起来,形成一个新的控件,将集中控件的功能集中 起来。 扩展控件:在现有控件的控件的基础上派生出一个新的控件,为原有控件增加新的功 能或者修改原有控件的控能。 自定义控件:直接从 System.Windows.Forms.Control 类派生出来。Control 类提供控件 所需要的所有基本功能,包括键盘和鼠标的事件处理。自定义控件是最灵活最强大的方法, 但是对开发者的要求也比较高,你必须为 Control 类的 OnPaint 事件写代码,你也可以重写 Control 类的 WndProc 方法,处理更底层的 Windows 消息,所以你应该了解 GDI+和 Windows API。 本系列文章主要介绍自定义控件的开发方法。 控件(可视化的)的基本特征: 1. 可视化。 2. 可以与用户进行交互,比如通过键盘和鼠标。 3. 暴露出一组属性和方法供开发人员使用。 4. 暴露出一组事件供开发人员使用。 5. 控件属性的可持久化。 6. 可发布和可重用。 这些特征是我自己总结出来,不一定准确,或者还有遗漏,但是基本上概括了控件 的主要方面。 接下来我们做一个简单的控件来增强一下感性认识。首先启动 VS2005 创建一个 ClassLibrary 工程,命名为 CustomControlSample,VS 会自动为我们创建一个 solution 与这 个工程同名,然后删掉自动生成的 Class1.cs 文件,最后在 Solution explorer 里右键点击 CustomControlSample 工 程 选 择 Add->Classes⋯ 添加一个新类,将文件的名称命名为 FirstControl。下边是代码: using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms; using System.ComponentModel; using System.Drawing; namespace CustomControlSample { public class FirstControl : Control { Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. public FirstControl() { } // ContentAlignment is an enumeration defined in the System.Drawing // namespace that specifies the alignment of content on a drawing // surface. private ContentAlignment alignmentValue = ContentAlignment.MiddleLeft; [ Category("Alignment"), Description("Specifies the alignment of text.") ] public ContentAlignment TextAlignment { get { return alignmentValue; } set { alignmentValue = value; // The Invalidate method invokes the OnPaint method described // in step 3. Invalidate(); } } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); StringFormat style = new StringFormat(); style.Alignment = StringAlignment.Near; switch (alignmentValue) { case ContentAlignment.MiddleLeft: style.Alignment = StringAlignment.Near; break; case ContentAlignment.MiddleRight: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. style.Alignment = StringAlignment.Far; break; case ContentAlignment.MiddleCenter: style.Alignment = StringAlignment.Center; break; } // Call the DrawString method of the System.Drawing class to write // text. Text and ClientRectangle are properties inherited from // Control. e.Graphics.DrawString( Text, Font, new SolidBrush(ForeColor), ClientRectangle, style); } } } 晚了,今天写到这里,下一篇文章介绍怎样使用我们写好的控件。 在上一篇文章里我们创建了一个简单的控件 FirstControl,现在我来介绍一下怎么使用和调 试自己的控件。我希望将过程写的尽可能的详细,让想学习控件开发的朋友容易上手,高手 们见谅。 在同一个 solution 里添加一个 Windows Application 工程(在 Solution Explorer 里右 键点击 CustomControlSample solution 选择 Add->New Project⋯),命名为 TestControl。VS 会为你自动生成一个 Form,文件名为 Form1.cs。在 Solution Explorer 里双击 Form1.cs 文件 进入到 Form 设计界面。现在我们将 FirstControl 控件添加到工具箱( ToolBox)里,在 Toolbox 上右键点击,在弹出的菜单中选择 Choose Items⋯,在出现的 Choose Toolbox Items 对话框 中点击 Browse⋯ 按钮,在 Open 对话框中选择我们的控件工程生成的 dll(我的 dll 在 F:\Programs\C#\CustomControlSample\CustomControlSample\bin\Debug 目录下,你可以根据实 际情况去找)。完成这一步,在 Toolbox 就会出现我们设计的控件,图标是一个蓝色的齿轮 (默认的都是这个,当然你也可以修改,后边的文章我会介绍),名称是 FirstControl。 现在我们在 Toolbox 中选中 FirstControl,在 form 设计器上左键点击,或者按住鼠标 拖放。我们制作的控件出现在了 Form 设计器上,在 Form 设计器上选中这个控件,然后在 属性浏览器中将 Text 属性设为 Hello World,现在我们的控件上的文字变成了 Hello World。 接下来我们要运行测试的工程,看看实际的效果。在运行之前,将测试工程设为启动工程, 具体做法是,在 solution explorer 中右键点击 TestControl 工程,选择“ Set as Startup Project”。 点击工具栏里的运行按钮,或者按键盘的 F5 功能键。实际效果如下图所示: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 在前面的文章里我们制作了一个非常简单的控件。现在我们回过头来看看这些代码透露出什 么信息。 这个类是直接从 Control 类派生出来的,自定义控件都是直接从 Control 类派生出来 的。这个类定义了一个属性 TextAlignment,用来控制文本在控件中显示的位置: [Category("Alignment"), Description("Specifies the alignment of text.")] public ContentAlignment TextAlignment { get { return alignmentValue; } set { alignmentValue = value; // The Invalidate method invokes the OnPaint method described // in step 3. Invalidate(); } } 在这个属性之上有两个 Attribute,这两个 attribute 描述了控件在设计时所表现出来的特征。 我们来看看在控件设计中有哪些主要用到的设计时 Attribute。 BrowsableAttribute:描述是否一个属性或事件应该被显示在属性浏览器里。 Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. CategoryAttribute:描述一个属性或事件的类别,当使用类别的时候,属性浏览器按类别 将属性分组。 DescriptionAttribute:当用户在属性浏览器里选择属性的时候,description 里指定的文本 会显示在属性浏览器的下边,向用户显示属性的功能。 BindableAttribute:描述是否一个属性倾向于被绑定。 DefaultPropertyAttribute:为组件指定一个默认的属性,当用户在 Form 设计器上选择一 个控件的时候,默认属性会在属性浏览器里被选中。 DefaultValueAttribute:为一个简单类型的属性设置一个默认值。 EditorAttribute:为属性指定一个特殊的编辑器。 LocalizableAttribute:指示一个属性是否能被本地化,任何有这个 Attribute 的属性将会被 持久化到资源文件里。 DesignerSerializationVisibilityAttribute:指示一个属性是否或者如何持久化到代码里。 TypeConverterAttribute:为属性指定一个类型转换器,类型转换器能将属性的值转化成 其它的数据类型。 DefaultEventAttribute:为组件指定一个默认的事件,当用户在 form 设计其中选择一个控 件的时候,在属性浏览器中这个事件被选中。 这些设计时的 Attribute 时很重要的,如果使用的好,将会对用户的使用带来很大的便利。 这一章我主要介绍了设计时的 Attribute,接下来的文章我将通过代码来介绍这些 Attribute。 前一篇文章介绍了常用的设计时Attribute。其中BrowsableAttribute, CategoryAttribute,DescriptionAttribute,DefaultPropertyAttribute, DefaultEventAttribute都是比较简单的,也是可有可无,但是为了提供更好的 用户体验这些 Attribute最好不要省掉,如果你对这些 Attribute还不熟悉,可 以参考我前一篇文章的描述或者查看 MSDN,这里我就不在赘述了。 下来我们主要介绍一下DesignerSerializationVisibilityAttribute 和 TypeConverterAttribute。 DesignerSerializationVisibilityAttribute的功能是指示一个属性 是否串行化和如何串行化,它的值是一个枚举,一共有三种类型 Content, Hidden,Visible。Content指示代码生成器为对象包含的内容生成代码,而不 是为对象本身,Hidden指示代码生成器不为对象生成代码,visible指示代码生 成器为对象生成代码。假如你的控件有一个集合属性,又想在设计时自动将集合 属性的内容生成代码,那么就使用这个 Attribute,并将值设为 DesignerSerializationVisibility.Content。 TypeConverterAttribute的作用就更大一些,也稍微复杂一些。 TypeConverterAttribute主要的目的是为属性指定一个类型转换器,这个转化 器可以将属性的值转换城其它的类型。.NET 框架已经为大部分常用的类型都提 供了类型转换器,比如 Color就有ColorConverter,枚举类型就有 EnumConverter,等等,所以一般情况下你没有必要写类型转换器,如果你的属 性的特殊的类型或者自定义的类型那么就必须要写了。类型转换器都是从 System.ComponentModel.TypeConverter派生出来的,你需要重写其中的一些方 Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 法来达到转换的目的,在我们开发的过程中,其实只关心属性的值如何转换成字 符串(因为属性的值需要在属性浏览器里显示出来,属性浏览器里显示的都是字 符串)和源代码(需要自动为属性的值生成源代码以实现持久化),当然反过来, 也要将字符串和源代码转换成属性的值。另外使用TypeConverter 也可以实现子 属性,让属性的子属性也显示在属性浏览器里,并且可以折叠。 接下来我就写一个简单的控件来演示一下这个控件。代码如下: using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms; using System.Drawing; using System.ComponentModel; using System.Collections; namespace CustomControlSample { public class MyListControl:System.Windows.Forms.Control { private List _list = new List(); public MyListControl() { } [Browsable(true)] public List Item { get { return _list; } set { _list = value; } } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g = e.Graphics; //绘制控件的边框 Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. g.DrawRectangle(Pens.Black,new Rectangle(Point.Empty,new Size(Size.Width-1,Size.Height-1))); for (Int32 i = 0; i < _list.Count; i++) { g.DrawString(_list[i].ToString(), Font, Brushes.Black,1, i * FontHeight); } } } } 我创建了一个简单的List 控件,将用户输入的数据显示在控件中,效果图如下: 在这个控件中,我声明了一个集合属性Item 供用户输入要显示的整型数值。我们按照 WinForm 控件制作教程(二)中的方法将控件加到ToolBox 里,然后拖到Form 设计器中, 然后选中控件,在属性浏览中查看控件的属性,属性中有一个Item 的属性,属性右边的值 显示为Collection,当你点击这个值的时候,值的右边出现一个小按钮,点击这个小按钮, 就会出现弹出一个Collection Editor 窗口,你可以在在这个编辑器里添加你想显示的整型 值,如图: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 添加完以后,关闭Collection Editor。现在我们看看Form 设计器为我们生成了什么 代码。对于用户在Form 设计器中设计的内容,设计器的代码生成器会将代码生成到窗口类 的 InitializeComponent()方法中,对于vs2005 来说,这个方法位于***.Designer.cs 文件中,在我当前的工程中位于Form1.Designer.cs 文件中。在solution 浏览器中双击打 开这个文件,看看Form 设计器为我们生成了什么代码: // // myListControl1 // this.myListControl1.BackColor = System.Drawing.SystemColors.ActiveCaptionText; this.myListControl1.Item = ((System.Collections.Generic.List)(resources.GetObject("myListControl1.Ite m"))); this.myListControl1.Location = new System.Drawing.Point(12, 34); this.myListControl1.Name = "myListControl1"; this.myListControl1.Size = new System.Drawing.Size(220, 180); this.myListControl1.TabIndex = 1; this.myListControl1.Text = "myListControl1"; 设计器将Item 的内容串行化到了资源文件里。现在我们修改控件的代码,让设计器 将 Item 的内容串行化到源代码里。我们为Item 属性添加 DesignerSerializationVisibilityAttribute,代码片断如下: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. [Browsable(true)] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibili ty.Content)] public List Item { get { return _list; } set { _list = value; } } 编辑完以后,Build 控件工程,回到测试工程里,将Item 属性里的值,删掉重新添 加,添加完以后,我们再来看看设计器生成的代码: // // myListControl1 // this.myListControl1.BackColor = System.Drawing.SystemColors.ActiveCaptionText; this.myListControl1.Item.Add(1); this.myListControl1.Item.Add(2); this.myListControl1.Item.Add(3); this.myListControl1.Item.Add(6); this.myListControl1.Item.Add(8); this.myListControl1.Item.Add(9); this.myListControl1.Location = new System.Drawing.Point(12, 34); this.myListControl1.Name = "myListControl1"; this.myListControl1.Size = new System.Drawing.Size(220, 180); this.myListControl1.TabIndex = 1; this.myListControl1.Text = "myListControl1"; 现在设计器将Item 的内容串行化到源代码里了。 时间有限,今天就写到这里,下一篇文章我来介绍TypeConverterAttribute。 上一篇文章我已经介绍了TypeConverterAttribute 元数据的作用,本文将通过 代码向你展示具体的实现。在这个例子中,我要给控件添加一个复杂的属性,这 个属性对这个控件没有什么功用,纯粹是为了演示,有些牵强附会了。 现在在前一篇文章中的创建的控件代码中添加一个Scope 属性: [Browsable(true)] public Scope Scope { get Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. { return _scope; } set { _scope = value; } } 这个属性的类型是Scope 类,代码如下: public class Scope { private Int32 _min; private Int32 _max; public Scope() { } public Scope(Int32 min, Int32 max) { _min = min; _max = max; } [Browsable(true)] public Int32 Min { get { return _min; } set { _min = value; } } [Browsable(true)] public Int32 Max { get { return _max; } set Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. { _max = value; } } } 添加完属性后,build 控件工程,然后在测试的工程里选中添加的控件, 然后在属性浏览器里观察它的属性,发现Scope 属性是灰的,不能编辑。前一篇 文章提到了,在属性浏览器里可以编辑的属性都是有类型转换器的,而.NET 框 架为基本的类型和常用的类型都提供了默认的类型转换器。接下来我们为Scope 类添加一个类型转换器,以便这个属性能够被编辑,而且也可以在源代码文件里 自动生成相应的代码。下面是类型转换器的代码: public class ScopeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(String)) return true; return base.CanConvertFrom(context, sourceType); } public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(String)) return true; if (destinationType == typeof(InstanceDescriptor)) return true; return base.CanConvertTo(context, destinationType); } public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { String result = ""; if (destinationType == typeof(String)) { Scope scope = (Scope)value; result = scope.Min.ToString()+"," + Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. scope.Max.ToString(); return result; } if (destinationType == typeof(InstanceDescriptor)) { ConstructorInfo ci = typeof(Scope).GetConstructor(new Type[] {typeof(Int32),typeof(Int32) }); Scope scope = (Scope)value; return new InstanceDescriptor(ci, new object[] { scope.Min,scope.Max }); } return base.ConvertTo(context, culture, value, destinationType); } public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { if (value is string) { String[] v = ((String)value).Split(','); if (v.GetLength(0) != 2) { throw new ArgumentException("Invalid parameter format"); } Scope csf = new Scope(); csf.Min = Convert.ToInt32(v[0]); csf.Max = Convert.ToInt32(v[1]); return csf; } return base.ConvertFrom(context, culture, value); } } 现在我们为类型提供类型转换器,我们在类型前面添加一个 TypeConverterAttribute,如下: [TypeConverter(typeof(ScopeConverter))] public class Scope 添加完以后build 工程,然后切换到测试工程,选中控件,在属性浏览 器里查看属性,现在的Scope 属性可以编辑了,如下图所示: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 我们修改默认的值,然后看看Form 设计器为我们生成了什么代码: this.myListControl1.BackColor = System.Drawing.SystemColors.ActiveCaptionText; this.myListControl1.Item.Add(1); this.myListControl1.Item.Add(2); this.myListControl1.Item.Add(3); this.myListControl1.Item.Add(6); this.myListControl1.Item.Add(8); this.myListControl1.Item.Add(9); this.myListControl1.Location = new System.Drawing.Point(12, 34); this.myListControl1.Name = "myListControl1"; this.myListControl1.Scope = new CustomControlSample.Scope(10, 200); this.myListControl1.Size = new System.Drawing.Size(220, 180); this.myListControl1.TabIndex = 1; this.myListControl1.Text = "myListControl1"; 关键是这一行this.myListControl1.Scope = new CustomControlSample.Scope(10, 200),Scope 类的类型转换器为属性提供了实例化的代码。 在上一篇文章,我为控件添加一个一个复杂属性,并且为这个属性的类型的编写 了一个类型转换器,现在我们来看看这个类型转换器的代码,并解释一下这些代 码的意义。 要实现一个类型转换器,我们必须要重写(override)四个方法: CanConvertFrom()――根据类型参数进行测试,判断是否能从这个类 型转换成当前类型,在本例中我们只提供转换string 和 InstanceDescriptor 类 型的能力。 CanConvertTo()――根据类型参数进行测试,判断是否能从当前类型 转换成指定的类型。 ConvertTo()――将参数value 的值转换为指定的类型。 ConvertFrom()――串换参数value,并返回但书类型的一个对象。 public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { String result = ""; if (destinationType == typeof(String)) { Scope scope = (Scope)value; result = scope.Min.ToString()+"," + scope.Max.ToString(); return result; Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. } if (destinationType == typeof(InstanceDescriptor)) { ConstructorInfo ci = typeof(Scope).GetConstructor(new Type[] {typeof(Int32),typeof(Int32) }); Scope scope = (Scope)value; return new InstanceDescriptor(ci, new object[] { scope.Min,scope.Max }); } return base.ConvertTo(context, culture, value, destinationType); } 上面是ConvertTo 的实现,如果转换的目标类型是string,我将Scope 的两个 属性转换成string 类型,并且用一个“,”连接起来,这就是我们在属性浏览器里看到的 表现形式,如图: 如果转换的目标类型是实例描述器(InstanceDescriptor,它负责生成实例化的代 码),我们需要构造一个实例描述器,构造实例描述器的时候,我们要利用反射机制获得 Scope 类的构造器信息,并在 new 的时候传入Scope 实例的两个属性值。实例描述器会为我 们生成这样的代码:this.myListControl1.Scope = new CustomControlSample.Scope(10, 200);在最后不要忘记调用 base.ConvertTo(context, culture, value, destinationType),你不需要处理的转换类型,交给基类去做好了。 public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { if (value is string) { String[] v = ((String)value).Split(','); if (v.GetLength(0) != 2) { throw new ArgumentException("Invalid parameter format"); } Scope csf = new Scope(); csf.Min = Convert.ToInt32(v[0]); csf.Max = Convert.ToInt32(v[1]); return csf; } return base.ConvertFrom(context, culture, value); Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. } } 上面是ConvertFrom 的代码,由于系统能够直接将实例描述器转换为Scope 类型,所 以我们就没有必要再写代码,我们只需要关注如何将String(在属性浏览出现的属性值的 表达)类型的值转换为Scope 类型。没有很复杂的转换,只是将这个字符串以“,”分拆开, 并串换为Int32 类型,然后 new 一个Scope 类的实例,将分拆后转换的两个整型值赋给Scope 的实例,然后返回实例。在这段代码里,我们要判断一下用户设定的属性值是否有效。比如, 如果用户在Scope 属性那里输入了“10200”,由于没有输入“,”,我们无法将属性的值 分拆为两个字符串,也就无法进行下面的转换,所以,我们要抛出一个异常,通知用户重新 输入。 前面的几篇文章中,我们给控件添加一个复杂的类型Scope,并且给它的类型提 供的一个类型转换器,现在我们可以在属性浏览器中编辑它的值,并且它的值也 被串行化的源代码里了。但是你有没有发现,在属性浏览器里编辑这个属性的值 还是不太方便。因为属性只是“10,200”这种形式的,所以,你必须按照这种 格式来修改,一旦格式错误就会引发异常,比如输入一个“10200”。我们期望 这个属性的每一子属性都能够被独立的编辑就好了,这并非不能实现,而且实现 还很简单。 为了在属性浏览器里能够独立的编辑子属性,我们还要重写两个方法: GetPropertiesSupported()和GetProperties();下面是ScopeConverter 的完整代码: public class ScopeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(String)) return true; return base.CanConvertFrom(context, sourceType); } public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(String)) return true; if (destinationType == typeof(InstanceDescriptor)) return true; return base.CanConvertTo(context, destinationType); } public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. destinationType) { String result = ""; if (destinationType == typeof(String)) { Scope scope = (Scope)value; result = scope.Min.ToString()+"," + scope.Max.ToString(); return result; } if (destinationType == typeof(InstanceDescriptor)) { ConstructorInfo ci = typeof(Scope).GetConstructor(new Type[] {typeof(Int32),typeof(Int32) }); Scope scope = (Scope)value; return new InstanceDescriptor(ci, new object[] { scope.Min,scope.Max }); } return base.ConvertTo(context, culture, value, destinationType); } public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { if (value is string) { String[] v = ((String)value).Split(','); if (v.GetLength(0) != 2) { throw new ArgumentException("Invalid parameter format"); } Scope csf = new Scope(); csf.Min = Convert.ToInt32(v[0]); csf.Max = Convert.ToInt32(v[1]); return csf; } return base.ConvertFrom(context, culture, value); } Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. public override bool GetPropertiesSupported(ITypeDescriptorContext context) { return true; } public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) { return TypeDescriptor.GetProperties(typeof(Scope), attributes); } } 在 GetProperties 方法里,我用TypeDescriptor 获得了Scope 类的所有 的属性描述器并返回。如果你对TypeDescriptor 还不熟悉的话,可以参考MSDN。 重写这两个方法并编译以后,在测试工程里查看控件的属性,你可以看到Scope 是 如下的形式: 前几篇文章我们一直在讨论如何更方便的编辑复杂类型的属性,在这个过程中我 介绍了类型转换器以及如何制作自己的类型转换器来实现属性值的串行化和实 现子属性的编辑。对于Scope 这种级别的复杂属性,一个类型转换器就已经足够 了,但是对于更为复杂的属性,单单使用类型转换器已经不足以应付了,比如我 们常用的Font 属性。 在这种情况下,我们就需要提供更为复杂的编辑方式,比如属性编辑对 话框,你还记得Font 对话框吗?现在我们就来看看如何实现更复杂的属性编辑。 复杂的属性编辑器分为两种类型,一种是弹出式模态对话框属性编辑器,一种式 下拉式属性编辑器。如果你还没有感性的认识的话,可以观察一下TextBox 控件 的属性,Font 属性的编辑器是模态对话框属性编辑器,Dock 属性的编辑器是下 拉式属性编辑器。 接下来我们来制作一个模态对话框编辑器,虽然Scope 属性并不复杂, 但是为了演示的方便,我们还是用它来做例子。 首先我们要做一个用来编辑属性的对话框,在对话框的构造函数里传入 要编辑的属性的值。在对话框类里,声明一个Scope 类型的私有变量_scope 用 以保存传入和编辑后的值。还要增加一个Scope 属性,以便外部环境能够获取编 辑后的结果。对话框的外观如下: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 在这个对话框里,我们要把OK 按钮的DialogResult 属性设为OK(当点 击 OK 按钮时,模态对话框关闭,并返回DialogResult.OK),将 Cancel 按钮的 DialogResult 属性设为Cancel(当点击OK 按钮时,模态对话框关闭,并返回 DialogResult.OK)。另外我们要对用户输入的值做验证,以保证Scope 的 min 和 max 值都是Int32 类型。下边是对话框的代码: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace CustomControlSample { public partial class ScopeEditorDialog : Form { private Scope _scope = null; public ScopeEditorDialog(Scope scope) { InitializeComponent(); _scope = scope; textBox1.Text = _scope.Min.ToString(); textBox2.Text = _scope.Max.ToString(); } private void button1_Click(object sender, EventArgs e) { _scope.Min = Convert.ToInt32(textBox1.Text); _scope.Max = Convert.ToInt32(textBox2.Text); } Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. private void textBox1_Validating(object sender, CancelEventArgs e) { try { Int32.Parse(textBox1.Text); } catch (FormatException) { e.Cancel = true; MessageBox.Show("无效的值", "验证错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void textBox2_Validating(object sender, CancelEventArgs e) { try { Int32.Parse(textBox2.Text); } catch (FormatException) { e.Cancel = true; MessageBox.Show("无效的值", "验证错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } public Scope Scope { get { return _scope; } set { _scope = value; } } } } Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 每一个属性的编辑器都是直接或者间接的派生于UITypeEditor。开发 环境从来也不会直接调用我们编写的模态对话框来编辑属性,而是调用 UITypeEditor 的某些虚方法,所以我们还必须提供一个派生于UITypeEditor 的 类来与开发环境通信。下边的代码实现了Scope 的编辑器: using System; using System.ComponentModel; using System.Drawing.Design; using System.Windows.Forms.Design; using System.Windows.Forms; namespace CustomControlSample { public class ScopeEditor:UITypeEditor { public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) { if (context != null && context.Instance != null) { return UITypeEditorEditStyle.Modal; } return base.GetEditStyle(context); } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { IWindowsFormsEditorService editorService = null; if (context != null && context.Instance != null && provider != null) { editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsE ditorService)); if (editorService != null) { MyListControl control = (MyListControl)context.Instance; ScopeEditorDialog dlg = new ScopeEditorDialog(control.Scope); if (dlg.ShowDialog()== DialogResult.OK) Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. { value = dlg.Scope; return value; } } } return value; } } } 在这个类里,我们重写了两个方法,一个是GetEditStyle,在这个方法 里,我们通知开发环境,属性的编辑器是一个模态对话框。另一个方法是 EditValue,这是最核心的方法,在这个方法里,我们通过上下文环境获得了正 在编辑的控件的实例,并将实例的Scope 属性传递给属性编辑对话框,显示对话 框供用户编辑属性的值,用户编辑完属性的值,并关闭对话框,这时,我们从对 话框里获取编辑后的结果反会给开发环境。 编写完Editor,我们就要将 它应用到MyListControl 的 Scope 属性上,现在的Scope 属性定义如下: [Browsable(true)] [Editor(typeof(ScopeEditor),typeof(UITypeEditor))] public Scope Scope { get { return _scope; } set { _scope = value; } } 我们在Scope 属性前加上了 [Editor(typeof(ScopeEditor),typeof(UITypeEditor))]元数据。Build 工程, 查看实际的效果。在测试工程的窗体上,选中控件,观察Scope 属性,当我们单 击 Scope 属性的值时,在属性值的后边出现了一个按钮,如图: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 当我们点击这个按钮后,弹出了属性编辑的对话框,如图: 我们在对话框里编辑属性的值,并点击 OK 关闭对话框,现在 Scope 属性 值已经被修改了。 在上一篇文章,我介绍了如何编写模态对话框属性编辑器,这篇文章我将介绍如 何编写下拉式属性编辑器。下拉式(DropDown)属性编辑器和模态对话框属性编 辑器的不同之处就是,当你点击属性值修改的时候,模态对话框编辑器是弹出一 个模态对话框,而下拉式属性编辑器却是在紧贴着属性值的地方显示一个下拉的 控件。不知道大家注意到了没有,这里我说的是显示一个下拉的控件,而这个控 件也是需要你去开发的,接下来我还是以Scope 属性为例,介绍一下具体的实现。 首先我们要创建一个用于编辑属性的控件,在本系列文章的开始,我们介 绍了自定义控件有三种类型:复合控件,扩展控件,自定义控件。在本例中我们 制作一个复合控件(Compsite control),复合控件的开发比较简单,不在本系 列文章的讲解范围,我简单做个介绍,在Solution 浏览器里右键点击 CustomControlSample 工程选择Add->User Control⋯,输入文件名 ScopeEditorControl.cs。我们做的这个复合控件上一篇文章介绍的模态对话框 所包含子控件基本一样,除了用于确认和取消的按钮,如下图: 由于我们取消了用于确认和取消的按钮,并且是一个下拉的编辑器控件, 在出现下面三种情况的时候下拉的编辑器控件会关闭:用户敲了回车,用户敲了 ESC 键,用户点击了编辑器以外的地方。当下拉编辑器控件关闭的时候我们就需 要更新属性的值。下边是这个控件的代码: using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Text; using System.Windows.Forms; Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. namespace CustomControlSample { public partial class ScopeEditorControl : UserControl { private Scope _oldScope; private Scope _newScope; private Boolean canceling; public ScopeEditorControl(Scope scope) { _oldScope = scope; _newScope = scope; InitializeComponent(); } public Scope Scope { get { return _newScope; } } private void textBox1_Validating(object sender, CancelEventArgs e) { try { Int32.Parse(textBox1.Text); } catch (FormatException) { e.Cancel = true; MessageBox.Show("无效的值", "验证错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void textBox2_Validating(object sender, CancelEventArgs e) { try { Int32.Parse(textBox2.Text); Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. } catch (FormatException) { e.Cancel = true; MessageBox.Show("无效的值", "验证错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } protected override bool ProcessDialogKey(Keys keyData) { if (keyData == Keys.Escape) { _oldScope = _newScope; canceling = true; } return base.ProcessDialogKey(keyData); } private void ScopeEditorControl_Leave(object sender, EventArgs e) { if (!canceling) { _newScope.Max = Convert.ToInt32(textBox1.Text); _newScope.Min = Convert.ToInt32(textBox2.Text); } } private void ScopeEditorControl_Load(object sender, EventArgs e) { textBox1.Text = _oldScope.Max.ToString(); textBox2.Text = _oldScope.Min.ToString(); } } } 和模态对话框编辑器一样,开发环境并不会直接调用我们的编辑器控 件,而是用过UITypeEditor 类的派生来实现编辑器的调用,所以我们必须实现 一个下拉式编辑器。代码如下: using System; using System.ComponentModel; using System.Drawing.Design; Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. using System.Windows.Forms.Design; using System.Windows.Forms; namespace CustomControlSample { public class ScopeDropDownEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) { if (context != null && context.Instance != null) { return UITypeEditorEditStyle.DropDown; } return base.GetEditStyle(context); } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { IWindowsFormsEditorService editorService = null; if (context != null && context.Instance != null && provider != null) { editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsE ditorService)); if (editorService != null) { MyListControl control = (MyListControl)context.Instance; ScopeEditorControl editorControl = new ScopeEditorControl(control.Scope); editorService.DropDownControl(editorControl); value = editorControl.Scope; return value; } } return value; } Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. } } 看过上一篇文章的朋友应该对这段代码很熟悉,是的,这两个编辑器的代 码只有几行不同之处,在GetEditStyle 方法中,我们返回的是 UITypeEditorEditStyle.DropDown,而不是UITypeEditorEditStyle.Modal,表 明我们的编辑器是一个下拉式的编辑器。在EditValue 中的不同之处是,我们使 用 DropDownControl 方法来显示编辑器。编辑器制作完毕,我们把Scope 以前的 编辑器替换成下拉式编辑器,如下: [Browsable(true)] [Editor(typeof(ScopeDropDownEditor), typeof(UITypeEditor))] public Scope Scope { get { return _scope; } set { _scope = value; } } 现在build CustomControlSample 工程,然后切换到测试工程查看Scope 属性。当 我们点击属性的值,在属性值的后边出现了一个按钮: 当点击这个按钮的时候,下拉的属性编辑器出现了: 好了,属性的编辑到这里就讲完了。 本系列的前面几篇文章讲解了如何来定义属性以及更有效的编辑属性,接下来我 要讲一下控件属性的默认值。如果我们希望自己开发的控件更易于被其它开发者 使用,那么提供默认值是非常值得的。 如果你为属性设定了默认值,那么当开发者修改了属性的值,这个值在 Property Explorer 中将会以粗体显示。VS 为属性提供一个上下文菜单,允许程 Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 序员使用控件把值重置为默认值。当VS 进行控件的串行化时,他会判断那些值 不是默认值,只有不是默认值的属性才会被串行化,所以为属性提供默认值时可 以大大减少串行化的属性数目,提高效率。 那么VS 怎么知道我们的属性值不是默认值了呢?我们需要一种机制来通 知 VS 默认值。实现这种机制有两种方法: 对于简单类型的属性,比如Int32,Boolean 等等这些Primitive 类型, 你可以在属性的声明前设置一个DefaultValueAttribute,在 Attribute 的构造 函数里传入默认值。 对于复杂的类型,比如Font,Color,你不能够直接将这些类型的值传递 给 Attibute 的构造函数。相反你应该提供Reset 和 ShouldSerialize方法,比如 ResetBackgroundColor(),ShouldSerializeBackgroundColor()。VS 能够根据方 法的名称来识别这种方法,比如Reset方法把重置为默认值, ShouldSerialize方法检查属性是否是默认值。过去我们把它称 之为魔术命名法,应该说是一种不好的编程习惯,可是现在微软依然使用这种机 制。我还是以前面几篇文章使用的例子代码。 using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms; using System.ComponentModel; using System.Drawing; namespace CustomControlSample { public class FirstControl : Control { private String _displayText=”Hello World!”; private Color _textColor=Color.Red; public FirstControl() { } // ContentAlignment is an enumeration defined in the System.Drawing // namespace that specifies the alignment of content on a drawing // surface. private ContentAlignment alignmentValue = ContentAlignment.MiddleLeft; [ Category("Alignment"), Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. Description("Specifies the alignment of text.") ] public ContentAlignment TextAlignment { get { return alignmentValue; } set { alignmentValue = value; // The Invalidate method invokes the OnPaint method described // in step 3. Invalidate(); } } [Browsable(true)] [DefaultValue(“Hello World”)] public String DisplayText { get { return _displayText; } set { _displayText =value; Invalidate(); } } [Browsable(true)] public Color TextColor { get { return _textColor; } set { Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. _textColor=value; Invalidate(); } } public void ResetTextColor() { TextColor=Color.Red; } public bool ShouldSerializeTextColor() { return TextColor!=Color.Red; } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); StringFormat style = new StringFormat(); 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; } // Call the DrawString method of the System.Drawing class to write // text. Text and ClientRectangle are properties inherited from // Control. e.Graphics.DrawString( DisplayText, Font, new SolidBrush(TextColor), ClientRectangle, style); Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. } } } 在上面的代码中,我增加了两个属性,一个是DisplayText,这是一个简 单属性,我们只需要在它的声明前添加一个DefaultValue Attribute 就可以了。 另外一个是TextColor 属性,这个复杂类型的属性,所以我们提供了 ResetTextColor 和 ShouldSerializeTextColor 来实现默认值。 默认值的实现就讲完了,但是有一点不要忽视了,你设定了默认值,就应 该相应的初始化这些属性,比如我们例子中的代码: private String _displayText=”Hello World!”; private Color _textColor=Color.Red; 前面的一些文章绝大部分都是要讲控件的设计时的行为,既然涉及到这么多的设 计时行为的代码编写,那么就有必要就一下如何来调试控件的设计行为。 调试控件的设计时行为和调试DLL 的方式非常的相似,因为DLL 是不能 够单独运行的,而一般的控件也会在一个DLL 里。当然如果你不考虑类的可复用 性而把控件写在一个Windows Application 里面也无可厚非,这样调试倒也变的 简单了。但是我们还是要考虑更通常的情况。一般来说,我们调试DLL 时,都是 创建一个可独立运行的应用程序,在这个应用程序里引用你希望调试的DLL 工 程,在DLL 工程的代码里设置断点,然后调试。所以,调试这一类东西,首要的 问题就是找到一个调用它的宿主。调试控件的设计时行为什么样的宿主最好呢, 当然是Visual studio 了,visual studio 里提供了非常全面的设计时支持。下 来我就来演示一下具体的做法。 首先将你要测试的控件所在的工程设为启动工程。在Solution Explorer 里右键点击控件所在的工程,在菜单里选择属性(Properties)进入工程属性设 置界面,点击“Debug”页面,将Start Action 选为“Start External Program”,接下来点击后边的选择按钮选中你的Visual Studio 的可执行程序, 我的Visual Studio 程序位于“D:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe”,你可以根据自己的情况选择。如下图: Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. 在设置完以后工程属性以后,在需要调试的地方设置断点,然后点击F5 或者点击工具栏的运行按钮。当点击以后,visual studio 会运行起来,在运行 起来的Visual studio 里面打开一个应用你这个Assembly 的工程,在这个工程 里切换到Form 设计器界面,选中你的控件,然后编辑你所要调设的功能,比如, 你要调试一个控件的属性的Editor,你在这个editor 类里设置断点,接着在属 性浏览器里编辑这个属性,程序就会停在你设置的断点。 今天我也把写前面的文章的时候用到的源码附上,方便朋友们使用。 最近真的真的太忙了,以至于一个多月都没哟更新我的blog。昨天晚上,一个 网上的朋友看了我的ToolBox 的文章,问我一个问题,他说如何让ToolBox 控件 也能响应键盘操作,也就是用Up,down 按键来选择工具箱控件里的Item,他添 加了键盘事件,但是不起作用。一开始做这个控件的时候也只是演示一下控件的 制作过程,只用了很短的时间做了一个,只考虑了用鼠标选取,没有考虑键盘操 作,我想要添加键盘操作无非重载KeyDown 事件,针对Up,Down 做一些响应就 可以了。可是添加了重载了OnKeyDown 事件后,结果和那位朋友所说的一样,没 有任何作用,我设了断点,调试了一下,发现KeyDown 根本捕获不到Up,Down 按键的点击,是什么原因呢,是不是忘记设控件的风格以便让它能够获得焦点? 于是,我使用了语句: SetStyle(ControlStyles.Selectable, true);依然没有效果,当我们在控 件上按下Down 键的时候,另一个控件获得了焦点。这时Up,Down 按钮只是起到 了导航的作用就像Tab 键一样。 接下来,我在测试工程的窗体上放置了一个ListBox 控件做一个对比,其 实 ToolBox 和 ListBox 在界面表现上有相似之处,就是都有子Item,并且在 Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. ListBox 上点击Down 是起作用的,ListBox 并没有失去焦点,这说明这时Up, Down 按键没有成为导航键。我想Windows 一定是对默认的导航键Up, Down,Left,Right 有默认的处理,除非你希望你的控件希望自己处理这些键。用 反汇编工具看了一下ListBoxControl 控件的源代码,发现一个有趣的函数: protected override bool IsInputKey(Keys keyData) { if ((keyData & Keys.Alt) == Keys.Alt) { return false; } switch ((keyData & Keys.KeyCode)) { case Keys.Prior: case Keys.Next: case Keys.End: case Keys.Home: return true; } return base.IsInputKey(keyData); } 在这里面,ListBoxControl 允许Prior,Next,End,Home 成为有效的输入键, 接着一路跟下去,看看WinForm 控件的基类Control 的这个函数是如何处理的: [UIPermission(SecurityAction.InheritanceDemand, Window=UIPermissionWindow.AllWindows)] protected virtual bool IsInputKey(Keys keyData) { if ((keyData & Keys.Alt) != Keys.Alt) { int num = 4; switch ((keyData & Keys.KeyCode)) { case Keys.Left: case Keys.Up: case Keys.Right: case Keys.Down: num = 5; break; case Keys.Tab: num = 6; break; } if (this.IsHandleCreated) Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. { return ((((int) this.SendMessage(0x87, 0, 0)) & num) != 0); } } return false; } 注意这一行return ((((int) this.SendMessage(0x87, 0, 0)) & num) != 0);0x87 是什么windows 消息呢,打开WinUser.h 文件,发现是WM_GETDLGCODE, 在 MSDN 中的描述是这样的: The WM_GETDLGCODE message is sent to the window procedure associated with a control. By default, the system handles all keyboard input to the control; the system interprets certain types of keyboard input as dialog box navigation keys. To override this default behavior, the control can respond to the WM_GETDLGCODE message to indicate the types of input it wants to process itself. 也就是说windows 用这个消息来判断哪些类型的输入交给控件本身来处 理。然后,我注意到,对于方向导航键,函数都给于一个值5与 this.SendMessage(0x87, 0, 0))的返回值进行与操作,那么 this.SendMessage(0x87, 0, 0))的返回值都可能是什么值呢,WinUser.h 中是这 样声明的: /**//* * Dialog Codes */ #define DLGC_WANTARROWS 0x0001 /* Control wants arrow keys */ #define DLGC_WANTTAB 0x0002 /* Control wants tab keys */ #define DLGC_WANTALLKEYS 0x0004 /* Control wants all keys */ #define DLGC_WANTMESSAGE 0x0004 /* Pass message to control */ #define DLGC_HASSETSEL 0x0008 /* Understands EM_SETSEL message */ #define DLGC_DEFPUSHBUTTON 0x0010 /* Default pushbutton */ #define DLGC_UNDEFPUSHBUTTON 0x0020 /* Non-default pushbutton */ #define DLGC_RADIOBUTTON 0x0040 /* Radio button */ #define DLGC_WANTCHARS 0x0080 /* Want WM_CHAR messages */ #define DLGC_STATIC 0x0100 /* Static item: don't include */ Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only. #define DLGC_BUTTON 0x2000 /* Button item: can be checked */ 5 最贴切的表达就是DLGC_WANTMESSAGE | DLGC_WANTARROWS,也就是将方向键发送给控件处理,对于6呢,也就是 DLGC_WANTMESSAGE| DLGC_WANTTAB,将Tab 键发送给控件处理。 从这段代码里和控件实际的行为我们可以得出一个结论,那就是,控件 本身是不处理方向键和Tab 键的,因为他们有默认的行为,也就是支持焦点在窗 体的控件之间转换。如果你想要处理这些导航键,那么结论很简单,就是重载 IsInputKey 方法,它是一个保护类型的虚方法。 在 ToolBox 控件的代码里重载IsinputKey 方法: protected override bool IsInputKey(Keys keyData) { if ((keyData & Keys.Alt) == Keys.Alt) { return false; } switch ((keyData & Keys.KeyCode)) { case Keys.Up: case Keys.Down: return true; } return base.IsInputKey(keyData); } 当用户点击的键是Up,Down 的时候,返回true,这时我们的OnKeyDown 方法里就可以捕获到Up,Down 的点击事件了。 Generated by Foxit PDF Creator © Foxit Software http://www.foxitsoftware.com For evaluation only.
还剩33页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

karl012

贡献于2011-01-10

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf