波形曲线控件在电力、财经、工业控制等众多领域有着广泛的应用。利用波形曲线控件做为数据的载体较表格控件的显示更直观,易于从大量数据中发现数据内部所存在的规律,深受广大朋友的欢迎。
MsChart是微软制作的功能强大的图表工具,用它可以很方便的建立各种图表,制作各种3维2维的直方图,折线图,但其本质上是基于COM技术应用而封装的ActiveX控件,其要求的技术门槛较高,为了能够灵活使用这个控件,必须熟悉这个组件的接口,MsChart也未必能够满足各个领域的个性化需求,若在COM的层面上想对MSChart控件进行修改,是一件非常困难的事情,用MSChart进行波形曲线的显示未必是一个很好的选择。若能够用MFC自己来封装一个这样的波形曲线控件,那这样的控件就很能够满足个性化的定制要求,便于维护和升级,而实践过程也证实了这一点,用MFC开发一个波形曲线显示控件来满足个性化的定制要求也并非是一件很难的事情。
经过对比,本人觉得采用静态文本控件(CStatic)作为的波形曲线控件的显示载体较为方便、简单。故只需要继承CStatic,派生出一个波形曲线显示控件即可(暂定为CGraghCtrl)。这个里面涉及两个难点,一个是静态文本的图形自绘,一个是坐标的转化。
一、静态文本的自绘
CStatic控件客户区的自绘是在WM_PAINT里面实现的。由于绘制图形可能会占用较多的时间,直接在控件表面绘制,图形的过渡就会很不自然,影响绘图质量,故在绘制图形的时候,应该尽量采用双缓冲机制。双缓冲机制就好如你要在你的墙壁上做画,由于直接在墙壁上做画,可能会对你的墙壁造成危害,而这种危害几乎都是不可逆的,于是对于画工来说,他们常常会选择一个与原先墙壁兼容的材质,并在那个材质上作完画后,直接将他附到墙壁上就可以了,至于作画的过程,我们是不会关心的。控件的双缓冲绘制代码如下:
- /
- // 消息响应函数
- void CGraghCtrl::OnPaint()
- {
- CPaintDC dc(this);
- CDC MemDC; //首先定义一个显示设备对象
- CBitmap MemBitmap;//定义一个位图对象
- MemDC.CreateCompatibleDC(&dc);//随后建立与屏幕显示兼容的内存显示设备
- MemBitmap.CreateCompatibleBitmap(&dc,m_rcClient.Width(),m_rcClient.Height());
- CBitmap *pOldBit=MemDC.SelectObject(&MemBitmap);
- DrawMemDC(&MemDC);// 这个函数完成图形的绘制
- dc.BitBlt(0, 0, m_rcClient.Width(), m_rcClient.Height(), &MemDC, 0, 0, SRCCOPY);//将内存中的图拷贝到屏幕上进行显示
- MemDC.SelectObject(pOldBit);
- MemBitmap.DeleteObject();
- MemDC.DeleteDC();
- }
其中,DrawMemDC(CDC* pDC)实现了对图形的绘制。对于我们的波形曲线来说,绘制的元素总体包含以下几种:
1、背景色。void DrawBgcolor();
2、曲线标题文字,用来描述曲线的一些基本信息。如泉州官桥变1#主变油色谱曲线走势图。void DrawTitle(CDC* pDC);
3、坐标系,其中原点、正方向和单位长度构成其三要素,这个在绘图里面是相当关键的。void DrawCoordinate(CDC* pDC);
4、波形曲线的绘制。void DrawGragh(CDC* pDC);
5、曲线的切换控制标签。如泉州官桥变1#主变油色谱曲线,在这个走势图上应可以同时查看所有油色谱的曲线情况(H2、CO、C02、CH4、C2H4、C2H6、C2H2),通过切换标签,可以选择自己需要查看的几组气体的曲线情况。DrawSwitch(CDC* pDC);
DrawMemDC的实现代码如下:
- /
- // 绘制各种图形元素
- void CGraghCtrl::DrawMemDC(CDC* pDC)
- {
- DrawBgcolor(pDC);
- DrawTitle(pDC);
- DrawCoordinate(pDC);
- DrawGragh(pDC);
- DrawSwitch(pDC);
- }
- // 绘制背景
- void CGraghCtrl::DrawBgcolor(CDC* pDC)
- {
- int nBkMode = pDC->SetBkMode(TRANSPARENT);
- pDC->FillSolidRect(m_rcClient, m_bkColor);
- pDC->SetBkMode(nBkMode);
- }
- // 绘制标题栏
- void CGraghCtrl::DrawTitle(CDC* pDC)
- {
- int nBkMode = pDC->SetBkMode(TRANSPARENT);
- CFont* pOldFont = pDC->SelectObject(&m_fontTitle);
- COLORREF oldClr = pDC->SetTextColor(0X0000FF);
- CRect rcTitle(m_rcClient.left,m_rcClient.top,m_rcClient.right,30);
- pDC->DrawText(m_layout.strTitle, 24, rcTitle, DT_CENTER);
- pDC->SetTextColor(oldClr);
- pDC->SelectObject(pOldFont);
- pDC->SetBkMode(nBkMode);
- }
- // 绘制坐标系
- void CGraghCtrl::DrawCoordinate(CDC* pDC)
- {
- int nBkMode = pDC->SetBkMode(TRANSPARENT);
- CPen* pOldPen = pDC->SelectObject(&pen2);
- CPen pen(PS_SOLID, 1, m_bkColor);
- if( ColyToScreen(0)<m_layout.page.nTopMargin || ColyToScreen(0)>m_rcClient.bottom-m_layout.page.nBottomMargin)
- {
- pDC->SelectObject(&pen);
- }
- else
- {
- pDC->SelectObject(&pen2);
- }
- pDC->MoveTo(m_layout.page.nLeftMargin,ColyToScreen(0));
- pDC->LineTo(m_rcClient.right-m_layout.page.nRightMargin,ColyToScreen(0));
- pDC->MoveTo(m_rcClient.right-m_layout.page.nRightMargin, ColyToScreen(0));
- pDC->LineTo(m_rcClient.right-m_layout.page.nRightMargin-m_layout.coordin.arrow.nArrowHeight, ColyToScreen(0)-m_layout.coordin.arrow.nArrowWidth);
- pDC->MoveTo(m_rcClient.right-m_layout.page.nRightMargin, ColyToScreen(0));
- pDC->LineTo(m_rcClient.right-m_layout.page.nRightMargin-m_layout.coordin.arrow.nArrowHeight, ColyToScreen(0)+m_layout.coordin.arrow.nArrowWidth);
- if(ColxToScreen(0)>m_rcClient.right-m_layout.page.nRightMargin || ColxToScreen(0)<m_layout.page.nLeftMargin)
- {
- pDC->SelectObject(&pen);
- }
- else
- {
- pDC->SelectObject(&pen2);
- }
- pDC->MoveTo(ColxToScreen(0), m_rcClient.bottom-m_layout.page.nBottomMargin);
- pDC->LineTo(ColxToScreen(0), m_layout.page.nTopMargin);
- pDC->MoveTo(ColxToScreen(0), m_layout.page.nTopMargin);
- pDC->LineTo(ColxToScreen(0)-m_layout.coordin.arrow.nArrowWidth, m_layout.page.nTopMargin+m_layout.coordin.arrow.nArrowHeight);
- pDC->MoveTo(ColxToScreen(0), m_layout.page.nTopMargin);
- pDC->LineTo(ColxToScreen(0)+m_layout.coordin.arrow.nArrowWidth, m_layout.page.nTopMargin+m_layout.coordin.arrow.nArrowHeight);
- //画坐标的刻度,用虚线画,横坐标,纵坐标
- pOldPen = pDC->SelectObject(&penDot);
- CString strXdel;
- CString strYdel;
- int nxSrv = m_layout.coordin.xCol.nxSrv;
- int nxEnd = ScreenxToCol(m_rcClient.right-m_layout.page.nRightMargin);
- for(int i=nxSrv; i<nxEnd; i+=m_layout.coordin.xCol.fxDel)
- {
- if(i%30==0)
- {
- pDC->MoveTo(ColxToScreen(i),m_rcClient.bottom-m_layout.page.nBottomMargin);
- pDC->LineTo(ColxToScreen(i),m_layout.page.nTopMargin);
- pDC->SetTextColor(RGB(0,98,255));
- strXdel.Format("%d",i);
- pDC->TextOut(ColxToScreen(i),m_rcClient.bottom-m_layout.page.nBottomMargin+5,strXdel);
- }
- }
- int nySrv = m_layout.coordin.yCol.nySrv;
- int nyEnd = ScreenyToCol(m_layout.page.nTopMargin);
- for(int i=nySrv; i<nyEnd; i+=m_layout.coordin.yCol.fyDel)
- {
- if(i%30==0)
- {
- pDC->MoveTo(m_layout.page.nLeftMargin,ColyToScreen(i));
- pDC->LineTo(m_rcClient.right-m_layout.page.nRightMargin,ColyToScreen(i));
- pDC->SetTextColor(RGB(0,98,255));
- strYdel.Format("%d", i);
- pDC->TextOut(m_layout.page.nLeftMargin-30, ColyToScreen(i), strYdel);
- }
- }
- pDC->SetBkMode(nBkMode);
- }
- // 绘制曲线
- void CGraghCtrl::DrawGragh(CDC* pDC)
- {
- int nCount = m_graghArray.GetSize();
- for(int i=0; i<nCount; i++)
- {
- GraghStruct* pGs = (GraghStruct*)m_graghArray.GetAt(i);
- DrawGragh(pDC, pGs);
- }
- }
DrawGragh实现了对所有参数曲线的绘制,它又调用了DrawGragh(pDC, pGs);来绘制某单幅曲线,并通过链表来实现对这些曲线的管理和维护,当然对于每种曲线定义了统一的曲线结构,定义如下:
- // 曲线结构
- struct GraghStruct
- {
- CString strTip; // 曲线的描述
- COLORREF clrGragh; // 曲线、控制标签的颜色
- CRect rcTip; // 控制标签的位置
- BOOL bFlag; // 曲线的显示状态
- float fData[50000]; // 曲线的数据,这边暂时支持50000个数据点。
- int nDataCount; // 曲线的实际数据点
- };
绘制曲线的时候还涉及到一个关键的问题,这个也是绘制曲线中的难点问题,即设备坐标与逻辑坐标之间的转换坐标转换。
设备坐标:一个实际物理屏幕是由像素组成的如平常所说的640×480,1024 ×768指的就是显示器的实际宽度和高度的像素数目。 C++绘图有好几种模式,默认情况下是MM_TEXT,在此模式下绘图就是设备坐标,因为它的单位是像素,在这个模式下,CDC的绘图都是以象素为单位。
逻辑坐标:本系统数据所采用的坐标。而绘制的时候都是以象素为单位,需要进行两者的坐标转换,转换函数如下:
- 以下为坐标转化函数/
- // 实际坐标转化为屏幕坐标
- int CGraghCtrl::ColxToScreen(float fColx)
- {
- return (int)m_layout.coordin.xCol.fxDel*(fColx-m_layout.coordin.xCol.nxSrv)+ m_layout.page.nLeftMargin;
- }
- int CGraghCtrl::ColyToScreen(float fColy)
- {
- return (int)m_rcClient.bottom-m_layout.page.nBottomMargin-m_layout.coordin.yCol.fyDel*(fColy-m_layout.coordin.yCol.nySrv);
- }
- // 屏幕坐标转换为实际坐标
- float CGraghCtrl::ScreenxToCol(int nSrcx)
- {
- return (nSrcx-m_layout.page.nLeftMargin)/(m_layout.coordin.yCol.fyDel*1.0)+m_layout.coordin.xCol.nxSrv;
- }
- float CGraghCtrl::ScreenyToCol(int nSrcy)
- {
- return (m_rcClient.bottom-m_layout.page.nBottomMargin-nSrcy)/(m_layout.coordin.yCol.fyDel*1.0)+m_layout.coordin.yCol.nySrv;
- }
- /
有了上述基础,就基本上可以实现曲线的自绘,轻松摆脱了MSCHART的束缚。曲线除了上面所描述的基本功能外,曲线还应该支持放大、缩小、拖动等功能。这些功能无非是改变比例因子和坐标原点来实现。
- // 实现曲线的拖动
- void CGraghCtrl::OnMouseMove(UINT nFlags, CPoint point)
- {
- m_rcGragh.left = m_layout.page.nLeftMargin;
- m_rcGragh.right = m_rcClient.right-m_layout.page.nRightMargin;
- m_rcGragh.top = m_layout.page.nTopMargin;
- m_rcGragh.bottom = m_rcClient.bottom - m_layout.page.nBottomMargin;
- if(m_rcGragh.PtInRect(point))
- {
- if(m_bDown)
- {
- m_layout.coordin.xCol.nxSrv += point.x-m_ptOrg.x;
- m_layout.coordin.yCol.nySrv -= point.y-m_ptOrg.y;
- m_ptOrg = point;
- Invalidate();
- }
- // 移动鼠标时候,出现一个提示框来实现对坐标位置的实时跟踪
- m_toolTipDlg.ShowWindow(SW_SHOW);
- m_toolTipDlg.x = ScreenxToCol(point.x);
- m_toolTipDlg.y = ScreenyToCol(point.y);
- ClientToScreen(&point);
- m_toolTipDlg.Invalidate();
- m_toolTipDlg.MoveWindow(point.x+1, point.y+1, 180, 90, false);
- }
- else
- {
- m_toolTipDlg.ShowWindow(SW_HIDE);
- }
- CStatic::OnMouseMove(nFlags, point);
- }
- // 实现曲线的切换,其中需判断鼠标是否在相应曲线标签上点击。
- void CGraghCtrl::OnLButtonDown(UINT nFlags, CPoint point)
- {
- CDC *pDC = this->GetDC();
- int nSize = m_graghArray.GetSize();
- for(int i=0; i<nSize; i++)
- {
- GraghStruct* pGs = (GraghStruct*)m_graghArray.GetAt(i);
- CPen pen(PS_SOLID, 3, 0xffffff-pGs->clrGragh);
- CPen* pOldPen = pDC->SelectObject(&pen);
- if (pGs->rcTip.PtInRect(point))
- {
- pGs->bFlag = !pGs->bFlag;
- if (!pGs->bFlag)
- {
- pDC->MoveTo(pGs->rcTip.left,pGs->rcTip.top);
- pDC->LineTo(pGs->rcTip.right,pGs->rcTip.bottom);
- pDC->MoveTo(pGs->rcTip.left,pGs->rcTip.bottom);
- pDC->LineTo(pGs->rcTip.right,pGs->rcTip.top);
- }
- else
- {
- pDC->FillSolidRect(pGs->rcTip,RGB(220,0,150));
- }
- }
- pDC->SelectObject(pOldPen);
- }
- if(m_rcGragh.PtInRect(point))
- {
- m_bDown = TRUE;
- m_ptOrg = point;
- }
- Invalidate();
- CStatic::OnLButtonDown(nFlags, point);
- }
- void CGraghCtrl::OnLButtonUp(UINT nFlags, CPoint point)
- {
- // TODO: 在此添加消息处理程序代码和/或调用默认值
- if(m_rcGragh.PtInRect(point))
- {
- m_bDown = FALSE;
- }
- CStatic::OnLButtonUp(nFlags, point);
- }
双击可以实现曲线的截图,其代码如下:
- void CGraghCtrl::OnLButtonDblClk(UINT nFlags, CPoint point)
- {
- // TODO: Add your message handler code here and/or call default
- CString filePath;
- //获取选中图像窗口的位图句柄
- CDC dc;
- HDC hdc = ::GetWindowDC(this->m_hWnd);
- dc.Attach(hdc);
- CDC memDC;
- memDC.CreateCompatibleDC(&dc);
- CBitmap bm;
- CRect r;
- this->GetWindowRect(&r);
- r.bottom = r.bottom-42;
- CSize sz(r.Width(), r.Height());
- bm.CreateCompatibleBitmap(&dc, sz.cx, sz.cy);
- CBitmap * oldbm = memDC.SelectObject(&bm);
- memDC.BitBlt(0, 0, sz.cx, sz.cy, &dc, 0, 0, SRCCOPY);
- static int i=0;
- i++;
- CString str ;
- str.Format("%d%s", i, ".bmp");
- CFileDialog m_toolTipDlg( FALSE,"bmp", str, OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT, "*.bmp||" );
- m_toolTipDlg.m_ofn.Flags|=OFN_FILEMUSTEXIST;
- m_toolTipDlg.m_ofn.lpstrTitle="保存为位图";
- if( m_toolTipDlg.DoModal() == IDCANCEL )
- return ;
- filePath = m_toolTipDlg.GetPathName();
- char *buf = (LPSTR)(LPCTSTR)filePath;
- SaveBitmapToFile((HBITMAP)bm.m_hObject ,buf);
- CStatic::OnLButtonDblClk(nFlags, point);
- }
- int CGraghCtrl::SaveBitmapToFile(HBITMAP hBitmap ,//hBitmap 为刚才的屏幕位图句柄
- LPSTR lpFileName) //lpFileName 为位图文件名
- {
- //设备描述表
- HDC hDC;
- //当前显示分辨率下每个像素所占字节数
- int iBits;
- //位图中每个像素所占字节数
- WORD wBitCount;
- //定义调色板大小, 位图中像素字节大小 ,
- //位图文件大小 , 写入文件字节数
- DWORD dwPaletteSize=0,dwBmBitsSize,dwDIBSize, dwWritten;
- //位图属性结构
- BITMAP Bitmap;
- //位图文件头结构
- BITMAPFILEHEADER bmfHdr;
- //位图信息头结构
- BITMAPINFOHEADER bi;
- //指向位图信息头结构
- LPBITMAPINFOHEADER lpbi;
- //定义文件,分配内存句柄,调色板句柄
- HANDLE fh, hDib, hPal,hOldPal=NULL;
- //计算位图文件每个像素所占字节数
- hDC = CreateDC("DISPLAY",NULL,NULL,NULL);
- iBits = GetDeviceCaps(hDC, BITSPIXEL) *GetDeviceCaps(hDC, PLANES);
- DeleteDC(hDC);
- switch(iBits)
- {
- case 1:
- wBitCount = 1;
- break;
- case 2:
- wBitCount = 4;
- break;
- case 4:
- wBitCount = 16;
- break;
- case 8:
- wBitCount = 256;
- break;
- default:
- break;
- }
- //计算调色板大小
- // if (wBitCount <= 8)
- {
- // dwPaletteSize = (1<<wBitCount) *sizeof(RGBQUAD);
- }
- //设置位图信息头结构
- GetObject(hBitmap, sizeof(BITMAP), (LPSTR)&Bitmap);
- bi.biSize = sizeof(BITMAPINFOHEADER);
- bi.biWidth = Bitmap.bmWidth;
- bi.biHeight = Bitmap.bmHeight;
- bi.biPlanes = 1;
- bi.biBitCount = 32;
- bi.biCompression = BI_RGB;
- bi.biSizeImage = 0;
- bi.biXPelsPerMeter = 0;
- bi.biYPelsPerMeter = 0;
- bi.biClrUsed = 0;
- bi.biClrImportant = 0;
- dwBmBitsSize = ((Bitmap.bmWidth *32+31)/32)*4*Bitmap.bmHeight ;
- //为位图内容分配内存
- hDib = GlobalAlloc(GHND,dwBmBitsSize+
- dwPaletteSize+sizeof(BITMAPINFOHEADER));
- lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDib);
- *lpbi = bi;
- // 处理调色板
- hPal = GetStockObject(DEFAULT_PALETTE);
- if (hPal)
- {
- hDC = ::GetDC(NULL);
- hOldPal = (HPALETTE)SelectPalette(hDC, (HPALETTE)hPal, FALSE);
- RealizePalette(hDC);
- }
- // 获取该调色板下新的像素值
- GetDIBits(hDC, hBitmap, 0, (UINT) Bitmap.bmHeight,
- (LPSTR)lpbi + sizeof(BITMAPINFOHEADER)
- +dwPaletteSize, (BITMAPINFO *)lpbi, DIB_RGB_COLORS);
- // +dwPaletteSize, (BITMAPINFOHEADER *)lpbi, DIB_RGB_COLORS);
- //恢复调色板
- if (hOldPal)
- {
- SelectPalette(hDC, (HPALETTE)hOldPal, TRUE);
- RealizePalette(hDC);
- ::ReleaseDC(NULL, hDC);
- }
- //创建位图文件
- fh = CreateFile(lpFileName, GENERIC_WRITE,
- 0, NULL, CREATE_ALWAYS,
- FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
- if (fh == INVALID_HANDLE_VALUE)
- return FALSE;
- // 设置位图文件头
- bmfHdr.bfType = 0x4D42; // "BM"
- dwDIBSize = sizeof(BITMAPFILEHEADER)
- + sizeof(BITMAPINFOHEADER)
- + dwPaletteSize + dwBmBitsSize;
- bmfHdr.bfSize = dwDIBSize;
- bmfHdr.bfReserved1 = 0;
- bmfHdr.bfReserved2 = 0;
- bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER)
- + (DWORD)sizeof(BITMAPINFOHEADER)
- + dwPaletteSize;
- // 写入位图文件头
- WriteFile(fh, (LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
- // 写入位图文件其余内容
- WriteFile(fh, (LPSTR)lpbi, dwDIBSize,&dwWritten, NULL);
- //清除
- GlobalUnlock(hDib);
- GlobalFree(hDib);
- CloseHandle(fh);
- return TRUE;
- }
通过上述原理封装后,就可以直接调用这个控件,来实现曲线的绘制了。调用方式如下:
- // .h文件中
- #include "GraghCtrl/GraghCtrl.h"
- #define IDC_GraghCtrl 1000
- GraghStruct gs;
- GraghStruct gs2;
- CGraghCtrl m_graghCtrl;
- // .cpp中
- CRect rect;
- GetClientRect(&rect);
- if(m_graghCtrl.m_hWnd == NULL)
- {
- m_graghCtrl.Create( NULL,WS_CHILD | WS_VISIBLE | SS_USERITEM | SS_NOTIFY, rect, this, IDC_GraghCtrl);
- gs.bFlag = TRUE;
- gs.clrGragh = 0xFF0000;
- gs.nDataCount = 3000;
- for(int i=0; i<gs.nDataCount; i++)
- {
- gs.fData[i] = 30*2*rand()/65536.0;
- }
- gs.strTip = "随机曲线";
- m_graghCtrl.m_graghArray.Add(&gs);
- gs2.bFlag = TRUE;
- gs2.clrGragh = 0x00FF00;
- gs2.nDataCount = 3000;
- for(int i=0; i<gs2.nDataCount; i++)
- {
- gs2.fData[i] = 200*sin(i/100.0);
- }
- gs2.strTip = "正弦曲线";
- m_graghCtrl.m_graghArray.Add(&gs2);
- }
最后,实现效果如下,