定义控件如何给特殊类型的属性添加默认值了,附自定义GroupBox一枚html
标题有点那啥,但确实能表达我掌握此法后的心情。ide
写自定义控件时每每会有一个需求,就是给属性指定一个默认值(就是能够在VS中右键该属性→重置),若是该属性的类型是内置值类型还好,直接使用DefaultValue特性就好,例如:函数
[DefaultValue(false)] public bool CanSelect { get; set; }
对于可以根据字符串常量转换获得的类型也还好,能够这样:post
[DefaultValue(typeof(Font), "宋体, 9pt")] public Font TitleFont { get; set; }
但这种状况下,DefaultValue的第2个参数必须是字符串常量,不能是变量、字段、属性、方法返回值啥的。题外,一个类型可否从字符串转换获得,依赖的是该类型的TypeConverter特性指定的转换类中的实现。有关TypeConverter的更多信息请参看:测试
http://msdn.microsoft.com/zh-cn/library/system.componentmodel.typeconverter(v=vs.80).aspx字体
回到正题,那么问题来了,若是我想让TitleFont的默认值是SystemFonts.DefaultFont咋办?几经磨难,总算让我学到一招,下面经过一个自定义控件示例说明:this
/// <summary> /// 加强型GroupBox /// </summary> /// <remarks> /// Author:AhDung /// Update:201411181832,可独立设置标题颜色和字体 /// </remarks> public class GroupBoxEx : GroupBox { static Font defaultTitleFont; //定义一个静态字段 /// <summary> /// 默认标题字体 /// </summary> public static Font DefaultTitleFont //封装该静态字段,其实不封装直接使用字段也行,但字段命名必须是DefaultXXX { get { return defaultTitleFont ?? (defaultTitleFont = SystemFonts.DefaultFont); } } Color titleColor; /// <summary> /// 获取或设置标题颜色 /// </summary> [Description("获取或设置标题颜色")] [DefaultValue(typeof(Color), "0, 70, 213")] public Color TitleColor { get { return titleColor; } set { if (titleColor != value) { titleColor = value; this.Invalidate(); } } } Font titleFont; /// <summary> /// 获取或设置标题字体 /// </summary> [Description("获取或设置标题字体")]
public Font TitleFont { get { return titleFont; } set { titleFont = value ?? DefaultTitleFont; //防止属性被设为null this.Invalidate(); } } /// <summary> /// 重置标题字体 /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] protected virtual void ResetTitleFont() //实现一个重置属性默认值的方法,命名须为ResetXXX { this.TitleFont = null; //属性setter中有null处理 } /// <summary> /// 是否显式设置标题字体 /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] protected virtual bool ShouldSerializeTitleFont() //实现一个指示是否把属性值写入窗体Designer文件的方法,命名须是ShouldSerializeXXX { return !titleFont.Equals(DefaultTitleFont); } /// <summary> /// 重绘 /// </summary> protected override void OnPaint(PaintEventArgs e) { if ((Application.RenderWithVisualStyles && (Width >= 10)) && (Height >= 10)) { TextFormatFlags flags = TextFormatFlags.PreserveGraphicsTranslateTransform | TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.TextBoxControl | TextFormatFlags.WordBreak; if (!this.ShowKeyboardCues) { flags |= TextFormatFlags.HidePrefix; } if (this.RightToLeft == RightToLeft.Yes) { flags |= TextFormatFlags.RightToLeft | TextFormatFlags.Right; } GroupBoxRenderer.DrawGroupBox( e.Graphics, this.ClientRectangle, this.Text, this.TitleFont, this.Enabled ? this.TitleColor : SystemColors.ControlDark, flags, this.Enabled ? System.Windows.Forms.VisualStyles.GroupBoxState.Normal : System.Windows.Forms.VisualStyles.GroupBoxState.Disabled); } else { base.OnPaint(e); } } /// <summary> /// 初始化该控件 /// </summary> public GroupBoxEx() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); titleColor = Color.FromArgb(0, 70, 213); ResetTitleFont(); //直接调用重置方法以初始化属性值 } }
说明一下,写这个控件的本意是让GroupBox在NT6下,标题变得显眼一点。 NT5下默认就是显眼的蓝色,但NT6是黑色,不那么显眼,影响程序体验。当然能够直接设置GroupBox的ForeColor和Font属性达到目 的,但这样的话,它里面的子控件会继承,还得把子控件的这俩属性改回来~蛋疼。因此为了能独立设置GroupBox的标题的颜色和字体,增长了 TitleColor和TitleFont这俩自定义属性,也正是想把TitleFont的默认值设为SystemFonts.DefaultFont时 遇到了本文的问题,几经搜索,看了些有用的帖子,后来又从Control类的源码中获得正果(上述例子参考的就是Control类中的标准作法),那么既 然解决了,就想着把招法和控件一块儿与你们分享一下。控件实现没什么好说的,下面主要就为很是规类型的属性指定默认值的招法说一下。url
就用上述控件中类型为Font、名为TitleFont的属性来讲事:spa
- 要有一个同类型的字段或属性,命名必须为Default+属性名,即DefaultTitleFont,而且为static。为该字段/属性赋值想要的默认值,本例为SystemFonts.DefaultFont,可见这里就不像DefaultValue只能赋值内置值类型或字符串常量那么蛋疼了,能够随意赋值~否则还说个球设计
- 要实现一个Reset+属性名的 无参无返回方法,即ResetTitleFont()。该方法的做用是从新把属性赋值为默认值。本例由于在属性的setter中有处理,即赋值为null 时就替换为默认值,因此直接赋值null无碍,若是setter没有这种处理,就须要赋值为上面的DefaultTitleFont~切记。至于修饰符无 所谓,Control中是public virtual,考虑到这个方法不必让外部调用,因此本例是protected virtual。至于加上[EditorBrowsable(EditorBrowsableState.Never)]特性是为了让用户在使用控件时, 避免在VS智能提示中出现该方法,这也是Control中的作法。缘由很显然,这种方法是给设计器用的,不是给人用的,显它作甚~碍眼
- 再实现一个ShouldSerialize+属性名的方法,无参,但要返回bool。 即ShouldSerializeTitleFont(),个方法从字眼上是跟序列化有关的,我没测试序列化,不知道是否有关,但能够确定与是否把默认值 写入窗体的Designer文件有关,就是VS为窗体自动生成的那个含有InitializeComponent()方法的文件,不止如此,没有这方法你 根本玩不转属性重置,缺它不可。方法的逻辑是,若是为属性的赋的值就是默认值,那么就告诉VS不要在InitializeComponent中显式为该属 性赋值了。须要注意的是,返回true表明要显式赋值,因此在写该方法的return时请注意逻辑。修饰符什么的与Reset方法同样,没要求
- 最后是在构造函数中 为属性赋初始值,因为Reset方法就是干这个的,因此本例直接调用这方法。这不是Control的作法,Control的构造函数中没见到调用 Reset方法,但有不少处理,包括调用一些internal方法,懒得追踪了,也没试过不赋初始值会不会有问题,保险起见,仍是赋了一下。这里再扯点题 外,就是经过DefaultValue指定的默认值其实只是在VS中右键→重置时,让VS再也不往InitializeComponent显式赋值,同时在 PropertyGrid中让值再也不粗体显式,并不表明属性的初始值已经设置为DefaultValue指定的值, 什么意思,好比本例,虽然为TitleColor指定了DefaultValue,但若是不在构造函数中初始化titleColor = Color.FromArgb(0, 70, 213)的话,TitleColor值就会是default(Color),即Color.Empty,因此在用DefaultValue后别忘了还得赋 初始值,要记住DefaultValue是不负责赋值的。可是对于用Reset这种方法会不会同样,没试验过,我猜也是不会自动赋初始值的,毕竟初始化是 构造函数的工做,VS再强大再智能,也不太可能自做主张见到Reset就自动往构造函数中插一条~不合适也不科学。因此保险起见,构造函数中我仍是对 TitleFont赋了
最后,晒一下成果:
美白前:
美白后: