基于C#波形显示控件升级版
一、概述
两个月前制作一个项目,需要用到上位机实时显示波形的功能,说实话,在当时我的水平来看,用上位机进行串口的读写这样的功能没问题,但是一遇见画图之类的功能我就瞬间蛋疼了,于是我就上网搜索相关的源代码,很幸运,我搜到了xf_z1988的波形显示控件,当时我感觉如释重负,终于看到了胜利的曙光。同时他还写了一篇关于这个控件的功能介绍http://www.cnblogs.com/xf_z1988/archive/2010/05/11/CSharp_WinForm_Waveform.html
通过仔细拜读,感觉楼主水平果然高超,思维缜密代码清晰,实为我辈学习的对象。里边的功能十分强大,使用十分方便,十分给力。
在项目程序设计的过程中,由于要求比较苛刻,这个波形控件的功能有点不能满足我的需求,于是在一个风雷交加的晚上,我暗暗下定决心,面对这座大山进行代码的修改。到目前为止,我对这个控件做出来了多处修改,具体修改内容如下:
(1)修改了“默认坐标范围”的功能,它现在表示在接收数据的时候,横坐标范围不变,波形图像一直向左平移,且波形的结尾处一直处于波形控件的右边。
(2)简化了放大波形的过程,添加了波形缩小的功能。
(3) 添加了波形拖动的功能,可以很方便的查看历史记录。
(4)添加显示曲线坐标的功能,可以很方便的显示出曲线上某一点的实际坐标,matlab风格的。
(5)添加了截图的功能。
(6) 添加了清屏的按钮。
(7)支持文件内容的读取与显示(由于显示风格比较复杂,没有把波形的储存集成到控件本身,需要用外部的程序进行实现,这段代码在下边会详细介绍,代码很简单,容易实现)。
(8)支持纯链表显示(就是原控件的工作模式),纯文件内容显示,链表和文件混合显示,方便对历史波形进行查看。
(9)在显示波形之前,添加了设置波形显示模式的内容,可以方便不同情况下的显示状况(这一点在下边详细介绍,在这里不做解释)。
(10)优化了部分驱动算法,删除了部分没用的代码(可能是楼主调试的时候忘了删除无用代码吧)。
OK就这些,虽然添加的东西不太多,但是加上之后给人的体验还是蛮不错的。奥对,我把留出来的接口函数里边的ref关键字统统去掉了,只要不是int、double之类的基本类型或者是struct的结构类型,都是可以不用ref关键字的。
升级版本在CSDN下载地址:https://download.csdn.net/download/pengpai0101/10677878
二、修改
对于波形的显示功能,我没有做任何修改该,包括标尺、网格、背景颜色设置,这些内容的具体实现方式请看原作者的博文,就是上边的连接,我这里就不再做解释了。
除了这个之外,别的函数我基本上都动过手术刀,你要问我我到底改过什么地方,这些功能是是什么,我只能轻轻的告诉你:我忘了。我现在只能记住一些主要的地方,那些局部细节的修改是在是太多了,我没法一一列举出来,特别是我这里支持对波形文件的读取,那么对数据的处理基本上改了个遍,现在想想都头皮发麻。鉴于此,在这里我按照逻辑顺序,由浅入深把主要的修改内容简单的介绍一下,我尽量保证文章的简单,突出重点。
2.1截图功能
好多内容都是牵一发而动全身的,只有这个截图功能相对比较独立。
关于截图我是把功能放到FuncPrivate中的CutPic函数里边的。只要调用这个函数,在哪个地方都可以截图。
private void CutPic() { Bitmap bit = new Bitmap(this.Width, this.Height); Graphics g = Graphics.FromImage(bit); g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; //设置图像质量 g.CopyFromScreen(pictureBoxLeft.PointToScreen(Point.Empty), Point.Empty, this.Size);//截图范围 string str = DateTime.Now.ToString("HH_mm_ss") + ".png";//设置路径 bit.Save(str); //保存图形 pictureBoxGraph.Refresh(); //更新显示 panelItemsIN.Refresh(); //刷新按钮显示 }
2.2放大和缩小功能
先说放大功能,在原来的控件中,选择放大的范围之后,需要再点击一下确定才能完成放大的功能,不能说这个功能多余,因为有些孩子不小心动了这个功能,可他们本来是不想放大的,这时候中间加一步却是很实用——不过这种误操作毕竟少数,大部分人不会无缘无故的频繁“误操作”的,所以在这里,我直接跳过了这一步,选取范围之后直接放大,给人以清爽的感觉,简单吧。
private void _Magnify() { _isAutoModeXY = false; //标记,取消自动调整大小模式 坐标自动调整ToolStripMenuItem.Checked = false; //转换坐标为实际值,更新波形显示控件坐标值 if (_fXEnd - _fXBegin < 1.0f || _fYEnd - _fYBegin < 1.0f) return; //防止放大到数据溢出的程度 _changeXYPointsToNum(pictureBoxBigXY.Location.X, pictureBoxBigXY.Location.X + pictureBoxBigXY.Width, pictureBoxBigXY.Location.Y, pictureBoxBigXY.Location.Y + pictureBoxBigXY.Height, ref _fXBegin, ref _fXEnd, ref _fYBegin, ref _fYEnd); pictureBoxGraph.Refresh(); //更新显示 panelItemsIN.Refresh(); //刷新按钮显示 }
缩小功能是在当前的状态下,在界面上点击鼠标右键,这时候就以鼠标点坐标为中心,对波形图进行缩小,不要认为实现起来麻烦,其实很简单的,也是一个函数。跟上边的函数基本类似,这个函数不仅能缩小,还支持蛋疼的放大功能,有兴趣的话你可以试试修改k的值,看看效果。
private void _Reduce(int x, int y, double k) { pictureBoxBigXY.Visible = false; //隐藏[波形放大框] _isAutoModeXY = false; //标记,取消自动调整大小模式 坐标自动调整ToolStripMenuItem.Checked = false; //转换坐标为实际值,更新波形显示控件坐标值 if (_fXEnd - _fXBegin > 100000f || _fYEnd - _fYBegin > 100000 ) return; //防止放大到数据溢出的程度 //放缩系数K,当k大于0.5时为缩小波形,k为0到0.5时为放大波形 _changeXYPointsToNum(x - (int)(k * pictureBoxGraph.Size.Width), x + (int)(k * pictureBoxGraph.Size.Width), y - (int)(k * pictureBoxGraph.Size.Height), y + (int)(k * pictureBoxGraph.Size.Height), ref _fXBegin, ref _fXEnd, ref _fYBegin, ref _fYEnd); pictureBoxGraph.Refresh(); //更新显示 panelItemsIN.Refresh(); //刷新按钮显示 }
2.3波形拖动功能
在说拖动之前,我先介绍一个函数:
private void ReChange(int X, int Y) { pictureBoxBigXY.Visible = false; //隐藏[波形放大框] _isDefaultMoveModeXY = false; //标记,禁用自动移动模式 _isAutoModeXY = false; //标记,取消自动调整大小模式 坐标自动调整ToolStripMenuItem.Checked = false; //转换坐标为实际值,更新波形显示控件坐标值 _changeXYPointsToNum(X, X + pictureBoxGraph.Size.Width - 1, Y, Y + pictureBoxGraph.Size.Height - 1, ref _fXBegin, ref _fXEnd, ref _fYBegin, ref _fYEnd); pictureBoxGraph.Refresh(); //更新显示 panelItemsIN.Refresh(); //刷新按钮显示 }
这个函数的作用其实很简单,就是根据坐标的偏移量,然后让显示的坐标也偏移(刷新一下就是影响到整个图像了)。又遇见了_changeXYPointsToNum函数,原作者设计这个函数的时候肯定没有想到,这个函数竟然在这么多地方用到。
OK,有了这个利器,我们在看看拖动波形是怎么实现的,首先设计个“全局变量”:_startMouse(Point类型的),用来储存鼠标的位置。然后在拖动波形的模式下,点击鼠标左键,先把这个时候的鼠标坐标存起来:
if (_isMoveModeXY && e.Button == MouseButtons.Left) { _startMouse.X = e.X; //记录鼠标移动前的坐标 _startMouse.Y = e.Y; }
之后在鼠标移动的过程中,实时监测当前鼠标的坐标位置,根据储存的_startMouse数据算出偏移量,导入ReChange函数里边刷新一下图像,然后把当前的鼠标坐标位置存到_startMouse里边,便于下次使用。
if (_isMoveModeXY && e.Button == MouseButtons.Left) { ReChange(_startMouse.X - e.X, _startMouse.Y - e.Y); _startMouse.X = e.X; _startMouse.Y = e.Y; }
这样只要你移动,就能算出偏移量,这样就可以实现波形图跟着你的鼠标跑的功能。也不难。
2.4清屏按钮
这个就更简单了,在工具栏里边添加一个按钮,在点击事件里边调用原作者写的f_ClearAllPix函数就可以了(当然这个函数我做了一些修改,适合“文件数据”的操作),之所以“画蛇添足”的加上这个功能,说白了还是为了使用方便而已。
2.5添加了文件所作出的底层修改
添加文件操作是基于两方面考虑,一般来说,我们在做监控的时候,需要对实时保存波形的信息,当然,你可有用自己的方法保存波形,但是你要是想把这个波形再显示出来你还得自己写函数转化成list,这时候我干脆把读取文件波形的功能固定到空间里边得了。另一方面,如果把数据放到链表里边,初期感觉还没神马,可是随着时间的推移,链表里边的数据越来越多,然后,然后,然后就没有然后了,所以把数据存到文件里边,然后链表的负担就小了很多,其中的深意可以慢慢领会。
因为这个功能是我自己编写的,所以我深知其中的Bug。我这个文件数据只支持Int32类型的。另外,每一次刷新,都要遍历数据内容,如果数据非常长,尽管可以分段读取数据,但是还是会死人的,所以我建议,如果是链表的话,要定时清理,如果是文件的话,要定时换文件。当然这个问题其实是可以避免的,不过需要修改底层的算法,我表示现在没精力,往后可以试着修改一下。
好了,废话不多说,首先添加两个链表:
private List<bool> _haveFile = new List<bool>();// 是否启用文件采集波形的标志位
private List<string> _filePath = new List<string>();// 储存波形信号文件的位置
这玩意儿跟_listX、_listColor是同一级别的,鉴于此,需要在添加波形函数里边添加这两句的初始化。
前边说过,我这个波形控件支持纯文件数据读取与显示,也支持文件和链表混合显示,所以再多写几个添加波形的重载函数才行,这些内容都在FuncPublic里边,我就不再粘贴代码了。
有了文件的概念当然就需要改一坨内容,首当其冲的就是_getPoint和_changeToDrawPoints函数,其中前一个函数是分段读取文件里边的数据的函数,这个貌似是我自己写的,用在函数里边。而_changeToDrawPoints函数是把文件或者链表里边的数据一个一个转化为图像的实际坐标,然后返回出去,也是用在pictureBoxGraph_Paint函数里边。
当然在这里我要吐槽一下原作者和自己,在函数设计的时候,_changeToDrawPoints是每读取一个点,就先把这个数据转换一下压到链表里边,也不管这个数据是否符合要求,我也是辛辛苦苦修改结束之后意识到这个浪费资源的算法。好的做法是应该先确定当前界面能够显示的横坐标范围,然后在读取数据的时候先判断数据是否在这个范围之内,再转换坐标压入链表。当然更好方式的是分段读取文件里边的数据、判断转换,这样能减少内存的负担。最好的方法是在前两个的基础上,通过中间值查找算法迅速找出符合规定的点的位置然后再进行处理,这样能够减少时间的负担。可是这些我统统都没有捣鼓,有点懒,有兴趣的话,你们可以自己试着修改一下。
有了这些基础,我们在讲一下下边的内容。
2.6曲线实际坐标的显示
在原来的控件中,你想显示波形上某一点的坐标,恭喜你,光标应着那个点点击鼠标右键就能显示,只不过显示不准确而已。为了能更好的显示曲线上的实际坐标,设计了一个函数ShowCoordinate,其中里边的参数e包含了鼠标指针的坐标信息。代码比较复杂,基本思路是遍历各个波形上边的数据,判断哪个数据和光标比较接近,如果有好多数据跟光标坐标接近,就判断哪个点更接近,然后把选出来的那个点抠出来,在那个位置的附近加一个白色的小方框,然后在控件的左上角显示这个点的实际坐标。给位读者,有兴趣想了解具体的请看看源码吧。
2.7“默认坐标范围”功能的修改
话说当时,我在编写上位机的时候,我同组的那哥们儿就对我吐槽,说我这个波形窗口在接收数据的时候没有向左滚动的感觉,看着不爽,听完他的话,我顿时感觉——的确不爽。但是我又不想再加按钮,于是我就寻寻觅觅,终于发现“默认坐标范围”这个按钮有点鸡肋,于是我就拿它开刀,着手修改。
如果你看懂源码就会知道,真正能显示功能的代码段不在这个功能键事件里边,而是在pictureBoxGraph_Paint函数里边(没错,你没看错,还是这个函数),功能键的click事件里边仅仅是把_isDefaultMoveModeXY这个标志位改变一下,而Paint函数通过判断标志位,然后做出一系列牛逼的相应。如果你没有动我的代码的话,在Drawing文件里边的141行左右,有一个if语句块,这块里边就是实现图形水平移动的功能。基本思路是,遍历数据,如果判断波形的最后一个点的横坐标超出控件的范围,就把控件的横坐标标定值往后拉,当然初始坐标也是,这样可以保证横坐标的范围不变,等于你设定的初始值(纵坐标没这个限制,它是根据波形纵坐标范围进行改变,保证波形能显示完整)。为了更好的显示他的功能,我把这个功能键按钮改为"按默认坐标范围平移"。
2.7初始化显示模式
算上原作者的设计,以及我添加的东东,一共有三种显示的模式:坐标自动调整模式、按默认坐标范围平移、正常显示模式。
当你载入一个波形的时候,肯想要有不同的表现形式,比如说,如果你是从文件里边导入的波形,那你十有八九想看一下这段波形的整体曲线,这时候就需要用到坐标“自动调整模式”,如果你是边采集边显示的那种方式,那你绝对需要用到“按默认坐标范围平移模式”,这些都是可以设置的。
public enum GraphStyle { // 自动调整坐标模式 AutoMode, // 按默认坐标范围平移 DefaultMoveMode, // 正常显示模式 None } public void f_InitMode(GraphStyle mode) { switch (mode) { case GraphStyle.AutoMode: _isAutoModeXY = true; _isDefaultMoveModeXY = false; break; case GraphStyle.DefaultMoveMode: _isAutoModeXY = false; _isDefaultMoveModeXY = true; break; case GraphStyle.None: _isAutoModeXY = false; _isDefaultMoveModeXY = false; break; default: break; } Refresh(); }
代码相当简单,就是根据参数设置标志位就行了。
三、例程
关于这个控件的例程,我仍然用的是原作者给的那个例程,就是里边做了一些修改,一个是色彩的配色,去除了没用的,添加了一些新加的颜色,这一点没什么好讲的,你们自己试试就知道了。
另外在每个加载波形的函数之前添加了f_InitMode函数,来保证波形显示的模式。不信你可以试着点击“模拟串口采样”按钮,绝对给你一种焕然一新的感觉。
最后,我给你讲一下传说中的文件储存与显示的功能。
首先,把随机点显示的那个按钮给换了,换成我这个“带文件的波形显示方式”。当你点击这个按钮的时候,会生成两个链表储存横纵坐标轴,然后在软件的根目录下生成一个“*.wave”文件,最后把文件路径和两个链表同时添加到波形控件里边。OK,剩下的活都是定时器干的,先检测定时器里边的数据长度是否大于设定值,如果是,则先把这两个链表里边的数据按照顺序写到文件里边,然后清空链表数据,“辞旧迎新”。所有的真谛都在“button数据显示模拟5_Click”和“f_ timerRandom_Tick”这两个事件里边。注释很详细,慢慢品味吧。这些都是内部的事儿,其实我们点击那个按钮的时候界面显示还是挺流畅的,只不过在程序的根目录下有个慢慢变大的“*.wave”文件而已。
关于文件操作的还有一个操作,就是没有链表,只有文件的那种,由于我懒得添加按钮,所以我设计的这个就是用鼠标拖着“*.wave”文件到控件上,然后控件就显示出这个文件里边显示的波形。代码在zGraphTest_DragEnter事件里边。
四、警告
这个控件有一些缺陷,我在这里一一介绍一下。
(1) 上边提到过的,链表和文件里边的数据量不能过分的多。
(2) 文件储存只能是整数的类型,如果想要储存浮点小数,则需要自己修改代码。
(3) 波形刷新不能太快,这玩意儿刷新越快,系统负担越大,每隔100毫秒刷新一次就足够了。
(4) 采集数据的周期不能太短,如果你是边采集边显示那种模式,采集速率太快,并且刷新频率太高,界面卡壳很严重,并且还会造成数据的丢失。解决方法是降低采样速率,如果你的系统就是这么高的采样率没法改,那你可以降低刷屏速率(就是上一条),如果你发现降低刷屏速率之后还是很卡,那么建议你把网格给去掉(画网格很浪费时间的),如果你还是觉得卡,得,在采集数据的时候不要刷屏,等采集结束之后统一显示。
(5) 我这个例程用的是VS2008。
祝你好运。