转自http://blog.csdn.net/xianglitian/article/details/6100767
先上一张效果图吧
这个是我曾经上传过的一个资源——自定义图谱控件使用范例。记得那个时候应该是毕业设计如火如荼日渐焦灼的时期,论坛里很多朋友询问有没有什么好的绘制图谱的控件。当然多数网友推荐mschart等成熟控件,不过我一直建议对于简单的图谱可以自行绘制。因为我现在的工作就是做上位机开发,在我的项目中也需要显示采集上来的数据,刚到公司时就一直在做这个图谱控件,后来经过一段时间的使用自问还是比较稳定的,而且可以很好的实现基本功能。项目中在这个控件基础上还做了相当于matlab中image功能一个可以表达三维信息的图谱绘制控件,实际应用使用效果也不错。
这个控件上传后再在论坛遇到类似的问题我都会推荐这个例子给大家试用,得到的反馈也还算可以。这些天想写些技术博客,一方面系统的数理一下知识,另一方面也是可以和朋友们交流以便共同提高。不过想要动笔的时候发现自己掌握的那些小伎俩都拿不太上台面,于是想起这个例子,以它为基础写点东西应该还看得过去吧,呵呵。
这是一个自定义控件,在自定义控件实现信号灯一文中我提过现在做东西我喜欢通过自定义控件实现,尤其是这种和MFC标准控件向去深远的控件。所以很自然的,这个控件也是从CWnd派生的一个自定义控件。关于自定义控件的一些基础的东西可以参见自定义控件实现信号灯中的相关介绍。这里主要介绍一下这个图谱控件的核心功能及其实现方法。我将这个控件的类命名为CGraphView,其声明的代码如下
- void CGraphView::DrawGraphy(CDC *pDC, CRect rectCoord)
- {
- CRect rectView;
- CRgn rgnTemp, rgnView;
- CPen penLine, *pOldPen;
- INT_PTR nCount = m_dataGraph.GetCount();
- if(nCount==0)
- return;
- double dbData;
- int nOffsetX, nOffsetY;
- int nRangX = abs(int((m_dbEndX-m_dbStartX)*m_dbResolutionX)),
- nRangY = abs(int((m_dbEndY-m_dbStartY)*m_dbResolutionY));
- int nCoordWith = rectCoord.Width(),
- nCoordHeight = rectCoord.Height(),
- nOriginX = rectCoord.left-int(m_dbStartX*nCoordWith/nRangX),
- nOriginY = rectCoord.bottom+int(m_dbStartY*nCoordHeight/nRangY);
- rgnTemp.CreateRectRgnIndirect(rectCoord);
- pDC->SelectObject(rgnTemp);
- penLine.CreatePen(PS_SOLID, 1, m_clrWave);
- pOldPen = pDC->SelectObject(&penLine);
- dbData = m_dataGraph.ElementAt(0);
- nOffsetY = int(nCoordHeight*dbData/nRangY);
- pDC->MoveTo(nOriginX, nOriginY-nOffsetY);
- for(int i=1; i<nCount; i++)
- {
- dbData = m_dataGraph.ElementAt(i);
- nOffsetX = int(nCoordWith*(i+1)/nRangX);
- nOffsetY = int(nCoordHeight*dbData/nRangY);
- pDC->LineTo(nOriginX+nOffsetX, nOriginY-nOffsetY);
- }
- pDC->SelectObject(pOldPen);
- GetClientRect(rectView);
- rgnView.CreateRectRgnIndirect(rectView);
- pDC->SelectObject(rgnView);
- }
阅读代码不难发现其实这个控件绘制图谱的基本思路就是描点绘图,通过MoveTo、LineTo函数把数据样本点串联起来。在控件中我通过CArray数组m_dataGraph存储绘制数据,而通过LoadGraphyData实现对数据的更新。
需要说明的是仿真绘图需要将数据映射到绘制坐标系中,否则绘制的图谱不具有分析意义。因此如何构建坐标系,如何计算映射关系就显得比较关键。这里我以x轴为例说一下我这个控件中这部分的实现,y轴原理与其相同。成员变量m_dbStartX, m_dbEndX标记了图谱x轴的起始坐标和终止坐标,m_nDivisionX变量则记录了显示图谱时的坐标轴分段。效果图所示的就是m_dbStartX=0、m_dbEndX=100、m_nDivisionX=10时x轴的效果,相信对比图示大家应该可以明白这几个参数的含义。m_dbResolutionX记录的是每一个单位刻度的分辨率,参考效果图,坐标轴长度是100,如果我们的绘图精度是1那么每个刻度内就应该绘制1个点,m_dbResolutionX就为1;如果我们的绘图精度是0.1,那么每个刻度内就应该绘制10个点,m_dbResolutionX就为10。也就是说,一屏幕内绘制点的个数最多为(m_dbEndX-m_dbStartX)*m_dbResolutionX个,图中是分辨率为1的效果。明确这些参数的含义就可以确定坐标系了,SetGraphyView和SetResolution两个函数的主要功能就是设置相关参数。
说到坐标映射,继续以x轴为例DrawGraphy函数中nRangX = abs(int((m_dbEndX-m_dbStartX)*m_dbResolutionX))一句计算出了x轴包含的数据点的总数,而参数rectCoord是坐标系即图中白色区域所占的rect,也就是说nCoordWith = rectCoord.Width()一句得到的是x轴长度的像素值。这样nCoordWith/nRangX就是每一个数据点之间间隔的像素值,而一旦确定坐标系坐标原点的绘制位置我们就可以通过数据点的x坐标计算出其在控件的客户区坐标系中的横向位置的像素值了。nOriginX = rectCoord.left-int(m_dbStartX*nCoordWith/nRangX)一句就是计算原点在控件客户区内的横向位置,而
nOffsetX = int(nCoordWith*(i+1)/nRangX);则是根据上诉原理计算数据点在横向上与坐标原点的偏移。有一点需要注意的是,屏幕坐标系和客户区坐标系纵向是向下为正,而我们绘制的坐标系y轴是向上的,因此在做计算时要注意相关符号。结合代码应该能够更好的理解坐标映射的关系,以下给出绘制坐标系的代码供大家参考
- void CGraphView::DrawCoordinate(CDC* pDC, CRect rectCoord)
- {
- CString strCoord;
- CRect rectTemp;
- int i, nSection, nOffset;
- CSize szText, szUnit;
- double dbTemp, dbTempStartY, dbTempStartX,
- dbRangX = m_dbEndX-m_dbStartX,
- dbRangY = m_dbEndY-m_dbStartY;
- dbTempStartX = m_dbStartX;
- dbTempStartY = m_dbStartY;
- pDC->FillSolidRect(rectCoord, m_clrCoordBkg);
- nOffset = 2;
- for(i=0; i<=m_nDivisionX; i++)
- {
- dbTemp = dbTempStartX+dbRangX*i/m_nDivisionX;
- strCoord.Format(_T("%g"), dbTemp);
- nSection = rectCoord.Width()*i/m_nDivisionX;
- szText = pDC->GetTextExtent(strCoord, strCoord.GetLength());
- rectTemp.SetRect(rectCoord.left+nSection-szText.cx/2, rectCoord.bottom+nOffset, rectCoord.left+nSection+szText.cx/2, rectCoord.bottom+szText.cy+nOffset);
- pDC->MoveTo(rectCoord.left+nSection, rectCoord.top);
- pDC->LineTo(rectCoord.left+nSection, rectCoord.bottom);
- pDC->DrawText(strCoord, strCoord.GetLength(), rectTemp, DT_CENTER);
- }
- nOffset = 2;
- for(i=0; i<=m_nDivisionY; i++)
- {
- dbTemp = dbTempStartY+dbRangY*i/m_nDivisionY;
- strCoord.Format(_T("%g"), dbTemp);
- nSection = rectCoord.Height()*i/m_nDivisionY;
- szText = pDC->GetTextExtent(strCoord, strCoord.GetLength());
- rectTemp.SetRect(rectCoord.left-szText.cx-nOffset, rectCoord.bottom-nSection-szText.cy*2/3, rectCoord.left-nOffset, rectCoord.bottom-nSection+szText.cy/3);
- pDC->MoveTo(rectCoord.left, rectCoord.top+nSection);
- pDC->LineTo(rectCoord.right+1, rectCoord.top+nSection);
- pDC->DrawText(strCoord, strCoord.GetLength(), rectTemp, DT_RIGHT);
- }
- nOffset = 4;
- rectTemp.SetRect(rectCoord.left+nOffset, rectCoord.top+1, rectCoord.right, rectCoord.bottom-nOffset);
- pDC->FillSolidRect(rectTemp, m_clrCoordBkg);
- }
应该说这个控件的实现并不复杂,其基本原理也比较简单。当然在这个基础之上我们还可以丰富其功能,在实际项目应用中我也确实做了很多包括网格显示、极值寻找、自动幅值等功能。欢迎大家使用这个控件,如果发现什么问题或增添了什么功能也希望大家能分享出来,让我们共同提高。