由於工作需要,調研過一段時間的工業控制方面的“組態軟件”(SCADA)的開發,組態軟件常用於自動化工業控制領域,其中包括實時數據采集、數據儲存、設備控制和數據展現等功能。其中工控組件的界面展現的實現類似於Windows系統下的各種開發控件,通過各種控件的組裝,和硬件協議的集成,就可以實現對相應設備的控制和實時狀態的顯示。
每個對應的硬件UI展示都可以用一個自定義控件來實現,如下圖的一個溫度計,就可以使用UserControl來實現。
對應的實現代碼如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Linq; using System.Text; using System.Windows.Forms; namespace HMIControls { public partial class ThermometerControl : UserControl { /// <summary> /// 初始化控件 /// 預設繪圖方式:雙緩沖、支持透明背景色、自定義繪制 /// </summary> public ThermometerControl() { SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.ResizeRedraw, true); SetStyle(ControlStyles.Selectable, true); SetStyle(ControlStyles.SupportsTransparentBackColor, true); SetStyle(ControlStyles.UserPaint, true); InitializeComponent(); } // 溫度 private float temperature = 0; [Category("溫度"), Description("當前溫度")] public float Temperature { set { temperature = value; } get { return temperature; } } // 最高溫度 private float highTemperature = 50; [Category("溫度"), Description("最高溫度")] public float HighTemperature { set { highTemperature = value; } get { return highTemperature; } } // 最低溫度 private float lowTemperature = -20; [Category("溫度"), Description("最低溫度")] public float LowTemperature { set { lowTemperature = value; } get { return lowTemperature; } } // 當前溫度數值的字體 private Font tempFont = new Font("宋體", 12); [Category("溫度"), Description("當前溫度數值的字體")] public Font TempFont { set { tempFont = value; } get { return tempFont; } } // 當前溫度數值的顏色 private Color tempColor = Color.Black; [Category("溫度"), Description("當前溫度數值的顏色")] public Color TempColor { set { tempColor = value; } get { return tempColor; } } // 大刻度線數量 private int bigScale = 5; [Category("刻度"), Description("大刻度線數量")] public int BigScale { set { bigScale = value; } get { return bigScale; } } // 小刻度線數量 private int smallScale = 5; [Category("刻度"), Description("小刻度線數量")] public int SmallScale { set { smallScale = value; } get { return smallScale; } } // 刻度字體 private Font drawFont = new Font("Aril", 9); [Category("刻度"), Description("刻度數字的字體")] public Font DrawFont { get { return drawFont; } set { drawFont = value; } } // 字體顏色 private Color drawColor = Color.Black; [Category("刻度"), Description("刻度數字的顏色")] public Color DrawColor { set { drawColor = value; } get { return drawColor; } } // 刻度盤最外圈線條的顏色 private Color dialOutLineColor = Color.Gray; [Category("背景"), Description("刻度盤最外圈線條的顏色")] public Color DialOutLineColor { set { dialOutLineColor = value; } get { return dialOutLineColor; } } // 刻度盤背景顏色 private Color dialBackColor = Color.Gray; [Category("背景"), Description("刻度盤背景顏色")] public Color DialBackColor { set { dialBackColor = value; } get { return dialBackColor; } } // 大刻度線顏色 private Color bigScaleColor = Color.Black; [Category("刻度"), Description("大刻度線顏色")] public Color BigScaleColor { set { bigScaleColor = value; } get { return bigScaleColor; } } // 小刻度線顏色 private Color smallScaleColor = Color.Black; [Category("刻度"), Description("小刻度線顏色")] public Color SmallScaleColor { set { smallScaleColor = value; } get { return smallScaleColor; } } // 溫度柱背景顏色 private Color mercuryBackColor = Color.LightGray; [Category("刻度"), Description("溫度柱背景顏色")] public Color MercuryBackColor { set { mercuryBackColor = value; } get { return mercuryBackColor; } } // 溫度柱顏色 private Color mercuryColor = Color.Red; [Category("刻度"), Description("溫度柱顏色")] public Color MercuryColor { set { mercuryColor = value; } get { return mercuryColor; } } /// <summary> /// 變量 /// </summary> private float X; private float Y; private float H; private Pen p, s_p; private Brush b; /// <summary> /// 繪制溫度計 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void ThermometerControl_Paint(object sender, PaintEventArgs e) { // 溫度值是否在溫度表最大值和最小值范圍內 if (temperature > highTemperature) { //MessageBox.Show("溫度值超出溫度表范圍,系統自動設置為默認值!"); temperature = highTemperature; } if (temperature < lowTemperature) { temperature = lowTemperature; } e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; e.Graphics.TranslateTransform(2, 2); X = this.Width - 4; Y = this.Height - 4; // 繪制邊框(最外邊的框) p = new Pen(dialOutLineColor, 2); e.Graphics.DrawLine(p, 0, X / 2, 0, (Y - X / 2)); e.Graphics.DrawLine(p, X, X / 2, X, (Y - X / 2)); e.Graphics.DrawArc(p, 0, 0, X, X, 180, 180); e.Graphics.DrawArc(p, 0, (Y - X), X, X, 0, 180); // 繪制背景色 X = X - 8; Y = Y - 8; b = new SolidBrush(dialBackColor); e.Graphics.TranslateTransform(4, 4); e.Graphics.FillRectangle(b, 0, X / 2, X, (Y - X)); e.Graphics.FillEllipse(b, 0, 0, X, X); e.Graphics.FillEllipse(b, 0, (Y - X), X, X); // 繪制指示柱 b = new SolidBrush(mercuryBackColor); e.Graphics.FillEllipse(b, X * 2 / 5, (X / 2 - X / 10), X / 5, X / 5); b = new SolidBrush(mercuryColor); e.Graphics.FillEllipse(b, X / 4, (Y - X * 9 / 16), X / 2, X / 2); e.Graphics.FillRectangle(b, X * 2 / 5, (X / 2 + 1), X / 5, (Y - X)); // 在溫度計底部,繪制當前溫度數值 b = new SolidBrush(tempColor); StringFormat format = new StringFormat(); format.LineAlignment = StringAlignment.Center; format.Alignment = StringAlignment.Center; e.Graphics.DrawString((temperature.ToString() + "℃"), tempFont, b, X / 2, (Y - X / 4), format); // 繪制大刻度線,線寬為2 // 繪制小刻度線,線寬為1 // 繪制刻度數字,字體,字號,字的顏色在屬性中可改 p = new Pen(bigScaleColor, 2); // 設置大刻度線的顏色,線粗 s_p = new Pen(smallScaleColor, 1); // 設置小刻度線的顏色,線粗 SolidBrush drawBrush = new SolidBrush(drawColor); // 設置繪制數字的顏色 format.Alignment = StringAlignment.Near; // 設置數字水平對齊為中間,垂直對其為左邊 // 計算要繪制數字的數值 int interval = (int)(highTemperature - lowTemperature) / bigScale; int tempNum = (int)highTemperature; for (int i = 0; i <= bigScale; i++) { float b_s_y = X / 2 + i * ((Y - X - X / 2) / bigScale); // 繪制大刻度線的垂直位置 e.Graphics.DrawLine(p, X / 5, b_s_y, (X * 2 / 5 - 2), b_s_y); // 繪制大刻度線 e.Graphics.DrawString(tempNum.ToString(), drawFont, drawBrush, X * 3 / 5, b_s_y, format); // 繪制刻度數字 tempNum -= interval; // 計算下一次要繪制的數值 // 繪制小刻度線 if (i < bigScale) { for (int j = 1; j < smallScale; j++) { float s_s_y = b_s_y + ((X / 2 + (i + 1) * ((Y - X - X / 2) / bigScale) - b_s_y) / smallScale) * j; e.Graphics.DrawLine(s_p, (X * 3 / 10), s_s_y, (X * 2 / 5 - 2), s_s_y); } } } // 計算當前溫度的位置 float L = Y - X * 3 / 2; H = L * (temperature - lowTemperature) / (highTemperature - lowTemperature); // 繪制當前溫度的位置 b = new SolidBrush(mercuryBackColor); e.Graphics.FillRectangle(b, X * 2 / 5, X / 2, X / 5, (L - H)); } } }
類似的一些實現,如下圖:
對應一些動態線條的繪制,可以采用ZedGraph這個開源的控件來實現,如下圖:
模擬的一些隨時間變化的溫度曲線圖,一些參考代碼如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Linq; using System.Text; using System.Windows.Forms; using ZedGraph; namespace HMIControls { public partial class AirMachine : UserControl { private bool isValveOn; private Timer timer; private double temperature; private Random random = new Random(); private Point arrowLocation1; private Point arrowLocation2; private Point arrowLocation3; // Starting time in milliseconds int tickStart = 0; public AirMachine() { InitializeComponent(); InitUI(); } private void InitUI() { isValveOn = false; this.labelTemperature.Text = "0"; this.button1.Text = "開"; this.button1.BackColor = Color.Snow; timer = new Timer(); timer.Interval = 1000; timer.Tick += new EventHandler(timer_Tick); this.Load += new EventHandler(AirMachine_Load); this.labelArrow1.Visible = false; this.labelArrow2.Visible = false; this.labelArrow3.Visible = false; arrowLocation1 = this.labelArrow1.Location; arrowLocation2 = this.labelArrow2.Location; arrowLocation3 = this.labelArrow3.Location; this.button1.Click += new EventHandler(button1_Click); } private void CreateGraph() { zedGraphControl1.IsEnableZoom = false; zedGraphControl1.IsShowContextMenu = false; // Get a reference to the GraphPane GraphPane myPane = zedGraphControl1.GraphPane; // Set the titles myPane.Title.Text = "實時數據"; myPane.YAxis.Title.Text = "數據"; myPane.XAxis.Title.Text = "時間"; // Change the color of the title myPane.Title.FontSpec.FontColor = Color.Green; myPane.XAxis.Title.FontSpec.FontColor = Color.Green; myPane.YAxis.Title.FontSpec.FontColor = Color.Green; // Save 1200 points. At 50 ms sample rate, this is one minute // The RollingPointPairList is an efficient storage class that always // keeps a rolling set of point data without needing to shift any data values RollingPointPairList list = new RollingPointPairList(1200); // Initially, a curve is added with no data points (list is empty) // Color is blue, and there will be no symbols LineItem myCurve = myPane.AddCurve("溫度值", list, Color.Blue, SymbolType.None); // Fill the area under the curves myCurve.Line.Fill = new Fill(Color.White, Color.Blue, 45F); myCurve.Line.IsSmooth = true; myCurve.Line.SmoothTension = 0.5F; // Increase the symbol sizes, and fill them with solid white myCurve.Symbol.Size = 8.0F; myCurve.Symbol.Fill = new Fill(Color.Red); myCurve.Symbol.Type = SymbolType.Circle; // Just manually control the X axis range so it scrolls continuously // instead of discrete step-sized jumps myPane.XAxis.Scale.Min = 0; myPane.XAxis.Scale.Max = 100; myPane.XAxis.Scale.MinorStep = 1; myPane.XAxis.Scale.MajorStep = 5; // Add gridlines to the plot myPane.XAxis.MajorGrid.IsVisible = true; myPane.XAxis.MajorGrid.Color = Color.LightGray; myPane.YAxis.MajorGrid.IsVisible = true; myPane.YAxis.MajorGrid.Color = Color.LightGray; // Scale the axes zedGraphControl1.AxisChange(); // Save the beginning time for reference tickStart = Environment.TickCount; } void AirMachine_Load(object sender, EventArgs e) { CreateGraph(); } private void UpdateZedGraph(double yValue) { // Make sure that the curvelist has at least one curve if (zedGraphControl1.GraphPane.CurveList.Count <= 0) return; // Get the first CurveItem in the graph LineItem curve = zedGraphControl1.GraphPane.CurveList[0] as LineItem; if (curve == null) return; // Get the PointPairList IPointListEdit list = curve.Points as IPointListEdit; // If this is null, it means the reference at curve.Points does not // support IPointListEdit, so we won't be able to modify it if (list == null) return; // Time is measured in seconds double time = (Environment.TickCount - tickStart) / 1000.0; // 3 seconds per cycle //list.Add(time, Math.Sin(2.0 * Math.PI * time / 3.0)); list.Add(time, yValue); // Keep the X scale at a rolling 30 second interval, with one // major step between the max X value and the end of the axis Scale xScale = zedGraphControl1.GraphPane.XAxis.Scale; if (time > xScale.Max - xScale.MajorStep) { xScale.Max = time + xScale.MajorStep; xScale.Min = xScale.Max - 100.0; } // Make sure the Y axis is rescaled to accommodate actual data zedGraphControl1.AxisChange(); // Force a redraw zedGraphControl1.Invalidate(); } private void UpdataArrowPosition() { this.labelArrow1.Location = new Point(this.labelArrow1.Location.X + 30, this.labelArrow1.Location.Y); if (this.labelArrow1.Location.X >= this.panelPic.Location.X + this.panelPic.Width) { this.labelArrow1.Location = arrowLocation1; } this.labelArrow2.Location = new Point(this.labelArrow2.Location.X + 30, this.labelArrow2.Location.Y); if (this.labelArrow2.Location.X >= this.panelPic.Location.X + this.panelPic.Width) { this.labelArrow2.Location = arrowLocation2; } this.labelArrow3.Location = new Point(this.labelArrow3.Location.X + 30, this.labelArrow3.Location.Y); if (this.labelArrow3.Location.X >= this.panelPic.Location.X + this.panelPic.Width) { this.labelArrow3.Location = arrowLocation3; } } void timer_Tick(object sender, EventArgs e) { temperature = random.NextDouble() * 100; this.labelTemperature.Text = Convert.ToInt32(temperature).ToString(); UpdateZedGraph(temperature); UpdataArrowPosition(); } private void button1_Click(object sender, EventArgs e) { isValveOn = !isValveOn; if (isValveOn) { timer.Start(); this.button1.Text = "關"; this.button1.BackColor = Color.LawnGreen; this.labelTemperature.BackColor = Color.LawnGreen; this.labelArrow1.Visible = isValveOn; this.labelArrow2.Visible = isValveOn; this.labelArrow3.Visible = isValveOn; } else { timer.Stop(); this.button1.Text = "開"; this.button1.BackColor = Color.Snow; this.labelTemperature.Text = "0"; this.labelTemperature.BackColor = Color.Snow; this.labelArrow1.Visible = isValveOn; this.labelArrow2.Visible = isValveOn; this.labelArrow3.Visible = isValveOn; } } } }
整個組態軟件的開發,從底層硬件相關的設備協議到上層的展現都是比較有難度的,特別是現在硬件協議不統一,業界沒有統一的標准,雖然有OPC和BACnet等一些標准協議,但是在實際項目中,有很多的設備是沒有實現OPC的,都是自己的私有協議,要基於這類的硬件做二次開發,需要向商家買協議,這也是成本的問題。
代碼下載:http://download.csdn.net/detail/luxiaoxun/8256371
組態界面開發的一些參考資源:
http://www.codeproject.com/Articles/36116/Industrial-Controls
http://www.codeproject.com/Articles/17559/A-fast-and-performing-gauge
http://dashboarding.codeplex.com/