WIN32汇编语言教程:第07章 图形操作 · 7.1 GDI原理(1)

Windows是基于图形界面的,所以在Win32编程中,图形操作是最常用的操作。GDI的意义在于将程序对图形界面的操作和硬件设备隔绝开来,在程序中可以将所有的图形设备都看成是虚拟设备,包括视频显示器和打印机等,然后通过GDI函数用同样的方法去操作它们,由Windows负责将函数调用转化成针对具体硬件的操作。只要一个设备提供了和Windows兼容的驱动程序,它就可以被看做是一个标准的设备。以前在DOS系统下写应用程序的时候,如果要进行图形操作,那么就要考虑到市场上每种显示卡的不同,否则在装配某种显卡的计算机上就可能无法正常运行,对汇编程序员来说,这真是一个恶梦。在Win32编程中,正是GDI函数让这个恶梦成为历史。

GDI函数全部包括在GDI32.DLL中,在编程的时候,注意要在源程序的开头加上相应的包含语句:

   include    gdi32.inc
   includelib gdi32.lib

和GDI相关的内容真是太庞大了,只要查看一下gdi32.inc文件就可以发现,函数的总数达到了300多个,和GDI相关的数据结构也非常多,要完全深入GDI编程,用上本书的全部篇幅可能也不够。在本章中,笔者希望通过几个例子,让读者能了解GDI的原理和基本的使用方法。

归纳起来,GDI操作可以从3个方面去了解——When,Where和How:

● When——指的是进行图形操作的时机,究竟什么时刻最适合程序进行图形操作呢?在7.1.1节“GDI程序的结构”中,将探讨这个问题。

● Where——指的是图形该往哪里画,既然Windows隔离了硬件图形设备,那么该把什么地方当做“下笔”的地方呢?7.1.2节的“设备环境”就是解答。

● How——了解了上面两个问题后,最后还要知道“如何画”,这就涉及如何使用大部分GDI函数的问题了,在本章余下来的篇幅中,将集中讨论这个问题。

7.1.1 GDI程序的结构

1. 客户区的刷新

正如上面所说的,本节讨论的是“When”的问题,读者可能会问:为什么会有这个问题,如果要向窗口输出图形,程序想在什么时候输出那就是什么时候,难道这个时刻还有规定不成?

但这个问题似乎不能这样来问,让我们来考虑这些情况:在DOS操作系统中编程的时候,程序把文字或图形输出到屏幕,在输出新的内容之前,这些内容总是保留在屏幕原处,这些内容会被意外覆盖的惟一情况是激活一个TSR程序,但TSR程序在退出之前有义务恢复原来的屏幕,如果它无法恢复屏幕的内容,那么这是它的责任,我们不会在自己的程序中去考虑屏幕内容会无缘无故消失这种情况,所以可以把屏幕看成是应用程序私有的。

如果程序输出的内容过多,如用dir显示一个含有很多文件的目录,用户根本无法看清快速上翻的屏幕,这时程序可以设计一个参数来暂停一下,如dir /p。这已经是DOS程序最“体贴”的做法了,如果用户想回过头去看已经滚出屏幕的内容,那可对不起,只能再执行一遍了!

所以对DOS程序来说,程序想在什么时候输出信息那就是什么时候,根本不存在When这个问题。

但在Windows操作系统中,屏幕是多个程序“公用”的,用户程序不要指望输出到窗口中的内容经过一段时间后还会保留在那里,它们可能被别的东西覆盖,如其他窗口、鼠标箭头或下拉的菜单等。在Windows中,恢复被覆盖内容的责任大部分属于用户程序自己,理由很简单:Windows是个多任务的操作系统,假如程序B覆盖了程序A的窗口内容,覆盖掉的内容由程序B负责恢复的话,它就必须保存它覆盖掉的内容,但是在它将保存的内容恢复之前,程序A也在运行,并可能在程序B恢复以前已经向它自己的窗口输出新的内容,结果当程序B恢复它保存的窗口内容时,保存的内容可能是过时的(而DOS的情况就不同,TSR程序激活的时候,用户程序是被挂起的),所以最好的办法就是让程序A自己来决定如何恢复。

Windows系统采用的方法是:当Windows检测到窗口被覆盖的地方需要恢复的时候,它会向用户程序发送一个WM_PAINT消息,消息中包括了需要恢复的区域,然后由用户程序来决定如何恢复被覆盖的内容。

如果程序因为忙于处理其他事务以至于无法及时响应WM_PAINT消息,那么窗口客户区原先被覆盖的地方可能会被Windows暂时画成一块白色(或者背景色)的矩形,或者根本就是保留被覆盖时的情形,直到程序有时间去响应WM_PAINT消息为止。我们常常可以看到这种情况发生在死锁程序的客户区内,这就是因为死锁的程序无法响应WM_PAINT消息来恢复客户区造成的。

所以对于“When”这个问题,答案是:程序应该在Windows要求的时候绘画客户区,也就是在收到WM_PAINT消息的时候。如果程序需要主动刷新客户区,那么可以通过调用InvalidateRect等函数引发一条WM_PAINT消息,因为在WM_PAINT消息中刷新客户区的代码是必须存在的,所以用这种看似“舍近求远”的办法实际上可以节省一份重复的代码。即使是在游戏程序这种“主动刷新”远远多于“被动刷新”的程序中,只要窗口有被其他东西覆盖的可能,那么这个原则就是适用的。

2. GDI程序的结构

对于Win32程序来说,WM_PAINT消息随时可能发生,这就意味着,程序再也不能像在DOS下一样输出结果后就不管了,反过来,程序在任何时刻都应该知道如何恢复整个或局部客户区中以前输出的内容,本着这个要求,可以按图7.1所示来安排程序结构。


图7.1 GDI程序的结构

如果程序的功能比较简单,可以采取图中左边的A程序结构,即计算及刷新整个客户区的代码全部安排在WM_PAINT消息中完成,这样,每次当客户区的全部或部分需要被更新的时候,程序重新执行整个生成客户区屏幕数据的功能模块并刷新客户区。这种结构适用于功能模块很短小且执行速度很快的情况,整个过程的时间最好不超过几百ms,否则,用户会在一个明显的等待时间后才看到程序把客户区中的“空洞”补上。考虑一个极端的情况:当程序输出的内容是经过千辛万苦才算出来的——这不是一件奇怪的事情,计算圆周率的程序就要动辄计算几个小时——那么即使客户区被别的窗口覆盖掉一点点,程序也要经过整个计算过程后才能重画客户区,而且在这个过程中,程序还没有从WM_PAINT消息返回,以至于无法处理其他消息,结果程序就会以客户区中有个空洞的难看姿势呆在屏幕上一动不动达几个小时!

当生成屏幕数据的功能模块有些复杂的时候,如刚才计算圆周率的例子,就应该考虑采用图中B程序所示的结构了。在这个程序中,功能模块和客户区刷新模块分别在不同的子程序中实现,功能模块单独用一个子程序完成,这个子程序可以由用户通过选择菜单项在WM_COMMAND消息中执行,也可以新建另外一个线程来完成,总之,它最后把计算结果放到一个缓冲区中,而每当客户区需要刷新时,程序在WM_PAINT消息中调用客户区刷新子程序,这个子程序从计算好的缓冲区中取出数据并输出到客户区中,由于单纯的屏幕刷新过程是很快的,所以用户根本来不及看到客户区中的空洞。

在本章后面的内容中有两个时钟的例子:Clock.exe和BmpClock.exe,前面一个例子采用的是A结构,后面一个例子采用的是B结构,读者在阅读的时候可以比较一下它们在结构上的不同。

上页:第06章 定时器 · 6.3 取Windows时间 下页:第07章 图形操作 · 7.1 GDI原理(2)

第07章 图形操作

版权所有 © 中山市飞娥软件工作室 证书:粤ICP备09170368号