一, 引子
因为最近很忙(lan), 很久没发博了. 不少朋友对那个右键弹出菜单和连线的功能很感兴趣, 因为 VS 本身是不包含这种功能的.
大家想这是什么鬼, 怎么我的设计器没有, 其实这是一个微软黑科技, 如果用好, VS 可以打造为你专用的神兵利器.
为什么我要扩展 Visual Studio 的界面设计器? 当时我在设计组态软件的时候面临最大的困难大概就是设计器了. 一套成熟的组态设计器包括: 界面设计器 (包括工具栏, 设计器, 属性管理器), 脚本编辑器(各种语法高亮, 语法检查, 自动完成等等等等), 编译(解释) 器, 调试器, 解决方案管理器(如何组织项目, 导入 / 导出文件, 添加资源, 添加引用等等等等), 说出来吓死人, 这些功能绝对不是我这类单兵作战人员能搞定的. 那是微软, 西门子这种级别的巨型公司以按人年计算的成本完成的. 也曾经想过套用网上开源设计器, 搜了半天, 得出一个结论: 网上的都是一些简单的 DEMO 或者原型设计, 和我想实现的目标还差的太远, 完善的好东西一般是不会开源的.
但是仔细想一下我上面列举的功能, 不就是 Visual Studio 现成的功能吗? 放着这个宇宙第一 IDE 不用, 想自己重新造轮子, 估计写到老都没有什么结果. 于是我想能不能通过扩展 VS, 去实现一些组态软件的特殊要求功能, 比如常用的变量组态编辑器, 连线这类的功能? 万能的谷歌让我找到了我想要的技术: WPF(含 Blend) 设计器扩展.
二, 什么是 WPF 设计器扩展
WPF 设计器, 常规的界面就是 工具栏 + XAML 编辑器 + 界面设计器. 界面设计器包括右键编辑菜单, 设计器装饰(如锚点进行缩放, 旋转), 属性编辑器等. 这些功能已经很强大, 完善了; 但考虑到用户的特殊需求, VS 提供 了强大的扩展功能, 参考 https://msdn.microsoft.com/zh-cn/library/windows/desktop/bb675306(v=vs.90).aspx 的介绍:
WPF 设计器基于一个具有可扩展的体系结构的框架, 用户可以扩展这种框架以创建自己的自定义设计体验.
通过扩展 WPF 设计器对象模型, 可以在很大程度上自定义 WPF 内容的设计时外观和行为. 例如, 可以通过下列方式扩展 WPF 设计器:
利用增强的图形自定义移动并调整标志符号的大小.
向设计图面添加一个标志符号, 在鼠标移动时该标志符号可以使所选控件倾斜.
在不同工具之间修改控件的设计时外观和行为.
WPF 设计器 体系结构支持 WPF 的所有表现力. 这样便可以创建很多以前不可能拥有的可视化设计体验.
也就是说, WPF 设计器扩展提供了一套 API, 可以自定义装饰器(如点选控件出现的旋转, 拖放, 拉伸, 定位锚点), 右键菜单(如编辑, 排序, 对齐, 剪切), 属性编辑器, 并控制它们的行为; 甚至可以改变设计器的外观. 是不是很强大? 然而这一黑科技很少人知道, 而且为了实现设计器扩展, 你必须严格遵守一些特殊的规则, 而且设计器扩展的调试方式也很特殊. 同时, 在 WPF 设计器的扩展基本可以不修改就移植到 Blend.
三, 如何实现设计器扩展
API 总体架构
VS 的状态分为设计时和运行时. 设计时就是你打开 VS, 拖拽控件, 界面布局, 属性设置, 代码编写, 打交道的对象是 Visual Studio; 运行时就是你编译运行自己的 exe 文件.
WPF 的界面设计器, 其核心目标就是对控件 (Contorl) 的控制, 包括对控件的拖放, 旋转, 移动, 属性编辑等. 而在设计时如果要操作控件, 首先要在设计, 编辑过程中通过一些 API"发现" 要操作的控件, 并使其能与 VS 设计器互动. API 这里使用了一个 "提供者模式" 来实现: 对装饰器, 菜单, 属性编辑器等的操作功能, 提供了相应的 Provider 来实现, 如装饰器的 AdornerProvider, 右键菜单的 ContextMenuProvider 等. 所有的 Provider 都遵循这样的场景: 当你做了一个 "选择" 的动作(比如拖动一个控件旋转 - 对应 AdornerProvider 的 Active 事件; 或点了某个右键菜单 - 对应 ContextMenuProvider 的 Execute 事件), 进而通过动作事件的 PrimarySelection 参数获取相对应的 ModelItem - 控件在设计时的 "马甲", 进而通过 ModelItem 的 GetCurrentValue 方法找到你选择的对象. 大家也许会问, 设计器扩展为何要多此一举的对控件加一层外壳 ModelItem, 直接操作控件不就行了吗? 回答是, 你对控件的设计时操作, 例如对控件的激活, 使之成为设计器选中的控件, 这一行为在控件本身并没有定义; 而设计器也要通过自己 "理解" 的上下文才能与控件交互. ModelItem 将用户对控件的操作反馈给设计器, 或者将设计的动作告知用户, 起了关键的中介作用. 而设计器本身的 "马甲" 是 DesignerView, 可以通过这个类获取设计器当前设置, 如当前界面大小, 缩放比例等.
如何实现
要实现一个完整的设计器扩展, 要经历以下过程:
定义元数据, 设计器需要知道哪些控件具有哪些扩展. 这是通过 Metadata 类来实现的: Metadata 类有一个 AttributeTable 方法, 在其中构建了控件和功能 (即相应的 Provider) 的映射关系.
- using Microsoft.Windows.Design.Features;
- using Microsoft.Windows.Design.Metadata;
- [assembly: ProvideMetadata(typeof(HMIControl.VisualStudio.Design.Metadata))]
- namespace HMIControl.VisualStudio.Design
- {
- internal class Metadata : IProvideAttributeTable
- {
- // Accessed by the designer to register any design-time metadata.
- public AttributeTable AttributeTable
- {
- get
- {
- AttributeTableBuilder builder = new AttributeTableBuilder();
- //InitializeAttributes(builder);
- // Add the adorner provider to the design-time metadata.
- builder.AddCustomAttributes(
- typeof(LinkableControl),
- new FeatureAttribute(typeof(ControlAdornerProvider))
- //new FeatureAttribute(typeof(TagComplexContextMenuProvider))
- );
- builder.AddCustomAttributes(
- typeof(HMIControlBase),
- //new FeatureAttribute(typeof(LinkLineAdornerProvider)),
- new FeatureAttribute(typeof(TagComplexContextMenuProvider)));
- builder.AddCustomAttributes(
- typeof(LinkLine),
- new FeatureAttribute(typeof(LinkLineAdornerProvider)),
- new FeatureAttribute(typeof(TagComplexContextMenuProvider)));
- builder.AddCustomAttributes(
- typeof(ButtonBase),
- new FeatureAttribute(typeof(TagWriterContextMenuProvider)));
- builder.AddCustomAttributes(
- typeof(HMIButton),
- new FeatureAttribute(typeof(TagWindowContextMenuProvider)),
- new FeatureAttribute(typeof(TagComplexContextMenuProvider)),
- new FeatureAttribute(typeof(TagWriterContextMenuProvider)));
- builder.AddCustomAttributes(
- typeof(FromTo),
- new FeatureAttribute(typeof(TagWindowContextMenuProvider)));
- return builder.CreateTable();
- }
- }
- }
- }
定义具体的 Provider, 所有的 Provider 都执行如下次序: 根据用户选择, 找到相关控件, 并进行操作, 将操作结果反馈给设计器.
根据设计器扩展的默认规则, 在正确的位置使用正确的命名方式, 否则你的扩展不会出现在设计器. 这些默认规则包括:
命名空间规则: 将设计器扩展项目的命名空间设置为 HMIControl.VisualStudio.Design(HMIControl 即控件库的命名空间), 以便设计器能够发现元数据.
项目路径规则: 将项目的输出路径设置为 "..\HMIControl\bin\"(HMIControl 即控件库的项目路径). 使控件的程序集与元数据程序集位于同一文件夹中, 从而可为设计器启用元数据发现.
如何调试
一段不能加断点调试的代码会给编写者带来很大困扰. 但设计器扩展有一个特殊性: 没法在运行时加断点. 好在微软早就为我们安排好了一切. 具体可参考 https://msdn.microsoft.com/zh-cn/sqlserver/bb514636
即调试时需要更改项目的属性, 设置启动程序为 VS 的可执行文件: devenv.exe. 相当于再打开一个新的 VS 作为运行时. 调试时打开你的设计器操作, 会发现第一个打开的 VS 中已经命中断点了.
四, 组态定制需求的实现
根据组态软件的特殊需求, 有两个重要功能是通过 WPF 设计器扩展实现的: 控件连线和右键弹出表达式编辑器, 具体代码在 LinkableControlDesign 项目中.
界面连线的实现
设计目标: 实现两个 HMI 控件的连线. 每个控件最多有上下左右四个位置(即锚点, 也可以少于四个甚至没有), 连线从 A 控件任一位置引出, 自动寻找路径, 连到 B 控件的任一位置; 路径不能穿越其他控件, 而应自动绕开. 连线均为直线, 不能为圆弧线或斜线; 在控件位置改变时, 连线重新计算并绘制.
设计过程: 具有锚点的控件均继承 LinkableControl 类. 锚点装饰器类为 ControlAdorner, 是一个控件容器, 包含上下左右四个锚点, 每个锚点由 PinAdorner 定义, 包含锚点的外形, 自动生成路径等功能. 路径发现由 PathFinder 类实现. 与设计器交互通过继承 AdornerProvider 类实现.
运行过程: 通过 AdornerProvider 类的 Activate 事件, 获取当前点击 (激活) 的控件并转换为 LinkableControl, 并找到控件的父容器 Panel, 控件的装饰器 ControlAdorner 及其包含的每个 PinAdorner, 设计器包装 DesignerView. 在每个 PinAdorner 的鼠标点击和拖放事件内, 可探索到其他控件的锚点, 规划路径, 生成连线 LinkLine.
同时要考虑设计器进行缩放时路径的变化, 在 DesignerView 的 ZoomLevelChanged 事件中处理.
右键菜单的实现
设计目标: 组态软件一般都有自己的变量表达式编辑器, 用来实现对界面控件的动画效果. 如果要求设计者手工输入表达式, 容易出错, 也没有语法检查, 很麻烦. 但 VS 并没有提供这个功能, 因此我想到了点选控件, 弹出的右键菜单加上一个编辑项. 这就要用到 ContextMenuProvider 的功能.
设计过程: TagComplexContextMenuProvider 继承了 ContextMenuProvider, 如果菜单 "ComplexEditor" 被激活, 触发 Exeute 事件, 则弹出窗体 TagComplexEditor, 以设置控件的动画关联的变量表达式; 操作结果将写回控件的 TagReadText 属性.
未来改进
编辑器改进: 支持命令自动完成, 语法高亮, 更完善的语法检查..
来源: https://www.cnblogs.com/evilcat/p/8859223.html