大多数的组态软件都具有脚本功能,脚本可能是VBS、Lua、C#等语言,也可能是厂家自定义的一种语言。通过脚本,组态软件可以实现非常灵活的功能。
脚本的功能,基本可以定义为:读入外部数据,改变可视化元素。外部数据,最重要的是采集到的数据,也可能是用户创建的配置文件。而改变可视化元素,则包括创建、改变位置、改变状态、删除等操作。
本文通过一个示例,介绍在组态软件中实现脚本功能的方法。示例有以下假定:
开发语言 | C# |
脚本语言 | C# |
软件类型 | WPF |
本文所使用的示例是简单且具有代表性的。本文所使用的脚本为:
string x = GetParameter("A"); //读取参数A,作为X位移
string y = GetParameter("B"); //读取参数B,作为Y位移
string id = CreateComponent("rect"); //在画布中创建一个矩形
MoveComponent(id, x, y); //将矩形移动到参数所定义的位置
可以看出,脚本首先读入外部数据(通过GetParameter方法),然后对可视化元素进行了改变(通过CreateComponent创建,通过MoveComponent移动)。读者在此肯定会提出疑问,GetParameter这些函数是哪里来的?这些是脚本跟主程序交互的API,我们会在一个类中事先定义好,然后传递到脚本中,下文会有详细介绍。
然后我们看软件的界面,如下图所示:
界面左侧,有两个框输入两个参数,然后是输入脚本内容,还有一个运行按钮。在右侧,则是一个画布,用于绘制脚本产生的效果。
1、新建一个WPF工程,然后通过NuGet获取Microsoft.CodeAnalysis.CSharp.Scripting包。
2、在WPF中创建好界面需要的内容(也就是各输入框、按钮、Canvas)。
对应于脚本,实际执行的代码包括获取参数、创建矩形和移动矩形。在本示例中,我们全在MainWindow.cs中实现功能。
private string GetParameter(string name)
{
switch (name)
{
case "A":
return ParaA.Text;
case "B":
return ParaB.Text;
default:
return "";
}
}
private string CreateComponent(string type)
{
if (type == "rect")
{
Random rand = new Random();
string id = $"com{rand.Next(10000):D4}";
var rect = new Rectangle() { Name = id, Width = 200, Height = 100, Fill = Brushes.Red };
RegisterName(id, rect);
DisplayCanvas.Children.Add(rect);
return id;
}
return "";
}
private void MoveComponent(string para)
{
string[] parts = para.Split(',');
string id = parts[0];
double x = double.Parse(parts[1]);
double y = double.Parse(parts[2]);
var rect = FindName(id) as Rectangle;
Canvas.SetLeft(rect, x);
Canvas.SetTop(rect, y);
}
GetParameter方法,根据参数的内容,返回相应输入框的内容。在实际情况中,也可能是根据采集数据的ID,获取采集数据的值。
CreateComponent方法,创建了一个红色矩形,添加到画布中,并返回了矩形的Name。
MoveComponent方法,假定参数的格式是"id,x,y",分解后,找到矩形并移动它。
也就是定义脚本与主程序交互的API。本示例中,没有传入数据对象,而是直接传入函数委托,这样的脚本中可以主动拉取数据或推送动作。
新建一个ScriptInteract类,内容如下所示:
public class ScriptInteract
{
private Func Callback;
public ScriptInteract(Func callback)
{
Callback = callback;
}
public string GetParameter(string name)
{
return Callback("GetParameter", name);
}
public string CreateComponent(string type)
{
return Callback("CreateComponent", type);
}
public void MoveComponent(string id, string x, string y)
{
Callback("MoveComponent", $"{id},{x},{y}");
}
}
由于API很多,为每一个API方法创建一个委托过于繁琐,故只创建一个通用委托,输入函数名和参数,返回一个字符串。
可以看到,上文中的脚本所调用的“未知函数”,就是ScriptInteract类中的方法。
交互类中只有一个通用委托,我们在MainWindow.cs中定义此委托的实现:
private string Callback(string func, string para)
{
switch (func)
{
case "GetParameter":
return GetParameter(para);
case "CreateComponent":
return CreateComponent(para);
case "MoveComponent":
MoveComponent(para);
return "";
default:
return "";
}
}
可以看到,通过回调函数调用了之前在MainWindow.cs中定义的实际执行函数。
然后把此函数作为参数传入到ScriptInteract对象中即可。
ScriptInteract interact = new ScriptInteract(Callback);
执行脚本,也就是点击运行按钮的动作。先摆出代码:
private void RunBtn_Click(object sender, RoutedEventArgs e)
{
ScriptInteract interact = new ScriptInteract(Callback);
var code = ScriptText.Text;
var options = ScriptOptions.Default
.WithImports("System")
.WithReferences(typeof(ScriptInteract).Assembly);
var script = CSharpScript.Create(code, options, typeof(ScriptInteract));
script.RunAsync(interact).Wait();
}
此代码,先创建一个ScriptInteract对象,关联了MainWindow.cs中的回调函数。然后把此参数传给脚本对象。那么,在脚本中就可以调用ScriptInteract所定义的API。
至此,所有功能开发完成。
示例Demo下载