如何解决绘图过程中的闪烁
在VC中进行绘图过程处理时,如果图形刷新很快,
经常出现图形闪烁的现象。利用先在内存绘制,然后
拷贝到屏幕的办法可以消除屏幕闪烁,具体的方法是先在内存
中创建一个与设备兼容的内存设备上下文,也就是开辟一快内
存区来作为显示区域,然后在这个内存区进行绘制图形。在绘制完成后利用
BitBlt函数把内存的图形直接拷贝到屏幕上即可。
具体的代码实现为:
(1)创建内存区域
CDC* pMem=new CDC;
CBitmap* pBmp=new CBitmap;
CBitmap* pOldBmp;
CDC* pDC=GetDC();
CRect rectTemp;为绘图区域
pMem->CreateCompatibleDC(pDC);
pBmp->CreateCompatibleBitmap(pDC, rectTemp.Width(), rectTemp.Height());
pOldBmp=pMem->SelectObject(pBmp);
(2)进行图形绘制
pMem->LineTo(...); 进行绘图处理
(3)拷贝到屏幕
pDC->BitBlt(rectTemp.left,rectTemp.top,rectTemp.Width(),rectTemp.Height(),pMem,0,0,SRCCOPY);
pMem->SelectObject(pOldBmp);
pBmp->DeleteObject() ;
pMem->DeleteDC();
================================================================================================
双缓存机制解决VC++绘图时的闪烁问题
显示图形如何避免闪烁,如何提高显示效率是问得比较多的问题。
而且多数人认为MFC的绘图函数效率很低,总是想寻求其它的解决方案。
MFC的绘图效率的确不高但也不差,而且它的绘图函数使用非常简单,
只要使用方法得当,再加上一些技巧,用MFC可以得到效率很高的绘图程序。
我想就我长期(呵呵当然也只有2年多)使用MFC绘图的经验谈谈
我的一些观点。
1、显示的图形为什么会闪烁?
我们的绘图过程大多放在OnDraw或者OnPaint函数中,OnDraw在进行屏
幕显示时是由OnPaint进行调用的。当窗口由于任何原因需要重绘时,
总是先用背景色将显示区清除,然后才调用OnPaint,而背景色往往与绘图内容
反差很大,这样在短时间内背景色与显示图形的交替出现,使得显示窗口看起来
在闪。如果将背景刷设置成NULL,这样无论怎样重绘图形都不会闪了。
当然,这样做会使得窗口的显示乱成一团,因为重绘时没有背景色对原来
绘制的图形进行清除,而又叠加上了新的图形。
有的人会说,闪烁是因为绘图的速度太慢或者显示的图形太复杂造成的,
其实这样说并不对,绘图的显示速度对闪烁的影响不是根本性的。
例如在OnDraw(CDC *pDC)中这样写:
pDC->MoveTo(0,0);
pDC->LineTo(100,100);
这个绘图过程应该是非常简单、非常快了吧,但是拉动窗口变化时还是会看见
闪烁。其实从道理上讲,画图的过程越复杂越慢闪烁应该越少,因为绘图用的
时间与用背景清除屏幕所花的时间的比例越大人对闪烁的感觉会越不明显。
比如:清楚屏幕时间为1s绘图时间也是为1s,这样在10s内的连续重画中就要闪
烁5次;如果清楚屏幕时间为1s不变,而绘图时间为9s,这样10s内的连续重画
只会闪烁一次。这个也可以试验,在OnDraw(CDC *pDC)中这样写:
for(int i=0;i<100000;i++)
{
pDC->MoveTo(0,i);
pDC->LineTo(1000,i);
}
呵呵,程序有点变态,但是能说明问题。
说到这里可能又有人要说了,为什么一个简单图形看起来没有复杂图形那么
闪呢?这是因为复杂图形占的面积大,重画时造成的反差比较大,所以感觉上要
闪得厉害一些,但是闪烁频率要低。
那为什么动画的重画频率高,而看起来却不闪?这里,我就要再次强调了,
闪烁是什么?闪烁就是反差,反差越大,闪烁越厉害。因为动画的连续两个帧之间
的差异很小所以看起来不闪。如果不信,可以在动画的每一帧中间加一张纯白的帧,
不闪才怪呢。
2、如何避免闪烁
在知道图形显示闪烁的原因之后,对症下药就好办了。首先当然是去掉MFC
提供的背景绘制过程了。实现的方法很多,
* 可以在窗口形成时给窗口的注册类的背景刷付NULL
* 也可以在形成以后修改背景
static CBrush brush(RGB(255,0,0));
SetClassLong(this->m_hWnd,GCL_HBRBACKGROUND,(LONG)(HBRUSH)brush);
* 要简单也可以重载OnEraseBkgnd(CDC* pDC)直接返回TRUE
这样背景没有了,结果图形显示的确不闪了,但是显示也象前面所说的一样,
变得一团乱。怎么办?这就要用到双缓存的方法了。双缓冲就是除了在屏幕上有
图形进行显示以外,在内存中也有图形在绘制。我们可以把要显示的图形先在内存中
绘制好,然后再一次性的将内存中的图形按照一个点一个点地覆盖到屏幕上去(这个
过程非常快,因为是非常规整的内存拷贝)。这样在内存中绘图时,随便用什么反差
大的背景色进行清除都不会闪,因为看不见。当贴到屏幕上时,因为内存中最终的图形
与屏幕显示图形差别很小(如果没有运动,当然就没有差别),这样看起来就不会闪。
3、如何实现双缓冲
首先给出实现的程序,然后再解释,同样是在OnDraw(CDC *pDC)中:
CDC MemDC; //首先定义一个显示设备对象
CBitmap MemBitmap;//定义一个位图对象
//随后建立与屏幕显示兼容的内存显示设备
MemDC.CreateCompatibleDC(NULL);
//这时还不能绘图,因为没有地方画 ^_^
//下面建立一个与屏幕显示兼容的位图,至于位图的大小嘛,可以用窗口的大小
MemBitmap.CreateCompatibleBitmap(pDC,nWidth,nHeight);
//将位图选入到内存显示设备中
//只有选入了位图的内存显示设备才有地方绘图,画到指定的位图上
CBitmap *pOldBit=MemDC.SelectObject(&MemBitmap);
//先用背景色将位图清除干净,这里我用的是白色作为背景
//你也可以用自己应该用的颜色
MemDC.FillSolidRect(0,0,nWidth,nHeight,RGB(255,255,255));
//绘图
MemDC.MoveTo(……);
MemDC.LineTo(……);
//将内存中的图拷贝到屏幕上进行显示
pDC->BitBlt(0,0,nWidth,nHeight,&MemDC,0,0,SRCCOPY);
//绘图完成后的清理
MemBitmap.DeleteObject();
MemDC.DeleteDC();
上面的注释应该很详尽了,废话就不多说了。
4、如何提高绘图的效率
我主要做的是电力系统的网络图形的CAD软件,在一个窗口中往往要显示成千上万个电力元件,而每个元件又是由点、线、圆等基本图形构成。如果真要在一次重绘过程重画这么多元件,可想而知这个过程是非常漫长的。如果加上了图形的浏览功能,鼠标拖动图形滚动时需要进行大量的重绘,速度会慢得让用户将无法忍受。怎么办?只有再研究研究MFC 的绘图过程了。
实际上,在OnDraw(CDC *pDC)中绘制的图并不是所有都显示了的,例如:你
在OnDraw中画了两个矩形,在一次重绘中虽然两个矩形的绘制函数都有执行,但是很有可能只有一个显示了,这是因为MFC本身为了提高重绘的效率设置了裁剪区。裁剪区的作用就是:只有在这个区内的绘图过程才会真正有效,在区外的是无效的,即使在区外执行了绘图函数也是不会显示的。因为多数情况下窗口重绘的产生大多是因为窗口部分被遮挡或者窗口有滚动发生,改变的区域并不是整个图形而只有一小部分,这一部分需要改变的就是pDC中的裁剪区了。因为显示(往内存或者显存都叫显示)比绘图过程的计算要费时得多,有了裁剪区后显示的就只是应该显示的部分,大大提高了显示效率。但是这个裁剪区是MFC设置的,它已经为我们提高了显示效率,在进行复杂图形的绘制时如何进一步提高效率呢?那就只有去掉在裁剪区外的绘图过程了。可以先用 pDC->GetClipBox()得到裁剪区,然后在绘图时判断你的图形是否在这个区内,如果在就画,不在就不画。
如果你的绘图过程不复杂,这样做可能对你的绘图效率不会有提高。
=================================================================================================
VC++大数据量绘图时无闪烁刷屏技术实现
引言
当我们需要在用户区显示一些图形时,先把图形在客户区画上,虽然已经画好但此时我们还无法看到,还要通过程序主动地刷新用户区,强制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编译通过。
=============================================================================================
用双缓存技术,和重载OnEraseBKgnd(),
怎么重载OnEraseBkgnd()函数??- -
选择View->ClassWizard->classinfo选项
卡:message filter下拉框: 选择window,然后再选择
message maps选项卡,在messages下拉框应该可以找到
wm_erasebkgnd.双击添加.
======================================================================================================
如何实现双缓冲
首先给出实现的程序,然后再解释,同样是在OnDraw(CDC *pDC)中:
CDC MemDC; //首先定义一个显示设备对象
CBitmap MemBitmap;//定义一个位图对象
//随后建立与屏幕显示兼容的内存显示设备
MemDC.CreateCompatibleDC(NULL);
//这时还不能绘图,因为没有地方画 ^_^
//下面建立一个与屏幕显示兼容的位图,至于位图的大小嘛,可以用窗口的大小,也可以自己定义(如:有滚动条时就要大于当前窗口的大小,在BitBlt时决定拷贝内存的哪部分到屏幕上)
MemBitmap.CreateCompatibleBitmap(pDC,nWidth,nHeight);
//将位图选入到内存显示设备中
//只有选入了位图的内存显示设备才有地方绘图,画到指定的位图上
CBitmap *pOldBit=MemDC.SelectObject(&MemBitmap);
//先用背景色将位图清除干净,这里我用的是白色作为背景
//你也可以用自己应该用的颜色
MemDC.FillSolidRect(0,0,nWidth,nHeight,RGB(255,255,255));
//绘图
MemDC.MoveTo(……);
MemDC.LineTo(……);
//将内存中的图拷贝到屏幕上进行显示
pDC->BitBlt(0,0,nWidth,nHeight,&MemDC,0,0,SRCCOPY);
//绘图完成后的清理
MemBitmap.DeleteObject();
MemDC.DeleteDC();
==================================================================================================
用vc 做程序,如何画图是一个大家都很关心,但是却感到很难以理解的问题,因为在mfc的封装之下,没有现成的画图函数供你直接调用,像vb等等里面直接来个 point之类的,常常让人感到无从下手。这两天帮人解决了一个用内存缓冲画图的问题,顺便也就谈谈这些东西,也算是总结。
我先来解释一下在mfc里面很关键的设备环境描述符,也就是所谓的DC(device context)。
还是从历史来看吧,dos时代,我们如果要绘图,必须通过一系列系统函数来启动图形环境(用过turbo pascal或者turbo c的人该还有印象吧),这之间对各种硬件的初始化参数都不相同,非常的烦人,常常还要查阅硬件手册,那时的程序智能针对最流行的硬件来编写,对不流行的就没有办法了。windows操作系统为了屏蔽不同的硬件环境,让编程时候不考虑具体的硬件差别,采取了一系列办法,设备环境描述符就是这样产生的。简单的说,设备描述符抽象了不同的硬件环境为标准环境,用户编写时使用的是这个虚拟的标准环境,而不是真实的硬件,与真实硬件打交道的工作一般交给了系统和驱动程序完成(这同样解释了为什么我们需要经常更新驱动程序的问题)。使用在windows图形系统(gdi,而不包括direct x)上面,就体现在一系列的图形DC上面,我们如果要在gdi上面绘图,就必须先得到图形DC的句柄(handle),然后指定句柄的基础上进行图形操作。
再来回忆一下,我们怎么在sdk的环境下面绘图呢,我想这个大家都不太清楚吧,但是确实很基础。在windows的sdk环境下面,我们用传统的c编写程序,在需要的绘图地方(比如响应WM_PAINT消息的分支)这样做:
hdc = GetDC( hwnd );
oldGdiObject = SelectObject( hdc,newGdiObject );
...绘图操作...
SelectObject( hdc,oldGdiObject );
DeleteObject( newGdiObject );
ReleaseDC( hdc);
或者是这样
BeginPaint( hwnd,&ps );//PAINTSTRUCT ps -- ps is a paint struct
...绘图操作...
EndPaint( hwnd )
这就是大概的过程,我们看到了hdc(图形DC句柄)的应用,在绘图的部分,每一个绘图函数基本上也要用到这个句柄,最后我们还必须释放它,否则将严重影响性能。每次我们都必须调用GetDC这个api函数得到(不能用全局变量保存结果重复使用,我在后面解释)。这些是最最基本的windows图形操作的方式,相比dos时代简单了些,但是有些概念也难理解了些。vb里面的简单的point函数其实最后也是被转化为这样的方式来执行,系统帮助做了很多事情。
到了mfc里面,由于有了封装,所有的hdc被隐藏在对象中做为隐藏参数传递(就是DC类的this啦~~),所以我们的关键话题就转变为了怎样得到想要的 DC类而已,这个过程其实大同小异的。在消息响应的过程中,WM_PAINT被转变为OnDraw(),OnPaint()一系列函数来响应,这些函数一般都有个参数CDC *pDC传入进来,因此在这些函数里面,我们就只需要直接画图就可以了,和以前sdk的方式一样。
但是WM_PAINT消息响应的频度太高了,比如最小化最大化,移动窗体,覆盖等等都引起重绘,经常的这样画图,很是消耗性能;在有些场合,比如随机作图的场合,每一次就改变,还导致了程序的无法实现。怎么解决后一种问题呢。
ms 在msdn的例子里面交给我们document/view的经典解决办法,将图形的数据存储在document类里面,view类只是根据这些数据绘图。比如你要画个圆,只是将圆心和半径存在document里面,view类根据这个里面的数据在屏幕上面重新绘制。那么,我们只需要随机产生一次数据就可以了。
这样还是存在性能的问题,于是我们开始考虑另外的解决方法。我们知道,将内存中的图片原样输出到屏幕是很快的,这也是我们在dos时代经常做的事情,能不能在windows也重新利用呢?答案就是内存缓冲绘图,我们今天的主题。
我们还是回到DC上来,既然DC是绘图对象,我们也就可以自己来在内存里面造一个,让它等于我们想要的图,图(CBitmap)可以存储在 document 类里面,每一次刷新屏幕都只是将这个图输出到屏幕上面,每一次作图都是在内存里面绘制,保存在document的图里面,必要时还可以将图输出到外存保存。这样既保证了速度,也解决了随机的问题,在复杂作图的情况下对内存的开销也不大(总是一副图片的大小)。这是一个很好的解决办法,现在让我们来实现它们。
我们在document类里面保存一个图片
CBitmap m_bmpBuf;//这里面保存了我们做的图,存在于内存中
在view类里面我们需要将这个图拷贝到屏幕上去
位于OnDraw(CDC *pDC)函数中:
CDC dcMem;//以下是输出位图的标准操作
CBitmap *pOldBitmap = NULL;
dcMem.CreateCompatibleDC(NULL);
pOldBitmap = dcMem.SelectObject(&pDoc->m_bmpBuf);
BITMAP bmpinfo;
pDoc->m_bmpBuf.GetBitmap(&bmpinfo);
pDC->BitBlt(0,0,bmpinfo.bmWidth,bmpinfo.bmHeight,&dcMem,0,0,SRCCOPY);
dcMem.SelectObject(pOldBitmap);
dcMem.DeleteDC();
在我们需要画图的函数里面,我们完成绘图工作
CBmpDrawDoc *pDoc = GetDocument(); //得到document中的bitmap对象
CDC *pDC = GetDC();
CDC dcMem;
dcMem.CreateCompatibleDC(NULL);//这里我们就在内存中虚拟建造了DC
pDoc->m_bmpBuf.DeleteObject();
pDoc->m_bmpBuf.CreateCompatibleBitmap(pDC,100,100);//依附DC创建bitmap
CBitmap *pOldBitmap = dcMem.SelectObject(&pDoc->m_bmpBuf);//我们调入了我们bitmap目标
dcMem.FillSolidRect(0,0,100,100,RGB(255,255,255));//这些时绘图操作,随便你^_^
dcMem.TextOut(0,0,"Hello,world!");
dcMem.Rectangle(20,20,40,40);
dcMem.FillSolidRect(40,40,50,50,RGB(255,0,0));
pDC->BitBlt(0,0,100,100,&dcMem,0,0,SRCCOPY);//第一次拷贝到屏幕
dcMem.SelectObject(pOldBitmap);
dcMem.DeleteDC();
全部的过程就是这样,很简单吧。以此为例子还可以实现2个缓冲或者多个缓冲等等,视具体情况而定。当然在缓冲区还可以实现很多高级的图形操作,比如透明,合成等等,取决于具体的算法,需要对内存直接操作(其实就是当年dos怎么做,现在还怎么做)。
再来解释一下前面说的为什么不能用全局变量保存DC问题。其实DC也是用句柄来标识的,所以也具有句柄的不确定性,就是只能随用随取,不同时间两次取得的是不同的(使用过文件句柄地话,应该很容易理解的)。那么我们用全局变量保存的DC就没什么意义了,下次使用只是什么也画不出来。(这一点的理解可以这样: DC需要占用一定的内存,那么在频繁的页面调度中,位置难免改变,于是用来标志指针的句柄也就不同了)。
=================================================================================
VC 双缓冲绘图
以下内容来自:http://dev.rdxx.com/VC/Problem/2008/12/1919491244642.shtml
在图形图象处理编程过程中,双缓冲是一种基本的技术。我们知道,如果窗体在响应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 );
经常出现图形闪烁的现象。利用先在内存绘制,然后
拷贝到屏幕的办法可以消除屏幕闪烁,具体的方法是先在内存
中创建一个与设备兼容的内存设备上下文,也就是开辟一快内
存区来作为显示区域,然后在这个内存区进行绘制图形。在绘制完成后利用
BitBlt函数把内存的图形直接拷贝到屏幕上即可。
具体的代码实现为:
(1)创建内存区域
CDC* pMem=new CDC;
CBitmap* pBmp=new CBitmap;
CBitmap* pOldBmp;
CDC* pDC=GetDC();
CRect rectTemp;为绘图区域
pMem->CreateCompatibleDC(pDC);
pBmp->CreateCompatibleBitmap(pDC, rectTemp.Width(), rectTemp.Height());
pOldBmp=pMem->SelectObject(pBmp);
(2)进行图形绘制
pMem->LineTo(...); 进行绘图处理
(3)拷贝到屏幕
pDC->BitBlt(rectTemp.left,rectTemp.top,rectTemp.Width(),rectTemp.Height(),pMem,0,0,SRCCOPY);
pMem->SelectObject(pOldBmp);
pBmp->DeleteObject() ;
pMem->DeleteDC();
================================================================================================
双缓存机制解决VC++绘图时的闪烁问题
显示图形如何避免闪烁,如何提高显示效率是问得比较多的问题。
而且多数人认为MFC的绘图函数效率很低,总是想寻求其它的解决方案。
MFC的绘图效率的确不高但也不差,而且它的绘图函数使用非常简单,
只要使用方法得当,再加上一些技巧,用MFC可以得到效率很高的绘图程序。
我想就我长期(呵呵当然也只有2年多)使用MFC绘图的经验谈谈
我的一些观点。
1、显示的图形为什么会闪烁?
我们的绘图过程大多放在OnDraw或者OnPaint函数中,OnDraw在进行屏
幕显示时是由OnPaint进行调用的。当窗口由于任何原因需要重绘时,
总是先用背景色将显示区清除,然后才调用OnPaint,而背景色往往与绘图内容
反差很大,这样在短时间内背景色与显示图形的交替出现,使得显示窗口看起来
在闪。如果将背景刷设置成NULL,这样无论怎样重绘图形都不会闪了。
当然,这样做会使得窗口的显示乱成一团,因为重绘时没有背景色对原来
绘制的图形进行清除,而又叠加上了新的图形。
有的人会说,闪烁是因为绘图的速度太慢或者显示的图形太复杂造成的,
其实这样说并不对,绘图的显示速度对闪烁的影响不是根本性的。
例如在OnDraw(CDC *pDC)中这样写:
pDC->MoveTo(0,0);
pDC->LineTo(100,100);
这个绘图过程应该是非常简单、非常快了吧,但是拉动窗口变化时还是会看见
闪烁。其实从道理上讲,画图的过程越复杂越慢闪烁应该越少,因为绘图用的
时间与用背景清除屏幕所花的时间的比例越大人对闪烁的感觉会越不明显。
比如:清楚屏幕时间为1s绘图时间也是为1s,这样在10s内的连续重画中就要闪
烁5次;如果清楚屏幕时间为1s不变,而绘图时间为9s,这样10s内的连续重画
只会闪烁一次。这个也可以试验,在OnDraw(CDC *pDC)中这样写:
for(int i=0;i<100000;i++)
{
pDC->MoveTo(0,i);
pDC->LineTo(1000,i);
}
呵呵,程序有点变态,但是能说明问题。
说到这里可能又有人要说了,为什么一个简单图形看起来没有复杂图形那么
闪呢?这是因为复杂图形占的面积大,重画时造成的反差比较大,所以感觉上要
闪得厉害一些,但是闪烁频率要低。
那为什么动画的重画频率高,而看起来却不闪?这里,我就要再次强调了,
闪烁是什么?闪烁就是反差,反差越大,闪烁越厉害。因为动画的连续两个帧之间
的差异很小所以看起来不闪。如果不信,可以在动画的每一帧中间加一张纯白的帧,
不闪才怪呢。
2、如何避免闪烁
在知道图形显示闪烁的原因之后,对症下药就好办了。首先当然是去掉MFC
提供的背景绘制过程了。实现的方法很多,
* 可以在窗口形成时给窗口的注册类的背景刷付NULL
* 也可以在形成以后修改背景
static CBrush brush(RGB(255,0,0));
SetClassLong(this->m_hWnd,GCL_HBRBACKGROUND,(LONG)(HBRUSH)brush);
* 要简单也可以重载OnEraseBkgnd(CDC* pDC)直接返回TRUE
这样背景没有了,结果图形显示的确不闪了,但是显示也象前面所说的一样,
变得一团乱。怎么办?这就要用到双缓存的方法了。双缓冲就是除了在屏幕上有
图形进行显示以外,在内存中也有图形在绘制。我们可以把要显示的图形先在内存中
绘制好,然后再一次性的将内存中的图形按照一个点一个点地覆盖到屏幕上去(这个
过程非常快,因为是非常规整的内存拷贝)。这样在内存中绘图时,随便用什么反差
大的背景色进行清除都不会闪,因为看不见。当贴到屏幕上时,因为内存中最终的图形
与屏幕显示图形差别很小(如果没有运动,当然就没有差别),这样看起来就不会闪。
3、如何实现双缓冲
首先给出实现的程序,然后再解释,同样是在OnDraw(CDC *pDC)中:
CDC MemDC; //首先定义一个显示设备对象
CBitmap MemBitmap;//定义一个位图对象
//随后建立与屏幕显示兼容的内存显示设备
MemDC.CreateCompatibleDC(NULL);
//这时还不能绘图,因为没有地方画 ^_^
//下面建立一个与屏幕显示兼容的位图,至于位图的大小嘛,可以用窗口的大小
MemBitmap.CreateCompatibleBitmap(pDC,nWidth,nHeight);
//将位图选入到内存显示设备中
//只有选入了位图的内存显示设备才有地方绘图,画到指定的位图上
CBitmap *pOldBit=MemDC.SelectObject(&MemBitmap);
//先用背景色将位图清除干净,这里我用的是白色作为背景
//你也可以用自己应该用的颜色
MemDC.FillSolidRect(0,0,nWidth,nHeight,RGB(255,255,255));
//绘图
MemDC.MoveTo(……);
MemDC.LineTo(……);
//将内存中的图拷贝到屏幕上进行显示
pDC->BitBlt(0,0,nWidth,nHeight,&MemDC,0,0,SRCCOPY);
//绘图完成后的清理
MemBitmap.DeleteObject();
MemDC.DeleteDC();
上面的注释应该很详尽了,废话就不多说了。
4、如何提高绘图的效率
我主要做的是电力系统的网络图形的CAD软件,在一个窗口中往往要显示成千上万个电力元件,而每个元件又是由点、线、圆等基本图形构成。如果真要在一次重绘过程重画这么多元件,可想而知这个过程是非常漫长的。如果加上了图形的浏览功能,鼠标拖动图形滚动时需要进行大量的重绘,速度会慢得让用户将无法忍受。怎么办?只有再研究研究MFC 的绘图过程了。
实际上,在OnDraw(CDC *pDC)中绘制的图并不是所有都显示了的,例如:你
在OnDraw中画了两个矩形,在一次重绘中虽然两个矩形的绘制函数都有执行,但是很有可能只有一个显示了,这是因为MFC本身为了提高重绘的效率设置了裁剪区。裁剪区的作用就是:只有在这个区内的绘图过程才会真正有效,在区外的是无效的,即使在区外执行了绘图函数也是不会显示的。因为多数情况下窗口重绘的产生大多是因为窗口部分被遮挡或者窗口有滚动发生,改变的区域并不是整个图形而只有一小部分,这一部分需要改变的就是pDC中的裁剪区了。因为显示(往内存或者显存都叫显示)比绘图过程的计算要费时得多,有了裁剪区后显示的就只是应该显示的部分,大大提高了显示效率。但是这个裁剪区是MFC设置的,它已经为我们提高了显示效率,在进行复杂图形的绘制时如何进一步提高效率呢?那就只有去掉在裁剪区外的绘图过程了。可以先用 pDC->GetClipBox()得到裁剪区,然后在绘图时判断你的图形是否在这个区内,如果在就画,不在就不画。
如果你的绘图过程不复杂,这样做可能对你的绘图效率不会有提高。
=================================================================================================
VC++大数据量绘图时无闪烁刷屏技术实现
引言
当我们需要在用户区显示一些图形时,先把图形在客户区画上,虽然已经画好但此时我们还无法看到,还要通过程序主动地刷新用户区,强制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编译通过。
=============================================================================================
用双缓存技术,和重载OnEraseBKgnd(),
怎么重载OnEraseBkgnd()函数??- -
选择View->ClassWizard->classinfo选项
卡:message filter下拉框: 选择window,然后再选择
message maps选项卡,在messages下拉框应该可以找到
wm_erasebkgnd.双击添加.
======================================================================================================
如何实现双缓冲
首先给出实现的程序,然后再解释,同样是在OnDraw(CDC *pDC)中:
CDC MemDC; //首先定义一个显示设备对象
CBitmap MemBitmap;//定义一个位图对象
//随后建立与屏幕显示兼容的内存显示设备
MemDC.CreateCompatibleDC(NULL);
//这时还不能绘图,因为没有地方画 ^_^
//下面建立一个与屏幕显示兼容的位图,至于位图的大小嘛,可以用窗口的大小,也可以自己定义(如:有滚动条时就要大于当前窗口的大小,在BitBlt时决定拷贝内存的哪部分到屏幕上)
MemBitmap.CreateCompatibleBitmap(pDC,nWidth,nHeight);
//将位图选入到内存显示设备中
//只有选入了位图的内存显示设备才有地方绘图,画到指定的位图上
CBitmap *pOldBit=MemDC.SelectObject(&MemBitmap);
//先用背景色将位图清除干净,这里我用的是白色作为背景
//你也可以用自己应该用的颜色
MemDC.FillSolidRect(0,0,nWidth,nHeight,RGB(255,255,255));
//绘图
MemDC.MoveTo(……);
MemDC.LineTo(……);
//将内存中的图拷贝到屏幕上进行显示
pDC->BitBlt(0,0,nWidth,nHeight,&MemDC,0,0,SRCCOPY);
//绘图完成后的清理
MemBitmap.DeleteObject();
MemDC.DeleteDC();
==================================================================================================
用vc 做程序,如何画图是一个大家都很关心,但是却感到很难以理解的问题,因为在mfc的封装之下,没有现成的画图函数供你直接调用,像vb等等里面直接来个 point之类的,常常让人感到无从下手。这两天帮人解决了一个用内存缓冲画图的问题,顺便也就谈谈这些东西,也算是总结。
我先来解释一下在mfc里面很关键的设备环境描述符,也就是所谓的DC(device context)。
还是从历史来看吧,dos时代,我们如果要绘图,必须通过一系列系统函数来启动图形环境(用过turbo pascal或者turbo c的人该还有印象吧),这之间对各种硬件的初始化参数都不相同,非常的烦人,常常还要查阅硬件手册,那时的程序智能针对最流行的硬件来编写,对不流行的就没有办法了。windows操作系统为了屏蔽不同的硬件环境,让编程时候不考虑具体的硬件差别,采取了一系列办法,设备环境描述符就是这样产生的。简单的说,设备描述符抽象了不同的硬件环境为标准环境,用户编写时使用的是这个虚拟的标准环境,而不是真实的硬件,与真实硬件打交道的工作一般交给了系统和驱动程序完成(这同样解释了为什么我们需要经常更新驱动程序的问题)。使用在windows图形系统(gdi,而不包括direct x)上面,就体现在一系列的图形DC上面,我们如果要在gdi上面绘图,就必须先得到图形DC的句柄(handle),然后指定句柄的基础上进行图形操作。
再来回忆一下,我们怎么在sdk的环境下面绘图呢,我想这个大家都不太清楚吧,但是确实很基础。在windows的sdk环境下面,我们用传统的c编写程序,在需要的绘图地方(比如响应WM_PAINT消息的分支)这样做:
hdc = GetDC( hwnd );
oldGdiObject = SelectObject( hdc,newGdiObject );
...绘图操作...
SelectObject( hdc,oldGdiObject );
DeleteObject( newGdiObject );
ReleaseDC( hdc);
或者是这样
BeginPaint( hwnd,&ps );//PAINTSTRUCT ps -- ps is a paint struct
...绘图操作...
EndPaint( hwnd )
这就是大概的过程,我们看到了hdc(图形DC句柄)的应用,在绘图的部分,每一个绘图函数基本上也要用到这个句柄,最后我们还必须释放它,否则将严重影响性能。每次我们都必须调用GetDC这个api函数得到(不能用全局变量保存结果重复使用,我在后面解释)。这些是最最基本的windows图形操作的方式,相比dos时代简单了些,但是有些概念也难理解了些。vb里面的简单的point函数其实最后也是被转化为这样的方式来执行,系统帮助做了很多事情。
到了mfc里面,由于有了封装,所有的hdc被隐藏在对象中做为隐藏参数传递(就是DC类的this啦~~),所以我们的关键话题就转变为了怎样得到想要的 DC类而已,这个过程其实大同小异的。在消息响应的过程中,WM_PAINT被转变为OnDraw(),OnPaint()一系列函数来响应,这些函数一般都有个参数CDC *pDC传入进来,因此在这些函数里面,我们就只需要直接画图就可以了,和以前sdk的方式一样。
但是WM_PAINT消息响应的频度太高了,比如最小化最大化,移动窗体,覆盖等等都引起重绘,经常的这样画图,很是消耗性能;在有些场合,比如随机作图的场合,每一次就改变,还导致了程序的无法实现。怎么解决后一种问题呢。
ms 在msdn的例子里面交给我们document/view的经典解决办法,将图形的数据存储在document类里面,view类只是根据这些数据绘图。比如你要画个圆,只是将圆心和半径存在document里面,view类根据这个里面的数据在屏幕上面重新绘制。那么,我们只需要随机产生一次数据就可以了。
这样还是存在性能的问题,于是我们开始考虑另外的解决方法。我们知道,将内存中的图片原样输出到屏幕是很快的,这也是我们在dos时代经常做的事情,能不能在windows也重新利用呢?答案就是内存缓冲绘图,我们今天的主题。
我们还是回到DC上来,既然DC是绘图对象,我们也就可以自己来在内存里面造一个,让它等于我们想要的图,图(CBitmap)可以存储在 document 类里面,每一次刷新屏幕都只是将这个图输出到屏幕上面,每一次作图都是在内存里面绘制,保存在document的图里面,必要时还可以将图输出到外存保存。这样既保证了速度,也解决了随机的问题,在复杂作图的情况下对内存的开销也不大(总是一副图片的大小)。这是一个很好的解决办法,现在让我们来实现它们。
我们在document类里面保存一个图片
CBitmap m_bmpBuf;//这里面保存了我们做的图,存在于内存中
在view类里面我们需要将这个图拷贝到屏幕上去
位于OnDraw(CDC *pDC)函数中:
CDC dcMem;//以下是输出位图的标准操作
CBitmap *pOldBitmap = NULL;
dcMem.CreateCompatibleDC(NULL);
pOldBitmap = dcMem.SelectObject(&pDoc->m_bmpBuf);
BITMAP bmpinfo;
pDoc->m_bmpBuf.GetBitmap(&bmpinfo);
pDC->BitBlt(0,0,bmpinfo.bmWidth,bmpinfo.bmHeight,&dcMem,0,0,SRCCOPY);
dcMem.SelectObject(pOldBitmap);
dcMem.DeleteDC();
在我们需要画图的函数里面,我们完成绘图工作
CBmpDrawDoc *pDoc = GetDocument(); //得到document中的bitmap对象
CDC *pDC = GetDC();
CDC dcMem;
dcMem.CreateCompatibleDC(NULL);//这里我们就在内存中虚拟建造了DC
pDoc->m_bmpBuf.DeleteObject();
pDoc->m_bmpBuf.CreateCompatibleBitmap(pDC,100,100);//依附DC创建bitmap
CBitmap *pOldBitmap = dcMem.SelectObject(&pDoc->m_bmpBuf);//我们调入了我们bitmap目标
dcMem.FillSolidRect(0,0,100,100,RGB(255,255,255));//这些时绘图操作,随便你^_^
dcMem.TextOut(0,0,"Hello,world!");
dcMem.Rectangle(20,20,40,40);
dcMem.FillSolidRect(40,40,50,50,RGB(255,0,0));
pDC->BitBlt(0,0,100,100,&dcMem,0,0,SRCCOPY);//第一次拷贝到屏幕
dcMem.SelectObject(pOldBitmap);
dcMem.DeleteDC();
全部的过程就是这样,很简单吧。以此为例子还可以实现2个缓冲或者多个缓冲等等,视具体情况而定。当然在缓冲区还可以实现很多高级的图形操作,比如透明,合成等等,取决于具体的算法,需要对内存直接操作(其实就是当年dos怎么做,现在还怎么做)。
再来解释一下前面说的为什么不能用全局变量保存DC问题。其实DC也是用句柄来标识的,所以也具有句柄的不确定性,就是只能随用随取,不同时间两次取得的是不同的(使用过文件句柄地话,应该很容易理解的)。那么我们用全局变量保存的DC就没什么意义了,下次使用只是什么也画不出来。(这一点的理解可以这样: DC需要占用一定的内存,那么在频繁的页面调度中,位置难免改变,于是用来标志指针的句柄也就不同了)。
=================================================================================
VC 双缓冲绘图
以下内容来自:http://dev.rdxx.com/VC/Problem/2008/12/1919491244642.shtml
在图形图象处理编程过程中,双缓冲是一种基本的技术。我们知道,如果窗体在响应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 );
====================================================================================================================