组态软件的开发(C#)

在工控领域,我们用到的组态软件有组态王、Cimplicity等,一方面这些软件是收费的,另一方面无论这些软件做得多好,都没办法把自己的品牌打出去,没办法满足各种自定义的需求。于是,我花了两个星期时间,开发了一款简易版的。这是流程图界面:

其实组态软件并没有我们想像的那么难。我们需要的功能无非就是有一张可以灵活编辑的图,这个图里面的元素会根据系统的状态去变化。

一、图片的呈现

我是使用WPF去开发的,首先整个画面是一个Canvas,然后里面放一些Image元素。我们知道,在组态里面,每一个元件有几种状态。例如一个阀,有半闭的状态和打开的状态,一条水管,有静止和向左向右流动的状态。我们设计的方法是,根据系统的数据,判断应该呈现哪一张图,然后把那张图添加在Canvas里面。当系统数据改变时,Canvas去掉旧图,添加新图。

静态的图可以用png、jpg这些格式,动态的图只能使用gif了。WPF默认是不能显示动态图的,我使用了一个第三方库去完成这项任务。有兴趣的朋友可以搜索一下WpfAnimatedGif,这是目前发现显示gif性能最好的一个第三方库。

二、元件的结构

其实在组态图中,有两种元件,一是图片,二是文字。而且,图片有三种拉伸方法,一是随意拉伸,二是只能横向拉伸(例如水平的管路),三是只能竖向位伸。我们把元件类结构定义如下:

其中,Component类完成了所有移动、放缩、旋转的功能,而下面继承的类只是指明了一些额外的属性。

三、图片的编辑

图片的编辑是最为复杂的一项功能。编辑界面如下图所示:

我实现了一些基本的功能,例如选中元件之后,进行拉伸拖拉、放大缩小、旋转等,还有上下移动一层、对齐等功能。在这里面,旋转之后的放缩是最为复杂的。

在WPF里面,元素的旋转都是使用RotateTransform完成的。旋转之后,元素在我们眼中,其Left和Top属性都变了,但其实在代码里,Left和Top并没有变化。这就产生了两个坐标系。我们看到的元件坐标系跟元件在代码里的坐标系是不一样的。而我们用鼠标去拖动元件的时候,鼠标的坐标其实是我们眼中的坐标系,对元件产生作用前,需要先转成元件真实的坐标系。当元件动了以后,它在自己坐标系里的位置需转换成我们眼中的坐标系。这里面需要用到一些微分的概念。具体怎么算的,在这里不赘述,文字很难表达。这是坐标转换的函数:

public void Translate(Point _OriginPoint/*斜旧点*/, Point _p/*斜新点*/, CursorX CurrentCursor, bool PressShift)
{
    if ((int)CurrentCursor < 0x100)
    {
        Point center = new Point() { X = OriginX + OriginWidth / 2, Y = OriginY + OriginHeight / 2 };//中心

        Point p = PointRotate(_p, 0 - RotateAngle, center);//正新点
        Point OriginPoint = PointRotate(_OriginPoint, 0 - RotateAngle, center);//正旧点

        double ChangeX = p.X - OriginPoint.X;//正X变化
        double ChangeY = p.Y - OriginPoint.Y;//正Y变化            

        double NewWidth = OriginWidth;
        double NewHeight = OriginHeight;
        double NewX = OriginX;
        double NewY = OriginY;

        bool do_it = false;
        switch (CurrentCursor)
        {
            case CursorX.WNES:
                NewWidth = OriginWidth - ChangeX;
                NewHeight = OriginHeight - ChangeY;
                if (PressShift)
                {
                    NewHeight = NewWidth * OriginHeight / OriginWidth;
                    ChangeY = OriginHeight - NewHeight;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    NewX = OriginX + ChangeX;
                    NewY = OriginY + ChangeY;
                    do_it = true;
                }
                break;
            case CursorX.ESWN:
                NewWidth = OriginWidth + ChangeX;
                NewHeight = OriginHeight + ChangeY;
                if (PressShift)
                {
                    NewHeight = NewWidth * OriginHeight / OriginWidth;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    do_it = true;
                }
                break;
            case CursorX.ENWS:
                NewWidth = OriginWidth + ChangeX;
                NewHeight = OriginHeight - ChangeY;
                if (PressShift)
                {
                    NewHeight = NewWidth * OriginHeight / OriginWidth;
                    ChangeY = OriginHeight - NewHeight;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    NewY = OriginY + ChangeY;
                    do_it = true;
                }
                break;
            case CursorX.WSEN:
                NewWidth = OriginWidth - ChangeX;
                NewHeight = OriginHeight + ChangeY;
                if (PressShift)
                {
                    NewHeight = NewWidth * OriginHeight / OriginWidth;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    NewX = OriginX + ChangeX;
                    do_it = true;
                }
                break;

            case CursorX.WE:
                NewWidth = OriginWidth - ChangeX;
                NewHeight = OriginHeight;
                ChangeY = 0;
                if (PressShift && ((int)componentType & 0x80) != 0)
                {
                    NewHeight = NewWidth * OriginHeight / OriginWidth;
                    ChangeY = (OriginHeight - NewHeight) / 2;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    NewX = OriginX + ChangeX;
                    NewY = OriginY + ChangeY;
                    do_it = true;
                }
                break;
            case CursorX.EW:
                NewWidth = OriginWidth + ChangeX;
                NewHeight = OriginHeight;
                ChangeY = 0;
                if (PressShift && ((int)componentType & 0x80) != 0)
                {
                    NewHeight = NewWidth * OriginHeight / OriginWidth;
                    ChangeY = (OriginHeight - NewHeight) / 2;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    NewY = OriginY + ChangeY;
                    do_it = true;
                }
                break;
            case CursorX.NS:
                NewWidth = OriginWidth;
                NewHeight = OriginHeight - ChangeY;
                ChangeX = 0;
                if (PressShift && ((int)componentType & 0x20) != 0)
                {
                    NewWidth = NewHeight * OriginWidth / OriginHeight;
                    ChangeX = (OriginWidth - NewWidth) / 2;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    NewX = OriginX + ChangeX;
                    NewY = OriginY + ChangeY;
                    do_it = true;
                }
                break;
            case CursorX.SN:
                NewWidth = OriginWidth;
                NewHeight = OriginHeight + ChangeY;
                ChangeX = 0;
                if (PressShift && ((int)componentType & 0x20) != 0)
                {
                    NewWidth = NewHeight * OriginWidth / OriginHeight;
                    ChangeX = (OriginWidth - NewWidth) / 2;
                }
                if (NewWidth >= 10 && NewHeight >= 10)
                {
                    NewX = OriginX + ChangeX;
                    do_it = true;
                }
                break;
        }

        if (do_it)
        {
            Point center1 = new Point() { X = NewX + NewWidth / 2, Y = NewY + NewHeight / 2 };
            Point center2 = PointRotate(center1, RotateAngle, center);

            Point LeftTop2 = PointRotate(new Point() { X = NewX, Y = NewY }, RotateAngle, center);
            Point LeftTop3 = PointRotate(LeftTop2, 0 - RotateAngle, center2);

            this.X = LeftTop3.X;
            this.Y = LeftTop3.Y;
            this.Width = NewWidth;
            this.Height = NewHeight;

            this.RenderTransform = new RotateTransform(RotateAngle, NewWidth / 2, NewHeight / 2);
        }
    }
    else if ((int)CurrentCursor == 0x100)
    {
        this.X = OriginX + _p.X - _OriginPoint.X;
        this.Y = OriginY + _p.Y - _OriginPoint.Y;
    }
    else
    {
        Point center = new Point() { X = OriginX + OriginWidth / 2, Y = OriginY + OriginHeight / 2 };
        double PlusAngle = TriPointAngle(center, _p, _OriginPoint);
        double NewAngle = OriginAngle + PlusAngle;
        if (PressShift)
        {
            NewAngle = (int)(NewAngle + 22.5) / 45 * 45;
        }
        this.RotateAngle = NewAngle;
    }
}

四、数据的交互

对于组态图,除了呈现图形外,我们还希望:

(1)图形根据系统状态变化而变化。

(2)点击图形时,组态图能向主程序发送一些内容。

关于这两点,我们定义了两个概念,一是显示条件,二是点击事件。

在一个元件里面,包含了多个图片,而每张图片,都有自己的显示条件和点击事件。显示条件和点击事件都是一些表达式,如上图所示,当“1号采样阀状态”为1的时候,绿色的图案就会显示,而当用户点击了这个绿色图案时,主程序就会向“1号采样阀”发送一个0的信号。

组态图控件是通过三个列表跟主程序交互的,分别是显示条件列表、显示条件值列表、点击事件列表。

显示条件列表就是List<string>,例如是{“1号采样阀状态”,"2号采样泵状态","清洗阀状态"}。控件在显示条件输入框里提示用。

显示条件值列表是Dictionary<string,string>,例如是{“1号采样阀状态”=1,"2号采样泵状态"=0,"清洗阀状态"=0}。主程序每隔一段时间向组态控件发送这个列表,组态控件解析每个组件的显示条件,判断显示哪一张图。

点击事件列表也是List<string>,在点击事件框里提示用。点击图片之后,控件调用一个声明好的回调函数,向主程序发送消息。

//初始化显示条件列表和点击事件列表
List<string> list1 = new List<string>();
List<string> list2 = new List<string>();
if (dt1 != null && dt1.Rows.Count != 0)
{
    for (int i = 0; i < dt1.Rows.Count; i++)
    {
        string DeviceName = Convert.ToString(dt1.Rows[i]["DeviceName"]);
        string FactorName = Convert.ToString(dt1.Rows[i]["FactorName"]);
        int DeviceType = Convert.ToInt32(dt1.Rows[i]["DeviceType"]);
        int FactorType = Convert.ToInt32(dt1.Rows[i]["FactorType"]);

        if (FactorType != 4)
        {
            list1.Add(DeviceName + "." + FactorName);
            list1.Add(FactorName);
        }
        else if (FactorType == 4 && DeviceType == 3)
        {
            list2.Add(FactorName);
        }
    }
}

Global.StateList = list1;
Global.CommandList = list2;



//定时更新组态图状态
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
    this.Dispatcher.Invoke(new Action(() =>
    {
        FlowChartCtrl page = Container.Children[0] as FlowChartCtrl;

        Random rand = new Random();

        Dictionary<string, string> dict = new Dictionary<string, string>();
        dict.Add("PLC.1号采样阀", rand.Next(100) > 50 ? "1" : "0");
        dict.Add("PLC.2号采样阀", rand.Next(100) > 50 ? "1" : "0");
        dict.Add("PLC.1号采样泵", DateTime.Now.Second % 10 > 5 ? "1" : "0");
        dict.Add("PLC.2号采样泵", rand.Next(100) > 50 ? "1" : "0");
        dict.Add("高锰酸盐指数分析仪.实时时间", DateTime.Now.ToString());

        page.UpdateData(dict);
    }));
}

 

  • 25
    点赞
  • 105
    收藏
    觉得还不错? 一键收藏
  • 26
    评论
BS组态软件,全称为Brain Studio Configuration Software,是一款用于工业自动化控制系统的配置软件。它通过图形化界面和丰富的功能模块,帮助工程师实现对自动化控制系统的参数配置和监控。BS组态软件的主要功能包括工艺图形显示、实时数据监视、用户权限管理、报警处理等。 BS组态软件以其易用性和灵活性而受到工程师们的青睐。它提供了直观、直观的工艺图形显示功能,使用户能够以图形化方式展示和操作自动化控制系统的各个组成部分。用户可以通过拖拽和放置元素来配置和布置各种设备和工艺流程,从而实现系统的可视化。 同时,BS组态软件提供了实时数据监视功能,允许用户实时查看和记录各个设备的工作状态和数据。用户可以设置数据采集间隔和显示方式,并通过趋势图、报表等方式分析和处理采集到的数据,为工程师们提供了有力的支持和指导。 此外,BS组态软件还具有强大的用户权限管理功能。它通过对用户进行分类和分组,并设置权限级别,确保只有经过授权的用户才能够进行系统配置和操作。这有效地保护了系统的安全性和稳定性。 最后,BS组态软件还提供了全面的报警处理功能。用户可以设置各种报警规则和条件,并在系统报警时及时进行响应和处理。同时,软件还能够自动生成各种报警和事件日志,为工程师们提供了更加全面和可追溯的运行数据。 综上所述,BS组态软件是一款功能强大且易于使用的工业自动化控制系统配置软件。它通过图形化界面、实时数据监视、用户权限管理和报警处理等功能,为工程师们提供了便捷的配置和监控手段,帮助他们更好地实现自动化控制系统的参数配置和调试。
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值