还是从历史来看吧,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需要占用一定的内存,那么在频繁的页面调度中,位置难免改变,于是用来标志指针的句柄也就不同了)。
利用内存DC,进行绘图,从而减少闪烁,方法原理为:
此方法涉及到两个DC,屏幕DC和内存DC。把所要绘制的一切现在内存DC中进行绘制,之后全部搬到
屏幕DC中,从而把所有烦琐的绘制过程都在内存DC中完成了,用户在屏幕上看到的是一幅完整的图画,所以不可能出现
闪烁情况。期间,关键是这幅图画。这幅图画是从屏幕DC中创建出来的,只不画面的尺寸就是客户区域的大小,之后把
这幅画选入内存DC中,之后在内存DC中绘制的动作都在这幅画中,最后把内存DC中的这幅已经绘制好的画
在选入到屏幕DC中,达到最终目的。
方法:
首先创建关于屏幕的内存DC,MemDC.CreateCompatibleDC( pDC);
之后创建一幅关于屏幕DC的图画
CRect rect;
this->GetClientRect(rect);
CBitmap bmpFace;
bmpFace.CreateCompatibleBitmap(pDC,rect.Width(),rect.Height());注意把握rect的尺寸为客户区域大小;
之后将这幅画选入内存DC中,
CBitmap* pOldBmp = NULL;pOldBmp = MemDC.SelectObject(&bmpFace);;
之后可以开始在内存DC中进行任何绘制动作;
CBrush brush(RGB(255,255,255));
MemDC.FillRect(rect,&brush);
for(int i=0;i<500;i++)
{
MemDC.MoveTo(22+i,22);
MemDC.LineTo(22+i,277);
}
绘制完后将内存DC中的这幅图绘制到屏幕DC中来,
pDC->BitBlt(rect.left,rect.top,rect.Width(),rect.Height(),&MemDC,rect.left,rect.top,SRCCOPY);
最后进行相关的资源回收动作,
MemDC.SelectObject(pOldBmp);
bmpFace.DeleteObject();。
同时我们要把系统的ON_WM_ERASEBKGND消息进行修改,否则也回出现狂闪情况。
return FALSE;
pOldBmp = MemDC.SelectObject(&bmpFace);;之后可以开始在内存DC中进行任何绘制动作;CBrush brush(RGB(255,255,255));MemDC.FillRect(rect,&brush);for(int i=0;i<500;i++){MemDC.MoveTo(22+i,22);MemDC.LineTo(22+i,277);}绘制完后将内存DC中的这幅图绘制到屏幕DC中来,pDC->BitBlt(rect.left,rect.top,rect.Width(),rect.Height(),&MemDC,rect.left,rect.top,SRCCOPY);最后进行相关的资源回收动作,MemDC.SelectObject(pOldBmp);bmpFace.DeleteObject();。同时我们要把系统的ON_WM_ERASEBKGND消息进行修改,否则也回出现狂闪情况。return FALSE;
用两个设备就行
CDC *mDC=new CDC;
CDC *mDC1=new CDC;
mDC->CreateCompatibleDC(&dc);
mDC1->CreateCompatibleDC(&dc);
.........
mDC->SelectObject(tempbmp); //设置mDC存储位图的格式
mDC1->SelectObject(bitmap); //选入位图
mDC->BitBlt(x,x,x,x,mDC1,x,x,x); //将位图贴到mDC
......
dc.BitBlt(x,x,x,x,mDC,x,x,x);
图形为什么会闪烁的原因是:我们的绘图过程大多放在OnDraw或者OnPaint函数中,OnDraw在进行屏幕显示时是由OnPaint进行调用的。当窗口由于任何原因需要重绘时,总是先用背景色将显示区清除,然后才调用OnPaint,而背景色往往与绘图内容反差很大,这样在短时间内背景色与显示图形的交替出现,使得显示窗口看起来在闪。如果将背景刷设置成NULL,这样无论怎样重绘图形都不会闪了。当然,这样做会使得窗口的显示乱成一团,因为重绘时没有背景色对原来绘制的图形进行清除,而又叠加上了新的图形。有的人会说,闪烁是因为绘图的速度太慢或者显示的图形太复杂造成的,其实这样说并不对,绘图的显示速度对闪烁的影响不是根本性的。
如何实现双缓冲:在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();
以论坛的一个帖子例子为例来说明一些具体如何解决问题.
帖子那容是:
我想让一个区域动起来,
如何解决窗口刷新时区域的闪烁。
void CJhkljklView::OnDraw(CDC* pDC)
{
CJhkljklDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
int i;
int x[20],y[20];
CPen hPen;
POINT w[5];
x[0]=a/100+10;
x[1]=a/100+30;
x[2]=a/100+80;
x[3]=a/100+30;
x[4]=a/100+10;
y[0]=10;
y[1]=10;
y[2]=25;
y[3]=40;
y[4]=40;
for (i=0;i<5;i++)
{ w[i].x=x[i];
w[i].y=y[i];
}
//CClientDC dc(this);
//hPen=CreatePen(PS_SOLID,1,RGB(255,0,0));
CRgn argn,Brgn;
CBrush abrush(RGB(40,30,20));
argn.CreatePolygonRgn(w, 5, 1);// point为CPoint数组,
pDC->FillRgn(&argn, &abrush);
abrush.DeleteObject();
}
void CJhkljklView::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
InvalidateRect(NULL,true);
UpdateWindow();
a+=100;
CView::OnTimer(nIDEvent);
}
int CJhkljklView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: Add your specialized creation code here
SetTimer(1,10,NULL);
return 0;
}
利用定时器直接进行10毫秒的屏幕刷新,这样效果会出现不停的闪烁的情况.
解决方法利用双缓冲,首先触发WM_ERASEBKGND,然后修改返回TRUE;
定义变量:
CBitmap *m_pBitmapOldBackground ;
CBitmap m_bitmapBackground ;
CDC m_dcBackground;
//绘制背景
if(m_dcBackground.GetSafeHdc()== NULL|| (m_bitmapBackground.m_hObject == NULL))
{
m_dcBackground.CreateCompatibleDC(&dc);
m_bitmapBackground.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height()) ;
m_pBitmapOldBackground = m_dcBackground.SelectObject(&m_bitmapBackground) ;
//DrawMeterBackground(&m_dcBackground, rect);
CBrush brushFill, *pBrushOld;
// 背景色黑色
brushFill.DeleteObject();
brushFill.CreateSolidBrush(RGB(255, 255, 255));
pBrushOld = m_dcBackground.SelectObject(&brushFill);
m_dcBackground.Rectangle(rect);
m_dcBackground.SelectObject(pBrushOld);
}
memDC.BitBlt(0, 0, rect.Width(), rect.Height(),
&m_dcBackground, 0, 0, SRCCOPY) ;
//绘制图形
int i;
int x[20],y[20];
CPen hPen;
POINT w[5];
x[0]=a/100+10;
x[1]=a/100+30;
x[2]=a/100+80;
x[3]=a/100+30;
x[4]=a/100+10;
y[0]=10;
y[1]=10;
y[2]=25;
y[3]=40;
y[4]=40;
for (i=0;i<5;i++)
{ w[i].x=x[i];
w[i].y=y[i];
}
//CClientDC dc(this);
//hPen=CreatePen(PS_SOLID,1,RGB(255,0,0));
CRgn argn,Brgn;
CBrush abrush(RGB(40,30,20));
argn.CreatePolygonRgn(w, 5, 1);// point为CPoint数组,
memDC.FillRgn(&argn, &abrush);
abrush.DeleteObject();
}