现在的位置: 首页 > 综合 > 正文

VC++大数据量绘图时无闪烁刷屏技术实现

2013年01月14日 ⁄ 综合 ⁄ 共 9078字 ⁄ 字号 评论关闭

http://hantayi.blog.51cto.com/1100843/383578

引言

  当我们需要在用户区显示一些图形时,先把图形在客户区画上,虽然已经画好但此时我们还无法看到,还要通过 程序主动地刷新用户区,强制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编译通过。

 

vc双缓冲:VC++双缓冲实现方法 (简单的较好的)

http://hantayi.blog.51cto.com/1100843/383579

在图形图象处理编程过程中,双缓冲是一种基本的技术。我们知道,如果窗体在响应WM_PAINT消息
的时候要进行复杂的图形处理,那么窗体在重绘时由于过频的刷新而引起闪烁现象。解决这一问题的有效方法
就是双缓冲技术。 

因为窗体在刷新时,总要有一个擦除原来图象的过程OnEraseBkgnd,它利用背景色填充窗体绘图区,然
后在调用新的绘图代码进行重绘,这样一擦一写造成了图象颜色的反差。当WM_PAINT的响应很频繁的时候
,这种反差也就越发明显。于是我们就看到了闪烁现象。 

我们会很自然的想到,避免背景色的填充是最直接的办法。但是那样的话,窗体上会变的一团糟。因为每次绘
制图象的时候都没有将原来的图象清除,造成了图象的残留,于是窗体重绘时,画面往往会变的乱七八糟。所
以单纯的禁止背景重绘是不够的。我们还要进行重新绘图,但要求速度很快,于是我们想到了使用BitBlt函数。
它可以支持图形块的复制,速度很快。我们可以先在内存中作图,然后用此函数将做好的图复制到前台,同时
禁止背景刷新,这样就消除了闪烁。以上也就是双缓冲绘图的基本的思路。 

一、普通方法: 

先按普通做图的方法进行编程。即在视类的OnDraw函数中添加绘图代码。在此我们绘制若干同心圆,代
码如下: 

CBCDoc* pDoc = GetDocument(); 

ASSERT_VALID(pDoc); 

CPoint ptCenter; 

CRect rect,ellipseRect; 

GetClientRect(&rect); 

ptCenter = rect.CenterPoint(); 

for(int i=20;i>0;i--) 

ellipseRect.SetRect(ptCenter,ptCenter); 

ellipseRect.InflateRect(i*10,i*10); 

pDC->Ellipse(ellipseRect); 

编译运行程序,尝试改变窗口大小,可以发现闪烁现象。 

二、双缓冲方法: 

在双缓冲方法中,首先要做的是屏蔽背景刷新。背景刷新其实是在响应WM_ERASEBKGND消息。我们在
视类中添加对这个消息的响应,可以看到缺省的代码如下: 

BOOL CMYView::OnEraseBkgnd(CDC* pDC) 

return CView::OnEraseBkgnd(pDC); 

是调用父类的OnEraseBkgnd函数,我们屏蔽此调用,只须直接return TRUE;即可。 

下面是内存缓冲作图的步骤。 

CPoint ptCenter; 

CRect rect,ellipseRect;

GetClientRect(&rect); 

ptCenter = rect.CenterPoint(); 

CDC dcMem; //用于缓冲作图的内存DC 

CBitmap bmp; //内存中承载临时图象的位图 

dcMem.CreateCompatibleDC(pDC); //依附窗口DC创建兼容内存DC 

bmp.CreateCompatibleBitmap(pDC,rect.Width(),rect.Height());//创建兼容位图 

dcMem.SelectObject(&bmp); //将位图选择进内存DC 

//按原来背景填充客户区,不然会是黑色 

dcMem.FillSolidRect(rect,pDC->GetBkColor()); 

for(int i=20;i>0;i--) //在内存DC上做同样的同心圆图象 

ellipseRect.SetRect(ptCenter,ptCenter); 

ellipseRect.InflateRect(i*10,i*10); 

dcMem.Ellipse(ellipseRect); 

pDC->BitBlt(0,0,rect.Width(),rect.Height(), 

& dcMem,0,0,SRCCOPY);//将内存DC上的图象拷贝到前台 

dcMem.DeleteDC(); //删除DC

bm.DeleteObject(); //删除位图 

由于复杂的画图操作转入后台,我们看到的是速度很快的复制操作,自然也就消除了闪烁现象。 

注意:bmp.CreateCompatibleBitmap(pDC,rect.Width(),rect.Height()); 

这里面CreateCompatibleBitmap第一个参数不能用dcMem,这样的话创建的是黑白位图。如果你要创建彩色
位图,需要用pDC,它用来创建了内存DC. 详细请见下面的MSDN: 

When a memory device context is created, it initially has a 1-by-1 monochrome bitmap selected into
it. If this memory device context is used in CreateCompatibleBitmap, the bitmap that is created is a
monochrome bitmap. To create a color bitmap, use the hDC that was used to create the memory
device context, as shown in the following code: 

HDC memDC = CreateCompatibleDC ( hDC ); 

HBITMAP memBM = CreateCompatibleBitmap ( hDC, nWidth, nHeight ); 

SelectObject ( memDC, memBM );            2008-11-6 16:45:41 

 

抱歉!评论已关闭.