当前位置:C++技术网 > 精选软件 > 绘图技术的闪烁原因探究4:详细分析编程实现双缓冲绘图技术

绘图技术的闪烁原因探究4:详细分析编程实现双缓冲绘图技术

更新时间:2016-04-23 00:49:09浏览次数:1+次

    在文章《绘图技术的闪烁原因探究1:大神学习方法(研究性学习即通过实验来探索学习) 》中,讲了学习方法,是主打授人鱼不如授人以渔。因为所有这些说法做法,都是参照我自己的学习方法来写的,因为我自己觉得很受用,大大提高了学习效率,也让我思考能力增强了很多。这是我的一个优点,能够拥有这个优点(善于思考分析问题和解决问题)的人不多,学习很机械,效率很低。
    在现在社会竞争如此激烈的情况下,拥有这样的能力是比较有优势的,同时大家都是技术人,相互之前可能还存在竞争,毕竟只有少数掌握这样的学习能力,那么自己也会更加有优势。如果大家都有了这样的学习能力,自己的优势也就变差了。绝大多数人都视为珍宝,我也如此。然而大多数人是不愿意公开的,或者遮遮掩掩的,生怕自己的优势被他人占据。
    然而,我经历过残忍低效的学习过程,历经千辛万苦才锻炼到了善于思考的能力。我知道很多人都缺乏这样的锻炼,所以我经常特意强调。并不是泛泛而谈,也不是废话连篇。越来越多的人认可我的写作风格,喜欢我的写作风格,因为我是用心写,写出我的真正的要写的,毫无保留。每次写完一个技术,从思想上的矫正,态度的分析和学习方法,最后才是技术的深度全面分析,都完全毫无保留的奉献给大家。因为我真心想帮助到读者,而不是博得眼球。 每次将一个技术主题全面深入总结之后,写完后总有一种内心被掏空的感觉,所有知道的都写出来了。这样才是分享。而且更多我们不是在讲技术本身,而是讲更多如何学习的问题。
    所以,如果你不要企图这些文章都能够简单罗列知识点,告诉你结论。不要觉得这些铺垫的话都是废话。为什么你会浮躁,为什么你看不下资料,为什么会觉得一些铺垫都是废话?如何静心,我的文章开篇大都是给你静心,这个比一开始讲技术更重要,如果你没有一个好的心态看技术文章,会忽略很多细节,看的很着急。我很喜欢看一个技术的发展背景和来龙去脉,技术里的为什么,而是技术里的是什么。然而当你看完我的系列文章,技术点都不是事,你会学的游刃有余。如果很着急,就不适合看我这些文章了。
    因为看到有人抱怨,所以就多说了几句,是希望能够用正确的心态来阅读。下面开始讲解消除绘图闪烁问题的技术,即双缓冲绘图技术。
    双缓冲绘图技术原理已经在文章《绘图技术的闪烁原因探究3:深入分析绘图闪烁原因和详细解释解决闪烁办法》深入浅出的分析了,就不在这里重复了。
    我们这里就使用MFC的代码实战编程来讲解双缓冲绘图技术。双缓冲绘图技术,又可以叫做间接绘图技术,或者用间接绘图技术叫法更加准确。为什么叫做双缓冲绘图技术,在“绘图技术的闪烁原因探究3”文章里解释了。

    我还是用一个简图来描述一下双缓冲绘图技术的过程,然后会更好理解代码。双缓冲绘图技术原理图如下所示:

双缓冲绘图技术原理图

【双缓冲绘图技术原理图】
    从图中可以看到,屏幕硬件是最终显示内容的东西。闪烁也来自于屏幕硬件。至于闪烁的原理以及人眼感受到闪烁这些都在前面的文章讲过了。我们普通的绘图,也就是用GDI直接绘图。我们看到屏幕硬件后面对应着一个显卡缓存。只要你将图形数据发送到显卡缓存,也就完成了绘制动作了。接下来从显卡缓存将图形数据复制到屏幕硬件显示则不是我们GDI绘图程序管的。GDI绘图也就是将数据发送到了显卡缓存了。反正显卡缓存到屏幕显示是很快的。
    我们感觉到的就是,GDI得到设备的DC句柄后,就好像直接向屏幕绘制了。每一次GDI绘图调用,都会完成一次图形数据到显卡缓存,然后显卡缓存数据到屏幕硬件,最后就显示出来了。我们如果大量的使用GDI函数绘图,执行了很多次,这样就向显卡缓存发送了很多次的数据。我们一个屏幕的画面调用了GDI函数很多次,也就是说,我们看到一个完整的画面,实际上是执行了很多次GDI绘图的结果。如果其中一次执行变慢了(如CPU占用过高),然后就导致两次有颜色反差的画面的叠加被人眼观察到了,这样就出现了闪烁。一个画面就很多次GDI操作,如果使用计时器不停的绘制这个画面,也就产生了大量的GDI操作,每一次都是很快就显示在屏幕,因此容易造成闪烁。其实这些在前面的文章解释了。
    那么我们想到的办法就是使用内存做一级缓存,先将一个完整画面的所有绘图操作都绘制到内存里存储起来,绘制完毕后然后一次性将内存的数据复制到显卡缓存中,这样就可以有效避免闪烁。

    那么问题来了,我们如何将图形画到内存,内存如何存储图形数据,图形数据如何叠加到内存,如何将内存的图形数据一次性高效复制到显卡缓存中。好的,下面就在铺垫之后,进入编程环境的技术讲解了。前面的铺垫对于编程的理解至关重要。很多文章也都介绍了编程的实现过程,但是都是解释的不清楚,看了也看不懂。我这里就将原理和代码关联起来,让你学到的理论直接强势的应用于编程代码的理解。理解之后,你知道哪些代码必不可少,哪些代码可以选择性使用,整个流程有哪些代码。
    在MFC中,CDC类是GDI绘图类,用CDC类创建的对象本身并不能表示什么。CDC对象必须关联到设备或者内存,才决定这个类绘制的图形是直接发送到显卡缓存还是内存。
    下面的代码将创建一个空白的CDC对象:
CDC dc;
    CDC类有一个成员为m_hDC,这个成员为GDI绘图的设备环境上下文句柄,类型为HDC。在win32中我们对于HDC是很熟悉的,CDC只是将这个句柄封装成了类里的一个成员而已。CDC的绘图还是通过HDC来完成的。也就是说,m_hDC才是真正决定CDC是将绘图的数据发送到设备还是发送到内存的。
    那么作为输出图像的屏幕,是硬件设备。而GDI设计为可以与硬件无关,所以我们在窗口绘图时,输出设备就是窗口了,那么窗口是逻辑的输出设备,系统最终会将窗口的图形映射到屏幕上,从而被我们看到。那么在窗口绘图编程中,我们只要将HDC关联到窗口或者直接关联到屏幕,就可以实现CDC绘制的图像数据直接显示在屏幕。
    如果你是创建的CDC对象,那么就需要调用CDC类的成员函数Attach(HDC hdc)来做一个关联。Attach只是将传入的HDC赋值给m_hDC,这样我们使用CDC对象执行的所有GDI操作都会发送到m_hDC指定的设备,一般就是窗口。而HDC如何获取呢?当然有各种API来获取,比如GetDC()、GetClientDC()、GetWindowDC()等,这些函数可以查询MSDN,没必要一个个说一下。这些函数也就是传入窗口的句柄返回窗口的绘图句柄(HDC)或者不传入句柄得到屏幕绘图句柄(HDC)。如果在Win32中,都调用API函数,传入窗口句柄还是比较方便的。然而在MFC中,我们就不需要直接使用API函数来获取了。
    实际上,在MFC程序中,我们很少直接用CDC对象,更多的时候是用CDC指针。因为CWnd窗口类直接支持返回CDC类指针,同时m_hDC也有正确的窗口绘图句柄值了。所以你可以看到,很少看到CDC对象使用,基本上都是CDC指针。
    下面是常用的代码:
CDC * pDC = GetDC();
    这句代码要运行在派生于CWnd类的类函数中,因为GetDC()是CWnd类的成员函数。如果在CWnd派生的窗口类中,你可以直接用API函数来获取HDC,然后调用CDC类的Attach函数关联,如:
CDC dc;
HDC hdc = GetDC(hwnd);//hwnd窗口句柄
dc.Attach(hdc);
    我们到这里,就准备好了CDC了。我们此时做的是将HDC关联到了窗口,即直接绘制到窗口上。这也是我们平时学习到的GDI绘图。接下来就是各种GDI绘图了,这里就不列举了。对于直接绘图到屏幕,也是最常见的。然而将HDC定向到内存,也就在有闪烁的时候才非常需要。
    我们在前面的图中看到,将图形绘制到内存中,最终还是要输出显示才行的,否则看不到输出,绘制还有什么意义呢?这也就意味着,将图形绘制到内存,也就表示之后不久一定会将图形输出到屏幕上。所以,在将HDC定向到内存时,一定要做好将内存HDC与设备HDC的关联,这样方便在需要的时候将数据从内存复制到设备。
    那么实际上,内存CDC和设备CDC是一样的,都是CDC类,只是输出图像数据的位置不一样罢了。如果先将图形数据输出到内存,然后从内存复制到显卡缓存,也就经历了两个缓存,这也是双缓冲名字的由来。所以下面的代码就是创建一个内存CDC对象:
CDC dcMem;
dcMem.CreateCompatibleDC(pDc);//将内存HDC和设备HDC做好关联,绘制完毕后可以方便转移图形数据
    我们经常将图形数据绘制到内存的HDC叫做兼容DC。请在听到兼容DC或者内存DC的时候不要不知所措,实际上都是一个意思。CreateCompatibleDC的中文意思就是创建兼容DC。因为dcMem只是一个空白的对象,m_hDC成员没有有效值,所以dcMem是无法绘图的。CreateCompatibleDC就会用pDc这个已经存在的设备HDC的相关信息,在内存中创建一个一样的DC,只是这个DC是在内存中的。创建好后,dcMem对象的m_hDC成员也就有了一个关联到内存DC的有效值了。然后在用dcMem绘图,就会将图形绘制到内存中了。
    那么到此,我们就知道如何将图形绘制到内存中了。我们如何在内存中创建一个盛放绘图的数据的东西呢?我们知道,我们将图形直接绘制到屏幕,GDI会自动发送到显卡缓存,然后显示出来。而我们将发送图形数据到内存中,我们就需要管理绘图的数据的存放问题了。我们不用想得太复杂,不用自己创建一个缓冲区什么的,不用分配内存什么的,自然CBitmap类给我们准备好了一个函数,即CreateCompatibleBitmap。这个成员函数会创建一个与参数中指定的DC一样的颜色信息,兼容DC的颜色信息和设备DC的颜色信息是不一样的,兼容的信息自然要少,这样才更加通用,才能达到兼容效果。而我们最终绘制的图形要显示在设备上如窗口,所以也要获取和设备DC一样的颜色信息。这样创建的兼容位图数据最终复制到设备DC时可以保持真实效果。如果你用兼容DC来创建兼容位图,颜色的数量有限,因此在绘制彩色的图形时,就无法表现出来,而是用少量的颜色模拟出来,然后再复制到设备DC时就出现黑白的图像,而不是彩色图像。而如果用设备DC创建兼容位图,则可以保留所有真实色彩信息,那么最终画图来的图像颜色和预想的一样。这个问题有人问过,说为什么他画出来的图像是黑白的,为什么不是色彩的,有人回答说是只能画黑白的,纯粹是想当然了。当你明白了这个道理时,你不仅可以绘制彩色图像,而且知道为什么会出现黑白图像,这就是知其然知其所以然。请原谅我的啰嗦,这样你才可以懂得更多,懂得更彻底。代码如下:
CBitmap bmp;
bmp.CreateCompatibleBitmap(pDC,位图的宽度,位图的高度);
dcMem.SelectObject(bmp.m_hObject);
    我们调用CreateCompatibleBitmap函数,可以理解为创建一个位图而已,只是第一个参数指定的DC给位图提供色彩信息罢了。只是这个函数名称看起来有点吓人。然后我们用内存DC对象dcMem将位图选进DC中,这样就将位图作为绘图的地方了。那么也就是说,这个位图也就是装载我们所有的绘图数据了。我们每一次的内存DC绘制的内容都是直接往位图上绘制的,多次绘制也就叠加在位图上。位图就像一面墙,每一次绘画都在上一次的基础上叠加。只是你要清楚区分两个DC哦。
    我们用dcMem执行的所有绘图操作都将叠加在位图上。下面是内存DC绘图代码:
int i=0;
CFont font;
font.CreatePointFont(300,_T("黑体"));
dcMem.SelectObject(font.m_hObject);
CString str;
str.Format(_T("C++技术网www.cjjjs.com:%d"),i++);
dcMem.TextOut(0,0,str);
dcMem.TextOut(0,60,str);
dcMem.TextOut(0,120,str);
dcMem.TextOut(0,180,str);
dcMem.TextOut(0,240,str);
dcMem.TextOut(0,300,str);
dcMem.TextOut(0,360,str);
    内存DC绘图和普通的设备DC是一模一样的。所以,平时怎么绘图,这里还是怎么绘图。每一次的绘图都在位图上绘制,直到所有的绘图操作完成,位图也就存放好了所有的绘图数据,也就是一整个画面的图形数据。不过这里要注意一点,我们创建的位图,初始化时,所有的像素位都为0,这样显示的话,就是黑色。也就是说,我们的内存DC是在一张黑色的位图上绘图的,这样看到的图形的背景是黑色的。这个自然不是我们想要的。要想背景不是黑色,很简单,我们只要第一次绘图时将整个位图填充为其他颜色即可,也就是先刷个底色。使用FillRect即可完成。代码就不写了。
    下一个操作也就是要将内存中装满图形数据的位图显示到屏幕上。
pDc->BitBlt(设备的起始x坐标,设备的起始y坐标,设备接收数据的宽度,设备接收数据的高度,&dcMem,位图的起始x坐标,位图的起始y坐标,SRCCOPY);
    我们要将内存的位图数据复制到设备DC中显示,调用CDC的BitBlt函数即可。前面四个参数指定接收位图的起始坐标和大小,这四个参数可以让内存的位图数据显示在设备的任何位置。第五个参数为内存CDC的地址,第六个和第七个是内存DC的位图要被复制的的起始坐标,根据前面的参数也就可以决定复制位图的范围有多大。最后一个参数使用SRCCOPY表示将位图的数据一比一的复制到显卡缓存中显示,这样复制速度很快。一定要区别这一处两个DC的位置。调用函数的是目标DC,参数中的是源DC,方向是将源DC的位图数据复制到目标DC中显示。这里就是完成内存位图数据到显卡缓存的步骤。
    最后就是释放位图对象和内存DC。代码如下:
bmp.DeleteObject();
dcMem.DeleteDC();
    下面是完整的代码:
static int i=0;
CDC * pDC = GetDC();
CRect rect;
GetClientRect(&rect);
CDC dcMem;
dcMem.CreateCompatibleDC(pDC);
CBitmap bmp;
bmp.CreateCompatibleBitmap(pDC,rect.Width(),rect.Height());
dcMem.SelectObject(bmp.m_hObject);
dcMem.FillRect(&rect,&CBrush(RGB(255,255,255)));

CFont font;
font.CreatePointFont(300,_T("黑体"));
dcMem.SelectObject(font.m_hObject);
CString str;
str.Format(_T("C++技术网www.cjjjs.com:%d"),i++);
dcMem.TextOut(0,0,str);
dcMem.TextOut(0,60,str);
dcMem.TextOut(0,120,str);
dcMem.TextOut(0,180,str);
dcMem.TextOut(0,240,str);
dcMem.TextOut(0,300,str);
dcMem.TextOut(0,360,str);

pDC->BitBlt(rect.left,rect.top,rect.Width(),rect.Height(),&dcMem,0,0,SRCCOPY);
bmp.DeleteObject();
dcMem.DeleteDC();
    总结一下,我们先创建一个内存DC来执行绘图,创建内存DC用来将绘图定向到内存。然后创建一个兼容位图,选进兼容DC中,用于存储在内存中绘制的图形数据。然后就是执行所有的GDI绘图操作,最后就是将位图数据复制到设备DC中显示。整个过程一共分为4步。因为中间绘制的过程全部是输出到内存,所以只有最后一个复制位图操作进行了设备的绘制,这样将大量的直接设备绘图处理成一次绘图,而且这样多次复制位图到设备,因为多张位图的差别很少,所以自然也就避免了闪烁。