一、   引子

因为最近很忙(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)的映射关系。

  1. 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

开源纯C#工控网关+组态软件(九)定制Visual Studio的更多相关文章

  1. 开源纯C#工控网关+组态软件

    一.   前言 在园子潜水也七八年了.说来惭愧,这么多年虽然一直自称.NET铁杆粉丝,然仅限于回几个不痛不痒的贴,既没有发布过代码,也没有写过文章. 看着.NET和C#在国外风生水起,国内却日趋没落, ...

  2. 开源纯C#工控网关+组态软件(二)工控网关的实现

    一.   工控网关是什么 网关是物联网和工控系统的核心组件.网关起的是承上启下的作用.上即上位机,电脑/触屏监控系统.MES这些:下即下位机,包括PLC.传感器.嵌入式芯片等. 不同厂家的下位机,往往 ...

  3. 开源纯C#工控网关+组态软件(七)数据采集与归档

    一.   引子 在当前自动化.信息化.智能化的时代背景下,数据的作用日渐凸显.而工业发展到如今,科技含量和自动化水平均显著提高,但对数据的采集.利用才开始起步. 对工业企业而言,数据采集日益受到重视, ...

  4. 开源纯C#工控网关+组态软件(八)表达式编译器

    一.   引子 监控画面的主要功能之一就是跟踪下位机变量变化,并将这些变化展现为动画.大部分时候,界面上一个图元组件的某个状态,与单一变量Tag绑定,比如电机的运行态,绑定一个MotorRunning ...

  5. 开源纯C#工控网关+组态软件(十)移植到.NET Core

    一.   引子 写这个开源系列已经十来篇了.自从十年前注册博客园以来,关注了张善友.老赵.xiaotie.深蓝色右手等一众大牛,也围观了逗比的吉日嘎啦.精密顽石等形形色色的园友.然而整整十年一篇文章都 ...

  6. 开源纯C#工控网关+组态软件(六)图元组件

    一.   图元概述 图元是构成人机界面的基本单元.如一个个的电机.设备.数据显示.仪表盘,都是图元.构建人机界面的过程就是铺排.挪移.定位图元的过程. 图元设计是绘图和编码的结合.因为图元不仅有显示和 ...

  7. 开源纯C#工控网关+组态软件(三)加入一个新驱动:西门子S7

    一.   引子 首先感谢博客园:第一篇文章.第一个开源项目,算是旗开得胜.可以看到,项目大部分流量来自于博客园,码农乐园,名不虚传^^. 园友给了我很多支持,并提出了很好的改进意见.现加入屏幕分辨率自 ...

  8. 开源纯C#工控网关+组态软件(四)上下位机通讯原理

    一.   网关的功能:承上启下 最近有点忙,更新慢了.感谢园友们给予的支持,现在github上已经有.目标是最好的开源组态,看来又近一步^^ 之前有提到网关是物联网的关键环节,它的作用就是承上启下. ...

  9. 开源纯C#工控网关+组态软件(五)从网关到人机界面

    一.   引子 之前都在讲网关,不少网友关注如何实现界面.想了解下位机变量变化,是怎样一步步触发人机界面动画的. 这个步步触发,实质上是变量组(Group)的批量数据变化(DataChange)事件, ...

随机推荐

  1. 关于java中的值传递与引用传递遇到的问题

    来源于:https://www.nowcoder.com/test/question/done?tid=14302398&qid=25373#summary 下列java程序的输出结果为___ ...

  2. Spring boot download file

    Springboot对资源的描述提供了相应的接口,其主要实现类有ClassPathResource.FileSystemResource.UrlResource.ByteArrayResource. ...

  3. SQL Quick Reference From W3Schools

    SQL Statement Syntax AND / OR SELECT column_name(s)FROM table_nameWHERE conditionAND|OR condition AL ...

  4. Java注解(2)-注解处理器(运行时|RetentionPolicy.RUNTIME)

    如果没有用来读取注解的工具,那注解将基本没有任何作用,它也不会比注释更有用.读取注解的工具叫作注解处理器.Java提供了两种方式来处理注解:第一种是利用运行时反射机制:另一种是使用Java提供的API ...

  5. 漫谈Java IO之 Netty与NIO服务器

    前面介绍了基本的网络模型以及IO与NIO,那么有了NIO来开发非阻塞服务器,大家就满足了吗?有了技术支持,就回去追求效率,因此就产生了很多NIO的框架对NIO进行封装--这就是大名鼎鼎的Netty. ...

  6. React Native 轻松集成统计功能(Android 篇)

    关于推送的集成请参考这篇文章,本篇文章将引导你集成统计功能,只需要简单的三个步骤就可以集成统计功能. 第一步 安装 在你的项目路径下执行命令: npm install janalytics-react ...

  7. 团队作业4——第一次项目冲刺(Alpha版本) Day 2

    小队@JMUZJB-集美震惊部 一.Daily Scrum Meeting照片 二.Burndown Chart 燃尽图 三.项目进展 成员 工作 丘雨晨 环境配置 刘向东 数据库搭建,环境配置 江泽 ...

  8. zookeeper提示Unable to read additional data from server sessionid 0x

    配置zookeeper集群,一开始配置了两台机器server.1和server.2. 配置参数,在zoo.cfg中指定了整个zookeeper集群的server编号.地址和端口: server.1=1 ...

  9. L2 约束的最小二乘学习法

    \[ \begin{align*} &J_{LS}{(\theta)} = \frac { 1 }{ 2 } { \left\| \Phi \theta - y \right\| }^{ 2 ...

  10. Java8-如何构建一个Stream

    Stream的创建方式有很多种,除了最常见的集合创建,还有其他几种方式. List转Stream List继承自Collection接口,而Collection提供了stream()方法. List& ...