开源纯C#工控网关+组态软件(九)定制Visual Studio
一、 引子
因为最近很忙(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的界面设计器,其核心目标就是对控件(Control)的控制,包括对控件的拖放、旋转、移动、属性编辑等。而在设计时如果要操作控件,首先要在设计、编辑过程中通过一些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 属性。
- 未来改进
编辑器改进:支持命令自动完成、语法高亮、更完善的语法检查。。
快捷键编辑:目前的右键弹出编辑器菜单方式操作还可以进一步改进为快捷键方式。但似乎WPF扩展没有提供快捷键弹出的API,期待进一步完善。
五、 下面的计划
写一系列帖子,把架构、原理讲清楚。大致如下:
github地址:https://github.com/GavinYellow/SharpSCADA。QQ群:102486275