一, 引子
监控画面的主要功能之一就是跟踪下位机变量变化, 并将这些变化展现为动画. 大部分时候, 界面上一个图元组件的某个状态, 与单一变量 Tag 绑定, 比如电机的运行态, 绑定一个 MotorRunning 信号; 但有些时候不会这么简单, 比如温度计在温度高于 50℃显示红色; 某设备报警, 可能是多个条件其中之一触发的结果; 变量变化触发一系列连锁反应... 如此种种. 考虑到工控行业大部分技术人员并非计算机专业出身, 如何能够用最少的编码解决各种复杂的变量 - 动画绑定问题, 无疑要费一番心思.
二, 方案选型
针对变量动画绑定问题, 可以选择的方案包括如下几种:
脚本编译器
不少大型组态软件包含强大的脚本编辑器, 支持诸如 VBS,Python 甚至 C 脚本语言. 脚本自带语法编辑器, 调试器和编译器, 调用的 API 包罗万象, 如数据库 API, 通讯 API, 画面组态 API... 可以用脚本实现非常复杂的逻辑.
但基于下面几种考虑, 我没有实现这类的脚本编译器:
不同于大部分组态软件包含一个独立的界面设计器, 我用 Visual Studio 来肩挑语法编辑, 调试, 编译和界面设计的重任, 没必要多此一举的搞一个独立的脚本编译器.
C# 结合 Visual Studio 来调用通讯, 数据库链接的各类函数, C# 包含强大的语法功能, 配合. NET 类库几乎无所不能, 同时 C# 也支持脚本化, 没有必要在使用其他脚本语言.
对于复杂的逻辑, 就让 C# 配合 VS 神器来完成吧.
运算符重载.
曾经研究过一个 C# 写的脚本编译系统, 它可以实现两个特定集合间的四则运算和逻辑运算, 如 List1.A+List2.A;List1.A>List2.B. 看上去集合就像一个普通的数值那样参与运算和操作.
运算符重载是 C# 一个强大的语法功能, 可以重载的操作符如下:
运算符 | 可重载性 |
+,-,!,~,++,--,true,false | 可以重载这些一元运算符. |
+,-,*,/,%,&,|,^,<<,>> | 可以重载这些二元运算符. |
==,!=,<,>,<=,>= | 可以重载比较运算符. 必须成对重载. |
&&,|| | 不能重载条件逻辑运算符. |
[] | 不能重载数组索引运算符, 但可以定义索引器. |
() | 不能重载转换运算符, 但可以定义新的转换运算符. |
+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>= | 不能显式重载赋值运算符. |
=,.,?:,->,new,is,sizeof,typeof |
无疑运算符重载用的好可以写出语义更清晰, 更简洁的代码.
比如有一种复数类型 Complex, 有两个坐标 x 和 y; 定义 ComplexA 大于 ComplexB 为: A 的 x,y 中至少有一个大于 B 的 x,y. 我只需要重载 > 操作符 (相应的最好重载 >=,<,<=), 以后只需要 A>B 就能代替重复啰嗦的 A.x>B.x||A.y>B.y. 更可喜的是, 重载后的 >,< 这些运算符, 在. Net 表达式树 (ExpressionTree) 中已经替换了它原来的语义. 因此运算符重载在我这个编译器也有它用武之地.
但出于下面两个原因, 它只适合作为编译引擎的辅助, 而不适合单独使用:
首先运算符重载只针对特定的类型; 对于不熟悉 C# 语法特性的编程者, 理解并正确的使用运算符重载不是件容易的事.
运算符重载可以减少重复的代码, 让语法更简洁; 但依然要写 C# 代码, 不适合大部分工控人员.
订阅事件
如果想省事, 最简单的办法是直接写代码, 例如: 如果一台电机的运行需要 A,B,C 三个前提条件均满足, 我就分别订阅 A,B,C 的变量变化事件, 如果 A 由 fasle 变为 true, 再看看其他两个变量触发没有. 也就是写这样几行代码:
var tag1 = App.Server["A"];
var tag2 = App.Server["B"];
var tag3 = App.Server["C"];
if (tag1 != null && tag2 != null && tag3 != null
{
tag1.ValueChanged += (s, e) =>
{
if (tag1.Value.Boolean && tag2.Value.Boolean && tag3.Value.Boolean)
{
// 执行
}
};
tag2.ValueChanged += (s, e) =>
{
if (tag1.Value.Boolean && tag2.Value.Boolean && tag3.Value.Boolean)
{
// 执行
}
};
tag3.ValueChanged += (s, e) =>
{
if (tag1.Value.Boolean && tag2.Value.Boolean && tag3.Value.Boolean)
{
// 执行
}
};
}
看上去不算复杂吧? 如果界面上有 50 个动画, 这样的代码就要写 50 次. 不但浪费时间, 改起来麻烦, 查起来也麻烦. 更糟糕的是, 不懂编程的人还用不了.
表达式编译器
对于大部分零编程基础的上位机设计人员, 他们需要的是一种没有学习和理解成本的, 简单直观的变量绑定方式.
比如温度计在温度高于 50℃显示红色, 就一句话 [temperature>50] ; 某设备显示报警, 可能是多个报警变量其中之一触发的结果, 只需写 [Alarm1||Alarm2||Alarm3] ... 借助微软强大的表达式引擎, 如果能解析这类变量表达式, 设计者只需要知道图元与变量的逻辑关系; 而极少数表达式也难以企及的功能, 略微懂一点 C# 就可以实现. 这样就可以做到使用简单, 上手容易, 同时又可以满足复杂的需求.
同时还有下面几个额外的好处:
最少的编码量: 在一个界面的 cs 文件里, 几乎没有代码. 绑定逻辑在 XAML 内用直观的方式嵌入:
可以用复制, 粘贴和文本替换等功能减少重复编码;
可以充分利用 WPF 的设计器扩展, 实现一个简单的语法编辑器, 实现语法高亮, 自动完成并执行语法检查;
查找变量逻辑和修改很方便.
这个编译器的主要代码在 Eval 类.
三, 自己实现一个编译器
编译原理
大学计算机都有一门编译原理课程. 当年我也捧着一本教材, 被 "波兰表达式","逆波兰表达式" 绕的云里雾里, 然而逆波兰表达式是实现编译器的关键.
逆波兰表达式的优势在于只用两种简单操作, 入栈和 出栈 就可以搞定任何普通表达式的运算. 其运算方式如下:
如果当前字符为变量或者为数字, 则压栈, 如果是运算符, 则将栈顶两个元素弹出作相应运算, 结果再入栈, 最后当表达式扫描完后, 栈里的就是结果.
如何实现自己的编译器, 微软已经给大家现成的轮子了. 微软的 Expression 类提供了一套拼接, 编译 Lambda 表达式的完整方法, 可以用它轻松定义你自己的语法. 相关知识可以参考博客园 装配脑袋的 自己动手开发编译器
List<TagNodeHandle> _valueChangedList;
private void HMI_Loaded(object sender, RoutedEventArgs e)
{
lock (this)
{
_valueChangedList = cvs1.BindingToServer(App.Server);
}
}
private void HMI_Unloaded(object sender, RoutedEventArgs e)
{
lock (this)
{
App.Server.RemoveHandles(_valueChangedList);
}
}
来源: https://www.cnblogs.com/evilcat/p/8379640.html