|
由于工作需要,调研过一段时间的工业控制方面的“组态软件”(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/Artic ... nd-performing-gauge
http://dashboarding.codeplex.com/ |
|