当我们需要在用户区显示一些图形时,先把图形在客户区画上,虽然已经画好但此时我们还无法看到,还要通过 程序主动地刷新用户区,强制Windows发送一条WM_PAINT消息,这将引发视类OnDraw函数简单地将所有的图形对象重画,这样才完成了图形的 显示工作,但在刷新的同时会引起较明显的闪烁尤其是当画面面积较大、图像元素过多时尤为明显甚至达到无法正常工作的地步。因此,我们需要做相应的处理。本 文介绍了采用先在内存中绘制图形,然后再把绘好的图形以位图方式从内存拷贝到窗口客户的消除刷屏闪烁的一种方法。
WM_PAINT消息和无效区
·在用户移动窗口或显示窗口时,窗口中先前被隐藏的区域重新可见。
·用户改变窗口的大小。
·滚动窗口用户区。
·程序调用InvalidateRect或InvalidateRgn函数显式地发送一条WM_PAINT消息。
当上面情况之一发生时,就要求应用程序一定刷新其用户区的一部分或全部,Windows会向窗口函数发送一条WM_PAINT消息。另外,当 Windows删除覆盖窗口部分区域的对话框或消息框时和菜单下拉出来又被释放时窗口用户区被临时覆盖,系统会试图保存显示区域,但是不一定能成功,可能 向窗口函数发送一条WM_PAINT消息,要求应用程序刷新其用户区。需要说明的是:光标或图符穿过窗口用户区时,也可能覆盖显示内容,但这种情况下,系 统一定能保留并恢复被覆盖的区域,所以此时并不会发送WM_PAINT消息来要求应用程序去刷新其显示区。在Windows 应用程序的窗口函数中,对WM_PAINT消息的处理就是刷新其用户区,这是一种固定的程序结构。
为提高刷新效率,我们可以只刷新用户区的一小部分,其余没有发生变化的我们可以不予刷新,窗口函数可以通过调用函数InvalidateRect显式 地使用户区内的一个矩形无效。而且只有当窗口客户区的某一部分失效时,其窗口函数才会收到WM_PAINT消息。
刷屏闪烁的产生原因与解决方法
当客户区有所改动,而又要将改动显示出来,就必然要强制Windows发送一条WM_PAINT消息,从而引发OnDraw函数的重画,这样虽完成了 图形的显示,却也会引起较明显的闪烁,当画面上数据不是很多时尚不明显,当客户区有成千上万个点的时候刷新一次会引起整幅画面的剧烈跳动,尤其是对于许多 实时监控软件和矢量电子地图软件,此类软件通常在屏幕上都会动辄几千、几万个要素点,很明显单靠发送WM_PAINT 消息引发OnDraw 的重画根本满足不了实际需求。
为了解决上述问题,我们需要做一些相应的处理。首先要先检取无效区,然后创建一个与原设备环境句柄pDC相兼容的内存设备环境,之后就可以采用在内存 中绘制图形并把绘好的图形以位图方式从内存拷贝到窗口客户的方法来消除刷屏时引起的闪烁。这还需要创建一个与原设备环境句柄pDC相兼容的、大小为整个客 户区的位图。然后再使新的设备环境dc与pDC具有同样的映射关系,将位图选入内存环境。再使dc的整个客户区都成无效区,再“与上”所检取的无效区,使 内存环境与pDC检取的无效区相等。之后便可以进行绘图工作了,绘图完毕之后应当释放所获取的设备环境句柄pDC。否则会造成系统资源的浪费。
程序示例
本示例程序通过打开任意存档文件,将其ASCII码码值当作要显示的数据,并通过一图画控件将其数据以图形的形式依次显示出来。本程序要处理的数据量较大,如不采用本文所述方法将会有很明显的闪烁。
首先新建一基于CFormView的单文档应用程序WaveShower并在Form上添加一"picture"控件,设置其ID为 IDC_SCREEN、Type为Rectangle、Color为Black。在"Extended Styles"属性页里选中Modal Frame检查框。继续添加一菜单“打开数据文件”,并生成其响应函数OnOpenData()。同时在视类中添加如下成员变量:
int m_BufLen; //数据长度 unsigned char* buffer; //数据缓存 int m_dx; //数据偏移量 int m_DY; //数据显示区的幅度 CPoint* value; //将要显示的数值 int m_DX; //数据显示区的宽度 int m_Y0; //数据显示区参照点位置 CRect rect; //数据显示区矩形 |
然后在视类中添加函数GetScreenRect()用以获取数据显示区的大小及其他参数;添加函数CleanScreen()完成清除数据显示区的功能;添加函数DrawPoint()以便在数据显示区画点:
void CWaveShowerView::GetScreenRect() { CWnd* pStatic = GetDlgItem(IDC_SCREEN); pStatic->GetWindowRect(&rect); ScreenToClient(&rect); rect.top+=4; rect.left+=4; rect.bottom-=4; rect.right-=4; m_Y0=(rect.bottom-rect.top)/2+rect.top; m_DX=rect.Width(); m_DY=rect.Height()/2; value=new CPoint[m_DX]; } void CWaveShowerView::CleanScreen() { CDC* pDC=GetDC(); CPen pen1(PS_SOLID,1,RGB(0,0,0)); CPen* oldPen1=pDC->SelectObject(&pen1); for(int i=rect.top;i<rect.bottom;i++) { pDC->MoveTo(rect.left,i); pDC->LineTo(rect.right,i); } pDC->SelectObject(&oldPen1); CPen pen2(PS_SOLID,1,RGB(0,0,255)); CPen* oldPen2=pDC->SelectObject(&pen2); pDC->MoveTo(rect.left,m_Y0); pDC->LineTo(rect.right,m_Y0); pDC->SelectObject(&oldPen2); ReleaseDC(pDC); } void CWaveShowerView::DrawPoint(CPoint pt, COLORREF color) { CDC* pDC=GetDC(); pDC->SetPixel(rect.left+pt.x,m_Y0-pt.y,color); ReleaseDC(pDC); } |
接下来,在视类的OnInitialUpdate()初始化函数中添加代码以进行数据显示的各项前期准备工作,并在“打开数据文件”菜单的响应函数中添加代码以读取文件的内码。
void CWaveShowerView::OnInitialUpdate() { CFormView::OnInitialUpdate(); GetParentFrame()->RecalcLayout(); ResizeParentToFit(); GetScreenRect(); for(int i=0;i<m_DX;i++) value[i].x=value[i].y=0; SetTimer(0,10,NULL); } void CWaveShowerView::OnOpenData() { CString FileName=""; CFile file; CFileDialog dlg(TRUE,"*","*.*", OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT,"所有文件(*.*)|*.*||",NULL); if(dlg.DoModal()==IDOK) { KillTimer(1); FileName=dlg.GetPathName(); file.Open(FileName,CFile::modeReadWrite); m_BufLen=file.GetLength(); buffer= new unsigned char[m_BufLen+m_DX+10]; file.Read(buffer,m_BufLen); file.Close(); SetTimer(1,10,NULL); } } |
下面将要添加的定时器响应函数正是本文的重点,为方便对比起见,笔者写了两个OnTimer响应函数,前一个是采用常 规的普通方法描点的,运行起来可以很明显地看到画面的闪烁跳动。而后一种则是采用本文所述方法采用的内存画图的方法,运行后几乎画面无闪烁。下面便是两段 对比代码的原码部分:
//代码一:有闪烁的代码 void CWaveShowerView::OnTimer(UINT nIDEvent) { if(nIDEvent==0) { CleanScreen(); for(int i=0;i<m_DX;i++) DrawPoint(value[i],RGB(0,255,0)); } if(nIDEvent==1) { m_dx+=2; for(int i=0;i<m_DX;i++) { value[i].x=i; if(m_dx+i<0) buffer[m_dx+i]=128; if(m_dx+i<-m_DX) m_dx-=2; if(m_dx+i>m_BufLen) buffer[m_dx+i]=128; if(m_dx+i>m_BufLen+m_DX) m_dx-=2; value[i].y=m_DY*(buffer[m_dx+i]-128)/256; } } CFormView::OnTimer(nIDEvent); } //代码二:无闪烁的代码 void CWaveShowerView::OnTimer(UINT nIDEvent) { if(nIDEvent==0) { CDC* pDC=GetDC(); CDC dc; CBitmap bitmap; CBitmap* pOldBitmap; CRect client; pDC->GetClipBox(client); //检取无效区 //创建一个与pDC兼容的内存设备环境 if(dc.CreateCompatibleDC(pDC)) { //创建一与pDC兼容的位图,大小为整个客户区 if(bitmap.CreateCompatibleBitmap(pDC,rect.Width(), rect.Height())) { //使dc与pDC具有同样的映射关系 OnPrepareDC(&dc,NULL); //将位图选入内存环境 pOldBitmap=dc.SelectObject(&bitmap); //使dc的整个客户区都成无效区 dc.SelectClipRgn(NULL); //再“与上”检取的无效区,使内存环境与 //pDC检取的无效区相等 dc.IntersectClipRect(client); } } CleanScreen(); for(int i=0;i<m_DX;i++) DrawPoint(value[i],RGB(0,255,0)); dc.SelectObject(pOldBitmap); ReleaseDC(pDC); } if(nIDEvent==1) { m_dx+=2; for(int i=0;i<m_DX;i++) { value[i].x=i; if(m_dx+i<0) buffer[m_dx+i]=128; if(m_dx+i<-m_DX) m_dx-=2; if(m_dx+i>m_BufLen) buffer[m_dx+i]=128; if(m_dx+i>m_BufLen+m_DX) m_dx-=2; value[i].y=m_DY*(buffer[m_dx+i]-128)/256; } } CFormView::OnTimer(nIDEvent); } |
虽然通过上述几步可以实现所有的功能,但为了防止内存泄露和养成良好的编程习惯,我们还须做些工作,在视类的构造函数中释放我们曾经申请过的内存以及定时器:
CWaveShowerView::~CWaveShowerView() { delete[] value; KillTimer(0); KillTimer(1); } |
小结
编译运行此程序,通过菜单选取需要显示的文件(任意文件均可),如在定时器响应代码中采用的是第一种代码,则会看到数据显示的同时伴随着明显的闪烁而 采用后一种代码编码则会很平稳的将数据显示出来。本文介绍的这种方法适用于各种牵扯到数组数据图形显示的程序,比如监控软件、数据分析软件、测量软件等 等,具有广泛的应用前景。本文所述程度代码在Windows 2000 Professional + SP4下由Microsoft Visual C++ 6.0编译通过。