控件库主题 HML.SkinTool
主题框架介绍
该文章详解 HML控件库 12.0.0.0 版本,主题开发框架部分。下面将讲解如何在主题框架下开发使用控件和完全删除主题功能部分代码。
下图为主题框架运作原理:
由上面结构图看出
- 主题文件都是以XML文件格式保存,XML内容保存节点结构为 “Skin / 主题对象所在程序集名称 / 主题对象类名称 / 主题风格名称(二级主题) / 嵌套属性名称”,控件库的主题可以是控件库自带的也可以从外部注册加载方式加载主题XML文件。
- 主题编辑器可以编辑导出自己想要的颜色主题。导出的XML文件可以添加到 HML 项目编译成自带的主题,或者直接通过注册加载外部主题文件方式直接使用。
- 主题管理器的功能:包括加载指定主题,当主题管理器应用某个主题时他会通知所有受管理的主题控制器主题发生变化。
- 主题控制器功能:当收到来自主题管理器通知后,他会修改和他绑定的窗体的背景色和前景色,同时通知受它管理的所有主题对象OnSkinChanged()告诉他们主题发生变化。
- 主题对象(控件):由上图看出要主题对象能实时对主题切换做出反应,它必须与主题控制器关联起来,而主题控制器又必须与主题管理器关联起来。
所有主题功能代码全部放在Skin目录。
Resources 目录下目前存放两个主题xml文件,都是以"嵌入的资源"生成方式存放,同时Skins.cs 同时要定义它们的名称,因为这里存放是控件库自带主题。
SkinController 目录下存放 “主题控制器组件” 。该组件用于实时同步主题样式。
SkinPicker 目录下存放 “主题列表弹层面板” 控件 提供主题切换使用。
主题控件基类 目录下存放 “SkinControl” ,
ISkinObject.cs 继承该接口的对象称为“主体对象”。
SkinManager.cs 主题管理器。
SkinObjectXmlMetadata.cs 主题对象在主题文件存放对应名称绑定。
SkinPropertyAttribute.cs 所以受主题影响的属性都要添加该特性,Enabled 代表属性是否启用主题功能,EnvironmentProperty代表该属性是否为环境属性,因为环境属性的值是可以根据父容器变化而变化的。
Skins.cs 里面包含控件库自带的所有主题名称,他的枚举数应该和Resources 目录下主题xml文件对应。
SkinStyle.cs 主题风格,其实相当于二级主题,例如Button(按钮)它可能应用某一个主题时但它依然想要四种不同的配色方案,这时候就用到。
主题使用教程
使用SkinManager.Apply() 方法切换主题时,如果主题是控件库自带则会先注册加载再应用,如果是外部主题则必须手动调用SkinManager.Register() 注册加载主题再进行手动切换。
当我们把主题控件从工具箱拖放到窗体,或者利用代码创建时,它有4个必备属性。你还要从工具箱拖放一个SkinStyleController 组件到窗体并关联起来。由上面主题框架运作原理知道它靠SkinStyleController 组件维持运行的。
如果通过工具箱拖放添加SkinStyleController 组件到窗体,SkinStyleController 组件带参构造函数会自动把它添加到主题管理器监听列表中管理。如果是通过代码调用SkinStyleController 组件无参构造函数创建该组件,则需要你手动调用它的AddSkinListener()方法添加到主题管理器监听列表中管理。当我们不需要使用SkinStyleController组件时,应该主动调用它的Dispose()或RemoveSkinListener() 方法把它从主题管理器监听列表中移除。由于Form类窗体是微软写好,如果要窗体的背景色也要跟随主题变化,利用它的StyleForm属性把窗体关联起来就可以了。
主题控件开发规范
用 TrackBarPlus (滑块控件)举例子:
开发带主题功能控件流程:
- 继承并实现 ISkinObject 接口 。
- 构造函数调用 this.OnSkinChanged()应用主题。
- 编写派生于 AppearanceObjectBase 的外观类。
- 屏蔽 BackColor 和 ForeColor 属性。
- 利用主题编辑器编辑好对应主题颜色。
第一步:
TrackBarPlus 继承 ISkinObject 接口。
SkinObjectXmlMetadata 属性用途为控件对象保存在主题文件的XPath路径,如果控件父类已经定义了并且你想当前类也继续使用父类主题xml配置,这个属性就可以不用写。如果想重新在主题文件生成新的XPath配置节点就需要重写。
1 private SkinObjectXmlMetadata skinObjectXmlMetadata = null; 2 /// <summary> 3 /// 主题对象在主题文件信息 4 /// </summary> 5 [Browsable(false)] 6 [EditorBrowsable(EditorBrowsableState.Never)] 7 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 8 public virtual SkinObjectXmlMetadata SkinObjectXmlMetadata 9 { 10 get 11 { 12 if (this.skinObjectXmlMetadata == null) 13 this.skinObjectXmlMetadata = new SkinObjectXmlMetadata(Assembly.GetAssembly(typeof(TrackBarPlus)).GetName().Name, typeof(TrackBarPlus).Name); 14 15 return this.skinObjectXmlMetadata; 16 } 17 }
SkinController 属性是为了能让主题控件实时跟随主题变化外观。照抄。
1 private SkinController skinController = null; 2 /// <summary> 3 /// 主题控制器 4 /// </summary> 5 [Description("主题控制器")] 6 [Category("杂项")] 7 [PropertyOrder(-360)] 8 [DefaultValue(null)] 9 [RefreshProperties(RefreshProperties.All)] 10 public virtual SkinController SkinController 11 { 12 get { return this.skinController; } 13 set 14 { 15 if (this.skinController == value) 16 return; 17 18 if (this.skinController != null) 19 this.skinController.RemoveSkinObject(this); 20 21 this.skinController = value; 22 23 if (this.skinController != null) 24 this.skinController.AddSkinObject(this); 25 } 26 }
SkinEnabled 属性控制控件是否具有主题功能,照抄。
1 private SkinEnabledState skinEnabled= SkinEnabledState.Auto; 2 /// <summary> 3 /// 主题是否启用 4 /// </summary> 5 [Description("主题是否启用")] 6 [Category("杂项")] 7 [PropertyOrder(-300)] 8 [DefaultValue(SkinEnabledState.Auto)] 9 [RefreshProperties(RefreshProperties.All)] 10 public virtual SkinEnabledState SkinEnabled 11 { 12 get { return this.skinEnabled; } 13 set 14 { 15 if (this.skinEnabled == value) 16 return; 17 18 this.skinEnabled = value; 19 this.OnSkinChanged(); 20 } 21 }
SkinStyle 属性 相当于二级主题样式风格,照抄。
1 private SkinStyle skinStyle = SkinStyle.Normal; 2 /// <summary> 3 /// 主题风格 4 /// </summary> 5 [Description("主题风格")] 6 [Category("杂项")] 7 [PropertyOrder(-260)] 8 [DefaultValue(SkinStyle.Normal)] 9 [RefreshProperties(RefreshProperties.All)] 10 public virtual SkinStyle SkinStyle 11 { 12 get { return this.skinStyle; } 13 set 14 { 15 if (this.skinStyle == value) 16 return; 17 18 this.skinStyle = value; 19 this.OnSkinChanged(); 20 } 21 }
StyleAppearance 属性是一个 StyleAppearanceObject 的派生类。所有能在主题文件设置的属性只能放在这个类里面。因为主题管理器处理时硬编码写死ISkinObject 接口的名称为 “StyleAppearance” 属性为主题属性的根节点。而且这个类的定义是有规范的。照抄
1 private StyleAppearanceObject stateAppearance; 2 /// <summary> 3 /// 风格外观 4 /// </summary> 5 [Description("风格外观")] 6 [PropertyOrder(-200)] 7 [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] 8 public StyleAppearanceObject StyleAppearance 9 { 10 get 11 { 12 if (this.stateAppearance == null) 13 this.stateAppearance = new StyleAppearanceObject(this, null); 14 return this.stateAppearance; 15 } 16 }
实现 OnSkinChanged() 主题更改方法,当中 SkinManager.SyncSkinValueToProperty(this); 是把一切换的主题文件样式更新到主题对象的所有主题属性上。照抄。
1 /// <summary> 2 /// 主题已更改 3 /// </summary> 4 public virtual void OnSkinChanged() 5 { 6 SkinManager.SyncSkinValueToProperty(this); 7 this.Invalidate(); 8 }
FollowSkinObject 属性是主题设置跟随指定主题对象(自己的设置不再生效,提供给代码使用),例如DatePicker 作为DateInput 的弹层辅助控件使用时DatePicker 主题应该默认跟随DateInput的设置。照抄
1 private ISkinObject followSkinObject = null; 2 /// <summary> 3 /// 主题设置跟随指定主题对象(自己的设置不再生效,提供给代码使用) 4 /// </summary> 5 [Browsable(false)] 6 [EditorBrowsable(EditorBrowsableState.Never)] 7 [Localizable(false)] 8 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 9 public virtual ISkinObject FollowSkinObject 10 { 11 get { return this.followSkinObject; } 12 set 13 { 14 if (this.followSkinObject == value) 15 return; 16 17 this.followSkinObject = value; 18 this.OnSkinChanged(); 19 } 20 }
GetSkinObjectSkinStateCore() 它配合FollowSkinObject 属性使用,因为主题对象的主题启用状态涉及FollowSkinObject 属性参与判断。。照抄
1 public virtual bool GetSkinObjectSkinStateCore() 2 { 3 return SkinManager.GetSkinObjectSkinState(this.FollowSkinObject ?? this); 4 }
InitializeInvalidate() 、Invalidate() 这两个方法需要 显式实现 ,他们得1用途是提供给 StyleAppearanceObject 的派生类用于刷新控件。
1 void ISkinObject.InitializeInvalidate() 2 { 3 // 你可以需要调用重新计算布局控件的方法 4 this.Invalidate(); 5 } 6 7 void ISkinObject.Invalidate() 8 { 9 this.Invalidate(); 10 }
第二步:
TrackBarPlus 的构造函数需要调用 this.OnSkinChanged() 确保控件创建时应用主题样式。
第三步:
为 TrackBarPlus 主题控件定义名为 StyleAppearanceObject 继承 AppearanceObjectBase,它里面所有复杂属性也必须继承AppearanceObjectBase。
它的构造函数这行没办法省略,照抄。AppearanceObjectBase基类提供 InitializeInvalidate() 、Invalidate() 这两个更新方法,而这两个方法最终调用 ISkinObject的 InitializeInvalidate() 、Invalidate() 个方法。这个基类提供通用方法使用,不希望每 StyleAppearanceObject 出现属性以外的东西,因为属性时可以嵌套的。
AppearanceObjectBase 派生类的所有有关主题的属性都要附加上 SkinProperty 特性 无论是否为复杂属性。
关于主题属性,它又分为 “普通属性”、“环境属性”两种,它们的get、set访问器写法有区别。环境1属性的区别在于啊的值可能是来自于它的父容器的对应值。单行的例子 Label控件BackColor 背景色这个属性就是环境属性。你可以发现在你没有设置颜色值时它的背景色会跟随容器发生变化。
由于某些属性默认值不能在编码时确定只能在对象实例化时确认,属性默认值不再采用 DefaultValueAttribute 特性而是统一采用 ShouldSerialize 、Reset 这两个方法来实现。所以所以你看到所有非复杂属性的属性后面都带有这两个方法。
一般情况下StyleAppearanceObject 内部应该重新定义 BackColor和ForeColor 两个属性替换 Control 类型自带的 BackColor和ForeColor 两个属性 不让没办法实现主题功能。
普通属性写法:3个字段 1个属性 2个方法 。属性写法变得复杂。
它的 SkinPropertyAttribute 特性第二个参数必须为 false。
skinActivateColor 的命名规则必须 “skin”+属性名称。它的值为空
defaultActivateColor 的命名规则必须 “default”+属性名称。属性必须要有一个值返回,所以它的值时属性最后防线。
activateColor 的命名规则应该为属性名称但首字母小写或下划线开头+属性名称首字母小写+属性名称
ActivateColor 的命名规则必须为首字母大写,
属性get返回逻辑:主题值 > 手动设置值 >默认值
属性set设值逻辑,把值保存到 activateColor 就可以了
ShouldSerializeActivateColor 的命名规则必须 “ShouldSerialize”+属性名称。内部照抄。
ResetActivateColor 的命名规则必须 “Reset”+属性名称。内部照抄。
1 //主题文件对应控件激活颜色 2 private Color skinActivateColor = Color.Empty; 3 //控件激活颜色默认值 4 private readonly Color defaultActivateColor = SystemColors.Window; 5 //控件激活颜色用户手动设置值 6 private Color activateColor = Color.Empty; 7 /// <summary> 8 /// 控件激活颜色 9 /// </summary> 10 [SkinProperty(true, false)] 11 public Color ActivateColor 12 { 13 get 14 { 15 if (this.GetSkinObjectSkinStateCore()) 16 { 17 return this.skinActivateColor; 18 } 19 20 if (this.activateColor != Color.Empty) 21 { 22 return this.activateColor; 23 } 24 25 return this.defaultActivateColor; 26 } 27 set 28 { 29 if (this.activateColor == value) 30 return; 31 32 this.activateColor = value; 33 this.Invalidate(); 34 } 35 } 36 // VS是否应该把属性当前值生成对应代码到Designer.cs文件中 37 private bool ShouldSerializeActivateColor() 38 { 39 return this.activateColor != Color.Empty; 40 } 41 // VS属性面板右键重置功能赋值 42 private void ResetActivateColor() 43 { 44 this.activateColor = Color.Empty; 45 this.Invalidate(); 46 } 47 48 private Color skinBackColor = Color.Empty; 49 private readonly Color defaultBackColor = SystemColors.Control; 50 private Color backColor = Color.Empty; 51 /// <summary> 52 /// 背景颜色 53 /// </summary> 54 [Description("背景颜色")] 55 [PropertyOrder(-195)] 56 [SkinProperty(true, true)] 57 public Color BackColor 58 { 59 get 60 { 61 if (this.GetSkinObjectSkinStateCore() && this.skinBackColor != Color.Empty) 62 { 63 return this.skinBackColor; 64 } 65 66 if (this.backColor != Color.Empty) 67 { 68 return this.backColor; 69 } 70 71 if (((Control)this.owner).Parent != null) 72 { 73 return ((Control)this.owner).Parent.BackColor; 74 } 75 76 return this.defaultBackColor; 77 } 78 set 79 { 80 if (this.backColor == value) 81 return; 82 83 this.backColor = value; 84 this.Invalidate(); 85 } 86 } 87 private bool ShouldSerializeBackColor() 88 { 89 return this.backColor != Color.Empty; 90 } 91 private void ResetBackColor() 92 { 93 this.backColor = Color.Empty; 94 this.Invalidate(); 95 }
环境属性写法:。
他的 3个字段 1个属性 2个方法 与普通属性写法一样。
唯一不同就是属性的get访问器的返回逻辑如下。主题值 > 手动设置值 > 父容器对应值 >默认值
它的 SkinPropertyAttribute 特性第二个参数必须为 true。
1 //主题文件对应背景颜色 2 private Color skinBackColor = Color.Empty; 3 //背景颜色默认值 4 private readonly Color defaultBackColor = SystemColors.Control; 5 //背景颜色用户手动设置值 6 private Color backColor = Color.Empty; 7 /// <summary> 8 /// 背景颜色 9 /// </summary> 10 [SkinProperty(true, true)] 11 public Color BackColor 12 { 13 get 14 { 15 if (this.GetSkinObjectSkinStateCore() && this.skinBackColor != Color.Empty) 16 { 17 return this.skinBackColor; 18 } 19 20 if (this.backColor != Color.Empty) 21 { 22 return this.backColor; 23 } 24 25 if (((Control)this.owner).Parent != null) 26 { 27 return ((Control)this.owner).Parent.BackColor; 28 } 29 30 return this.defaultBackColor; 31 } 32 set 33 { 34 if (this.backColor == value) 35 return; 36 37 this.backColor = value; 38 this.Invalidate(); 39 } 40 } 41 // VS是否应该把属性当前值生成对应代码到Designer.cs文件中 42 private bool ShouldSerializeBackColor() 43 { 44 return this.backColor != Color.Empty; 45 } 46 // VS属性面板右键重置功能赋值 47 private void ResetBackColor() 48 { 49 this.backColor = Color.Empty; 50 this.Invalidate(); 51 }
第四步:
上面提过应该在StyleAppearanceObject 内部 应该重新定义 BackColor和ForeColor 两个属性替换 Control 类型自带的 BackColor和ForeColor 两个属性。所以如果控件父类链包含Control,我们应该隐藏它并重写它的返回值,值应该指向我们新定义对应值。
1 [Browsable(false)] 2 [EditorBrowsable(EditorBrowsableState.Never)] 3 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 4 public override Color BackColor 5 { 6 get { return this.StyleAppearance.BackColor; } 7 set { } 8 } 9 10 [Browsable(false)] 11 [EditorBrowsable(EditorBrowsableState.Never)] 12 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] 13 public override Color ForeColor 14 { 15 get { return this.StyleAppearance.Normal.ThumbBackColor; } 16 set { } 17 }
第五步:
当我们写好控件后影把它添加到HML.SkinTool 项目 的窗体上,通过为控件编辑好编辑主题后保存成xml文件,你可以直接通过外部调用指定路径xml文件应用主题,或者把xml文件复制到HML项目Skin/Resources 目录后重新编译项目。如果是新名称的主题要改成生成操作为 嵌入的资源 同时为Skins.cs文件添加对应主题名称。
项目完全删除主题框架教程
删除 HML项目的Skin目录。
当你的控件继承 ISkinObject ,删除这个继承,然后删除用 #region 主题 #endregion 包含的代码 但保留当中 StyleAppearance 属性、InitializeInvalidate方法、Invalidate方法。
StyleAppearance 属性内部所有属性的 SkinPropertyAttribute 特性全部删除。
同时属性的get访问器 的 if (SkinManager.GetSkinObjectSkinState 开头这个逻辑节点也删除,因为已经没有主题了。
SkinProperty