1. 程式人生 > >組態軟體的開發(C#)

組態軟體的開發(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);
    }));
}