C#自定义工业控件开发

转自阿凡卢原文C#自定义工业控件开发

由于工作需要,调研过一段时间的工业控制方面的“组态软件”(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/

时间: 2024-11-06 13:30:20

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

IOS开发自定义CheckBox控件

IOS本身没有系统的CheckBox组件,但是实际开发中会经常用到,所以专门写了一个CheckBox控件,直接上代码 效果图: UICheckBoxButton.h文件如下: #import #import "Common.h" @interface UICheckBoxButton : UIControl { UILabel *label; UIImageView *icon; BOOL checked; id delegate; } @property (retain, nonat

iOS开发UI篇—Quartz2D(自定义UIImageView控件)

一.实现思路 Quartz2D最大的用途在于自定义View(自定义UI控件),当系统的View不能满足我们使用需求的时候,自定义View. 使用Quartz2D自定义View,可以从模仿系统的ImageView的使用开始. 需求驱动开发:模仿系统的imageview的使用过程 1.创建 2.设置图片 3.设置frame 4.把创建的自定义的view添加到界面上(在自定义的View中,需要一个image属性接收image图片参数->5). 5.添加一个image属性(接下来,拿到image之后,应

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

自定义组合控件的过程 1.自定义一个View 一般来说,继承相对布局,或者线性布局 ViewGroup:2.实现父类的构造方法.一般来说,需要在构造方法里初始化自定义的布局文件:3.根据一些需要或者需求,定义一些API方法: ----------------------------------4.根据需要,自定义控件的属性,可以参照TextView属性: 5.自定义命名空间,例如: xmlns:itheima="http://schemas.android.com/apk/res/<包名&

Android自定义控件之自定义组合控件(三)

前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发成本,以及维护成本. 使用自定义组合控件的好处? 我们在项目开发中经常会遇见很多相似或者相同的布局,比如APP的标题栏,我们从三种方式实现标题栏来对比自定义组件带来的好处,毕竟好的东西还是以提高开发效率,降低开发成本为导向的. 1.)第一种方式:直接在每个xml布局中写相同的标题栏布局代码 <?xm

自定义组合控件和在自定义控件中使用自定义属性

今天,整理了一下我平时的笔记,写一个比较简单的自定义组合控件,仅供小白参考,大神请绕道,希望能够对大家有一些帮助 首先,得明白为什么我们需要自定义组合控件,它是因为原有控件并不能满足开发的需求,或者说并不能达到我们想要的一种效果,这个时候,就需要我们自己定义一些控件,以达到目的 ![先来看一下效果](http://img.blog.csdn.net/20160716224219109) 个人总结自定义控件的步骤: 1.先写一个布局,这里我用的是一个相对布局,我这里的相对布局就是根布局了 <?xm

利用ArcGIS Engine、VS .NET和Windows控件开发GIS应用

原文:利用ArcGIS Engine.VS .NET和Windows控件开发GIS应用 此过程说明适合那些使用.NET建立和部署应用的开发者,它描述了使用ArcGIS控件建立和部署应用的方法和步骤. 你可以在下面的目录下找到相应的样例程序: <安装目录>\DeveloperKit\Samples\Developer_Guide_Scenarios\ ArcGIS_Engine\Building_an_ArcGIS_Control_Application\Map_Viewer 注:ArcGIS样

【转】带checkbox的ListView实现(二)——自定义Checkable控件的实现方法

原文网址:http://blog.csdn.net/harvic880925/article/details/40475367 前言:前一篇文章给大家展示了传统的Listview的写法,但有的时候我们并不想在DataHolder类中加一个标识是否选中的checked的成员变量,因为在项目开发中,大部分的ListItemLayout布局都是大家共用的,有些人根本不需要checkbox控件,所以会在初始化的时候把这个控件给隐藏掉,但我们的DataHolder在构造的时候以及ListItemAdapt

Android View measure (二) 自定义UI控件measure相关

本篇模拟三个角色:Android 架构师-小福.Android  控件开发工程师-小黑. Android 开发工程师-小白,下面按照三个角色不同角度分析measure过程. 小福负责分享: measure的本质 measure代码流程 onMeasure方法与MeasureSpec 提出问题 小黑负责分享: 布局控件开发中覆写Measure例子 - ok 从遇到的一个异常说起 什么时候需要覆写onMeaure? - ok view.getWidth与view.getMeasureWidth区别

【转载】基于MFC的ActiveX控件开发(3)

原文:http://iysm.net/?p=122 3.事件 ActiveX 控件使用事件通知容器控件上发生了某些事情.事件的常见示例包括单击控件.使用键盘输入数据和控件状态更改.当发生这些操作时,控件将引发事件以提醒容器. MFC 支持两种事件:常用和自定义.常用事件是 COleControl 类自动处理的事件.自定义事件使控件得以在该控件特定的操作发生时通知容器.控件内部状态发生更改或收到某个窗口消息即属于此类事件. 常用事件 常用事件由 COleControl 类自动引发.COleCont