C#自定义工业控件开发

时间:2022-09-22 16:38:57

由于工作需要,调研过一段时间的工业控制方面的“组态软件”(SCADA)的开发,组态软件常用于自动化工业控制领域,其中包括实时数据采集、数据储存、设备控制和数据展现等功能。其中工控组件的界面展现的实现类似于Windows系统下的各种开发控件,通过各种控件的组装,和硬件协议的集成,就可以实现对相应设备的控制和实时状态的显示。

每个对应的硬件UI展示都可以用一个自定义控件来实现,如下图的一个温度计,就可以使用UserControl来实现。

C#自定义工业控件开发

对应的实现代码如下:

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 = ;
[Category("温度"), Description("当前温度")]
public float Temperature
{
set { temperature = value; }
get { return temperature; }
} // 最高温度
private float highTemperature = ;
[Category("温度"), Description("最高温度")]
public float HighTemperature
{
set { highTemperature = value; }
get { return highTemperature; }
} // 最低温度
private float lowTemperature = -;
[Category("温度"), Description("最低温度")]
public float LowTemperature
{
set { lowTemperature = value; }
get { return lowTemperature; }
} // 当前温度数值的字体
private Font tempFont = new Font("宋体", );
[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 = ;
[Category("刻度"), Description("大刻度线数量")]
public int BigScale
{
set { bigScale = value; }
get { return bigScale; }
} // 小刻度线数量
private int smallScale = ;
[Category("刻度"), Description("小刻度线数量")]
public int SmallScale
{
set { smallScale = value; }
get { return smallScale; }
} // 刻度字体
private Font drawFont = new Font("Aril", );
[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(, ); X = this.Width - ;
Y = this.Height - ; // 绘制边框(最外边的框)
p = new Pen(dialOutLineColor, );
e.Graphics.DrawLine(p, , X / , , (Y - X / ));
e.Graphics.DrawLine(p, X, X / , X, (Y - X / ));
e.Graphics.DrawArc(p, , , X, X, , );
e.Graphics.DrawArc(p, , (Y - X), X, X, , ); // 绘制背景色
X = X - ;
Y = Y - ;
b = new SolidBrush(dialBackColor);
e.Graphics.TranslateTransform(, );
e.Graphics.FillRectangle(b, , X / , X, (Y - X));
e.Graphics.FillEllipse(b, , , X, X);
e.Graphics.FillEllipse(b, , (Y - X), X, X); // 绘制指示柱
b = new SolidBrush(mercuryBackColor);
e.Graphics.FillEllipse(b, X * / , (X / - X / ), X / , X / );
b = new SolidBrush(mercuryColor);
e.Graphics.FillEllipse(b, X / , (Y - X * / ), X / , X / );
e.Graphics.FillRectangle(b, X * / , (X / + ), X / , (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 / , (Y - X / ), format); // 绘制大刻度线,线宽为2
// 绘制小刻度线,线宽为1
// 绘制刻度数字,字体,字号,字的颜色在属性中可改
p = new Pen(bigScaleColor, ); // 设置大刻度线的颜色,线粗
s_p = new Pen(smallScaleColor, ); // 设置小刻度线的颜色,线粗
SolidBrush drawBrush = new SolidBrush(drawColor); // 设置绘制数字的颜色
format.Alignment = StringAlignment.Near; // 设置数字水平对齐为中间,垂直对其为左边
// 计算要绘制数字的数值
int interval = (int)(highTemperature - lowTemperature) / bigScale;
int tempNum = (int)highTemperature;
for (int i = ; i <= bigScale; i++)
{
float b_s_y = X / + i * ((Y - X - X / ) / bigScale); // 绘制大刻度线的垂直位置
e.Graphics.DrawLine(p, X / , b_s_y, (X * / - ), b_s_y); // 绘制大刻度线
e.Graphics.DrawString(tempNum.ToString(), drawFont, drawBrush, X * / , b_s_y, format); // 绘制刻度数字
tempNum -= interval; // 计算下一次要绘制的数值 // 绘制小刻度线
if (i < bigScale)
{
for (int j = ; j < smallScale; j++)
{
float s_s_y = b_s_y + ((X / + (i + ) * ((Y - X - X / ) / bigScale) - b_s_y) / smallScale) * j;
e.Graphics.DrawLine(s_p, (X * / ), s_s_y, (X * / - ), s_s_y);
}
}
} // 计算当前温度的位置
float L = Y - X * / ;
H = L * (temperature - lowTemperature) / (highTemperature - lowTemperature);
// 绘制当前温度的位置
b = new SolidBrush(mercuryBackColor);
e.Graphics.FillRectangle(b, X * / , X / , X / , (L - H));
}
}
}

类似的一些实现,如下图:

C#自定义工业控件开发

对应一些动态线条的绘制,可以采用ZedGraph这个开源的控件来实现,如下图:

C#自定义工业控件开发

模拟的一些随时间变化的温度曲线图,一些参考代码如下:

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 = ; public AirMachine()
{
InitializeComponent();
InitUI();
} private void InitUI()
{
isValveOn = false;
this.labelTemperature.Text = "";
this.button1.Text = "开";
this.button1.BackColor = Color.Snow;
timer = new Timer();
timer.Interval = ;
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(); // 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 = ;
myPane.XAxis.Scale.Max = ;
myPane.XAxis.Scale.MinorStep = ;
myPane.XAxis.Scale.MajorStep = ; // 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 <= )
return; // Get the first CurveItem in the graph
LineItem curve = zedGraphControl1.GraphPane.CurveList[] 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 + , 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 + , 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 + , 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() * ;
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 = "";
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/

C#自定义工业控件开发的更多相关文章

  1. Winform自定义键盘控件开发及使用

    最近有学员提出项目中要使用键盘控件,系统自带的osk.exe不好用,于是就有了下面的内容: 首先是进行自定义键盘控件的开发,其实核心大家都知道,就是利用SendKeys.Send发送相应 的字符,但是 ...

  2. 在IE中点击转跳,并打开chorme浏览器继续浏览指定页面,IE自定义ocx控件开发

    因项目需要,需要开发一个功能:在IE中点击转跳,并打开chorme浏览器继续浏览指定页面. 分析需求后,参考了: https://www.cnblogs.com/ffjiang/p/7908025.h ...

  3. 自定义select控件开发

    目的:select下拉框条目太多(上百),当用户选择具体项时会浪费用户很多时间去寻找,因此需要一个搜索框让用户输入关键字来匹配列表,便于用户选择 示例图: 1.html结构 <div class ...

  4. iOS开发UI篇—Quartz2D&lpar;自定义UIImageView控件&rpar;

    iOS开发UI篇—Quartz2D(自定义UIImageView控件) 一.实现思路 Quartz2D最大的用途在于自定义View(自定义UI控件),当系统的View不能满足我们使用需求的时候,自定义 ...

  5. 【Android开发日记】之入门篇(十四)——Button控件&plus;自定义Button控件

        好久不见,又是一个新的学期开始了,为什么我感觉好惆怅啊!这一周也发生了不少事情,节假日放了三天的假(好久没有这么悠闲过了),实习公司那边被组长半强制性的要求去解决一个后台登陆的问题,结果就是把 ...

  6. Android开发之自定义组合控件

    自定义组合控件的步骤1.自定义一个View,继承ViewGroup,比如RelativeLayout2.编写组合控件的布局文件,在自定义的view中加载(使用View.inflate())3.自定义属 ...

  7. Android开发学习笔记-自定义组合控件的过程

    自定义组合控件的过程 1.自定义一个View 一般来说,继承相对布局,或者线性布局 ViewGroup:2.实现父类的构造方法.一般来说,需要在构造方法里初始化自定义的布局文件:3.根据一些需要或者需 ...

  8. 安卓自定义组合控件--toolbar

    最近在学习安卓APP的开发,用到了toolbar这个控件, 最开始使用时include layout这种方法,不过感觉封装性不好,就又改成了自定义组合控件的方式. 使用的工具为android stud ...

  9. Android自定义控件之自定义组合控件

    前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发 ...

随机推荐

  1. requirejs:模块加载&lpar;require&rpar;及定义&lpar;define&rpar;时的路径小结

    原文地址:http://www.tuicool.com/articles/7JBnmy 接触过requirejs的童鞋可能都知道,无论是通过define来定义模块,还是通过require来加载模块,模 ...

  2. 1、Android Studio集成极光推送&lpar;Jpush&rpar; 报错 java&period;lang&period;UnsatisfiedLinkError&colon; cn&period;jpush&period;android&period;service&period;PushProtoco

    Android studio 集成极光推送(Jpush) (华为手机)报错, E/JPush: [JPushGlobal] Get sdk version fail![获取sdk版本失败!] W/Sy ...

  3. vs2008&sol;2010安装无法打开数据文件解决方案

    本人在安装VS2008或2010时,在开始的第一个页面(进度条大约加载到75%左右),提示“无法打开数据文件 'C:/Documents and Settings/Administrator/Loca ...

  4. Ways to access Oracle Database in PostgreSQL

    Today, organizations stores information(data) in different database systems. Each database system ha ...

  5. 【转载】Mysql创建表时报错error150

    从mysql数据库中导出正常数据库的脚本语句,而后使用脚本语句创建数据库的过程中,执行语句提示Can't Create Table 'XXX' erro150的错误,语句执行中断,创建table失败, ...

  6. faster-rcnn 笔记

    2019-02-18,15点00 ''' 下面是别人写的原始的笔记,我在上面自己补充了一些. ''' #https://www.cnblogs.com/the-home-of-123/p/974796 ...

  7. 大话Java中的哈希&lpar;hash&rpar;结构(一)

    o( ̄▽ ̄)d 小伙伴们在上网或者搞程序设计的时候,总是会听到关于“哈希(hash)”的一些东西.比如哈希算法.哈希表等等的名词,那么什么是hash呢? 一.相关概念 1.hash算法:一类特殊的算法 ...

  8. Spark分析之TaskScheduler

    TaskScheduler概述: TaskScheduler是一个可插拔任务调度接口,通过不同的SchedulerBackend进行任务的调度.主要功能如下: 1.一个TaskScheduler只为一 ...

  9. linux内存分配方法总结【转】

    转自:http://www.bkjia.com/Linuxjc/443717.html 内存映射结构: 1.32位地址线寻址4G的内存空间,其中0-3G为用户程序所独有,3G-4G为内核占有. 2.s ...

  10. BZOJ1022 &lbrack;SHOI2008&rsqb;小约翰的游戏John 【博弈论】

    1022: [SHOI2008]小约翰的游戏John Time Limit: 1 Sec  Memory Limit: 162 MB Submit: 3014  Solved: 1914 [Submi ...