终于又见面了

隔了好一阵子,终于又和大家见面了。最近我有点忙,忙得已经好几周没看过电影了,不过我喜欢这种感觉,这让我过的充实,过的问心无愧。我最近喜欢写 东西,因为每当我提笔写字或在键盘上码字时,我就能静下浮躁的心来学习思考,看不下去的书可以看得下去。我总是靠写读书笔记来迫使我自己读书,感觉这方法 不错,如果你看不下书或感到浮躁时,你可以试试。书这东西不管你喜不喜欢,还是要多读的。有句话叫什么来的?书到用时方恨少!

GDI?何方神圣?

GDI是GraphicsDevice Interface(图形设备接口)的简称,当Windows应用程序需要显示点、线、图像、文字等内容,在显示器或打印机输出这些内容时,就需要使用到 GDI。图形设备接口是Windows图形界面的基础。正如你所认为的那样,GDI是Windows非常重要的部分。当然GDI不是可以实现这种功能的唯 一程序设计接口,GDI只是其中最基本的。除了GDI外,还有GDI+、OpenGL、DirectX、WindowsImage Acquisition等可以实现类似或更高级的功能。

GDI的意义在于将程序对图形界面的操作和硬件设备隔绝开来,即支持与设备无关的图形,在程序中可以将所有的图形设备都看成是虚拟设备,包括视频显 示器和打印机等,然后通过GDI函数用同样的方法去操作它们,由Windows负责将函数调用转化成针对具体硬件的操作。只要一个设备提供了和 Windows兼容的驱动程序,它就可以被看做是一个标准的设备。GDI的出现使程序员无需要关心硬件设备及设备驱动,就可以将应用程序的输出转化为硬件 设备上的输出,实现了程序开发者与硬件设备的隔离,大大方便了开发工作。

三探GDI

从程序员的观点来看,GDI由很多个函数调用和一些相关的数据类型、宏和结构组成。不幸的是,如果要对GDI进行全面的讲述,将需要一大本书的内容。为此,我们对GDI操作可以从3个方面去简要了解——When,Where和How:

When——指的是进行图形操作的时机,究竟什么时刻最适合程序进行图形操作呢?

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

How——了解了上面两个问题后,最后还要知道“如何画”,这就涉及如何使用大部分GDI函数的问题了。

一、When——WM_PAINT消息

1、客户区的刷新

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

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

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

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

所以对于“When”这个问题,答案是:程序应该在Windows要求的时候绘制客户区,也就是在收到WM_PAINT 消息的时候。处理WM_PAINT消息要求程序员改变自己向显示器输出的思维方式,仅当“有需求”——即Windows给窗口过程发送WM_PAINT消 息时才进行绘制。如果程序在其它时间需要更新其客户区,它可以强制Windows产生一个WM_PAINT消息(例如可以通过调用 InvalidateRect等函数引发一条WM_PAINT消息)。这看来似乎是在屏幕上显示内容的一种舍近求远的方法,但是请相信,你的程序结构会从 中受益的。

2、系统何时发送WM_PAINT消息?

大多数Windows程序在WinMain中进入消息循环之前的初始化期间都要调用函数UpdateWindow。Windows利用这个机会给窗口过程发送第一个WM_PAINT消息。这个消息通知窗口过程:必须绘制客户区。

此后,窗口过程应在任何时刻都准备好处理其它WM_PAINT消息,必要的话,甚至重新绘制窗口的整个客户区。当然Windows并不是在任何情况下都发送WM_PAINT消息的,下面是几种不同的情况介绍:

i  当鼠标光标移过窗口客户区以及图标拖过客户区这两种情况,Windows总是自己保存被覆盖的区域并恢复它,并不需要发送WM_PAINT消息通知用户程序。

ii  当窗口客户区被自己的下拉式菜单覆盖,或者被自己弹出的对话框、消息框覆盖后,Windows会尝试保存被覆盖的区域并在以后恢复它,如果因为某种原因无法保存并恢复的话,Windows会发送一个WM_PAINT消息通知程序。

iii  当用户移动窗口或显示窗口,窗口中先前被隐藏的区域重新可见,比如其他的窗口覆盖程序客户区后移开或程序从最小化的状态恢复;用户改变了窗口的大小(如果 窗口类风格具有CS_HREDRAW和CS_VREDRAW设置);用户按动滚动条;程序调用UpdateWindow,InvalidateRect以 及InvalidateRgn等函数。在这些情况下,Windows会向窗口发送WM_PAINT消息。

3、无效矩形与有效矩形

尽管窗口过程一接收到WM_PAINT消息之后,就准备更新整个客户区,但它经常只需要更新一个较小的区域(最常见的是客户区中的矩形区域)。显 然,当对话框覆盖了部分客户区时,情况即是如此。在擦除对话框之后,需要重新绘制的只是先前被对话框遮住的矩形区域。这个区域称为“无效区域”或“更新区 域”。正是客户区内无效区域的存在,才会让Windows将一个WM_PAINT消息放在应用程序的消息队列中。只有在客户区的某一部分无效时,窗口才会接收WM_PAINT消息。(可见“无效”才是产生WM_PAINT消息的根本诱因)

Windows内部为每个窗口保存一个“绘图信息结构”①,这个结构包含了包围无效区域的最小矩形的坐标以及其它信息, 这个矩形就叫做“无效矩形”。如果在窗口过程处理WM_PAINT消息之前客户区中的另一个区域变为无效,则Windows计算出一个包围两个区域的新的 无效区域(以及一个新的无效矩形),并将这种变化后的信息放在上面提到的绘图信息结构中。Windows不会将多个WM_PAINT消息都放在消息队列 中。

窗口过程可以通过调用InvalidateRect使客户区内的矩形无效。如果消息队列中已经包含一个WM_PAINT消息,Windows将计算出新的无效矩形。否则,它将一个新的WM_PAINT消息放入消息队列中。

 

二、where——设备描述表(Device Context,简称DC)

解决了“When”的问题后,再考虑一下“Where”的问题。在Windows中,GDI把程序和硬件分隔出来,那么,究竟该往哪里输出图形呢——这就是“Where”的问题。答案是:通过“设备描述表”来输出图形。

什么是设备描述表?

在Windows中,所有与图形相关的操作都是用统一的方法来完成的(不然就不能称为“图形设备接口”了)。不管是绘画 屏幕上的一个窗口,还是把图形输出到打印机,或者对一幅位图进行绘制,使用的绘图函数都是相同的,为了实现方法上的统一,必须将所有的图形对象看成是一个 虚拟的设备,这些设备可能有不同的属性,如黑白打印机和彩色屏幕的颜色深度是不同的,不同打印机的尺寸和分辨率可能是不同的,绘图仪只支持矢量而不支持位 图等。不同设备的不同属性就构成了一个绘图的“环境”,这个绘图的“环境”就是Win32编程中图形操作的对象,把它叫做设备描述表②。设备描述表又称为 设备上下文,或者设备环境。

在Windows应用程序中,设备描述表与图形对象共同工作,协同完成绘图显示工作。就像画家绘画一样,设备描述表好比是画家的画布,图形对象好比是画家的画笔。用画笔在画布上绘画,不同的画笔将画出不同的画来。选择合适的绘图对象和图形对象,才能按照要求完成绘图任务。

在实际使用中,通过设备描述表可以操作的对象很广泛,除了可以是打印机或绘图仪等硬件设备外,也可以是窗口的客户区,包括大大小小的所有可以被称为窗口的按钮与控件等的客户区,也可以是一个位图。总之,任何需要用到图形操作的东西都可以通过设备描述表进行绘图。

三、How——见例子吧

大家在对以上两个问题有了初步了解后,我们就最后来看看“How”的问题吧。GDI函数还真不少,它们的具体用法估计可以写一本厚书了,当然我们也没必要涉及到每个函数。在后面我们再具体展开一些常用的GDI函数,现在嘛我们来解决一下“历史遗留问题”。

我们就先来看一个简单显示文本例子吧:

62case WM_PAINT:
63 HDC hDC;
64 PAINTSTRUCT ps;
65 hDC=BeginPaint(hwnd,&ps);
66 TextOut(hDC,0,0,"Hello World!",strlen("Hello World!"));
67 EndPaint(hwnd,&ps);
68 break;

眼熟吧,这就是第二回代码的一部分,其功能是在窗口客户区的左上角显示"HelloWorld!",这部分我一直没有讲,就是为了等到这儿再来告诉大家。

行63定义了一个设备描述表句柄变量hDC,行64就是定义一个上面曾提到的“绘图信息结构”变量,行65来把申请的设备描述表的句柄赋给变量hDC,行66 表示我们就可以在这个申请的这个“环境”上操作了,即在屏幕窗口客户去输出文本,行67释放设备描述表句柄。

通过这个例子我们可以更好的了解到设备描述表,只是“环境”而不是真正的“设备”,这个“环境”与特定的显示设备(本例中的显示器)相关。我们只是在这个“环境”上表达我们要做什么(如这个例子,我们要输出文本"Hello World!"),具体这个“环境”怎么让设备(显示器)去做,这就是系统的事了,我们就不管了,这不是简化了程序员的工作了吗。

 

再补充一点,设备描述表中的有些值是图形化的“属性”,这些属性定义了一些GDI绘图函数工作情况的特殊内容。例如,对于 TextOut(hdc,x,y,psText,iLength),设备描述表的属性确定了文本的颜色、文本的背景色、TextOut函数的 x 坐标和 y 坐标映射到窗口的客户区的方式,以及显示文本时Windows 使用的字体。其实设备描述表实际上是一个数据结构,结构中保存的就是设备的属性,当对设备描述表进行图形操作的时候,Windows可以根据这些属性找到 对应的设备进行相关的操作。

 

由此推广开来这个例子还告诉我们:当你想在一个图像输出设备(诸如屏幕或者打印机)上绘图时,你首先必须获得一个设备描 述表(或者DC)的句柄。在获取该句柄后,Windows用默认的属性值填充内部设备描述表结构。在后面文章中你会看到,可以通过调用不同的GDI函数改 变这些默认值。利用其它的GDI函数可以取得这些属性的当前值。当然,还有其它的GDI函数能够真正地绘图。接着,我们就是调用GDI函数在当前设备描述 表上绘图来完成我们的任务呀。最后绘图完毕后,必须释放设备描述表句柄。句柄被释放后就不再有效,且不能再被使用。程序必须在处理单个消息期间获取和释放 句柄。
可能你对这一系列繁琐的操作感到反感,我劝你还是忍受吧,既然你享用Windows提供的便利,就要无条件地遵守它的“规则”。

 

读到此我想“How”的问题你大概已经有所了解了,从三个方面了解GDI的任务我们已经基本完成了,可以放松一下了,我们下回再见吧。

 

①Windows为每个窗口保存一个“绘图信息结构”,这就是PAINTSTRUCT,定义如下:

typedef struct tagPAINTSTRUCT {

  HDC hdc;

  BOOL fErase;

  RECT rcPaint;

  BOOL fRestore;

  BOOL fIncUpdate;

  BYTE rgbReserved[32];

  } PAINTSTRUCT, *PPAINTSTRUCT;         

在程序调用BeginPaint(下回讲)时,Windows会适当填入该结构的各个字段值。用户程序只使用前三个字段,其它字段由Windows内部使用。

hdc字段是设备描述表句柄。

fErase字段记录Windows是否已经擦除了无效矩形的背景,在大多数情况下,如果被标志为FALSE(0),这意味着Windows已经擦 除了无效矩形的背景。(Windows使用WNDCLASS结构的hbrBackground字段指定的画刷来擦除背景,这个WNDCLASS结构是程序 在WinMain初始化期间登录窗口类时使用的。许多Windows程序使用白色画刷。以下显示了程序设定窗口类结构字段的语句:

wndcls.hbrBackground=(HBRUSH)GetStockObject(WHILE_BRUSH); 
//我们之前写过的,还记得吗       
不过,如果程序通过调用Windows函数InvalidateRect使客户区中的矩形失效,该函数的最后一个参数会指定是否擦除背景。如果这个参数为FALSE(即0),则Windows将不会擦除背景,并且在调用完BeginPaint后PAINTSTRUCT结构的fErase字段将为TRUE(非零)。)

PAINTSTRUCT结构的rcPaint字段是RECT类型的结构。

rect结构定义了一个矩形框左上角以及右下角的坐标

  typedef struct _RECT {

  LONG left;

  LONG top;

  LONG right;

  LONG bottom;

  } RECT, *PRECT;

成员  left : 指定矩形框左上角的x坐标

      top: 指定矩形框左上角的y坐标

      right: 指定矩形框右下角的x坐标

bottom:指定矩形框右下角的y坐标

PAINTSTRUCT结构的rcPaint字段定义了无效矩形的边界,如图所示。这些值均以图素为单位,并相对于显示 区域的左上角。无效矩形是应该重画的区域。PAINTSTRUCT中的rcPaint矩形不仅是无效矩形,它还是一个剪裁矩形。(绘图可以限制在客户区的 某一部分中,这就是所谓的剪裁,剪裁区域可以是矩形或非矩形)

②为方便大家对设备描述表的理解这里还有两种叙述,供大家参考。

a设备描述表是一个定义一组图形对象及其属性、影响输出的图形方式(数据)结构。windows提供设备描述表,用于应用程序和物理设备之间进行交互,从而提供了应用程序设计的平台无关性。设备描述表又称为设备上下文,或者设备环境。
设备描述表是一种数据结构,它包括了一个设备(如显示器和打印机)的绘制属性相关的信息。所有的绘制操作通过设备描述表进行。设备描述表与大多WIN32结构不同,应用程序不能直接访问设备描述表,只能由各种相关API函数通过设备描述表的句柄间接访问该结构。
设备描述表总是与某种系统硬件设备相关。比如屏幕设备描述表与显示设备相关,打印机设备描述表与打印设备相关等等。
屏幕设备描述表,一般我们简单地称其为设备描述表。它与显示设备具有一定的对应关系,在windows GDI界面下,它总是相关与某个窗口或这窗口上的某个显示区域。通常意义上窗口的设备描述表,一般指的是窗口的客户区,不包括标题栏、菜单栏所占有的区 域,而对于整个窗口来说,其设备描述表严格意义上来讲应该称为窗口设备描述表,它包含窗口的全部显示区域。二者的操作方法完全一致,所不同的仅仅是可操作 的范围不同而已。
windows 窗口一旦创建,它就自动地产生了与之相对应的设备描述表数据结构,用户可运用该结构,实现对窗口显示区域的GDI操作,如划线、写文本、绘制位图、填充等,并且所有这些操作均要通过设备描述表句柄了进行。

b 孙鑫:我们可以用一个形象的比喻来说明它的作用。现在有一个美术老师,他让

他的学生画一幅森林的图像,有的学生采用素描,有的学生采用水彩画,有的学生采用油

画,每个学生所作的图都是森林,然而表现形式却各不相同。如果让我们来画图,老师指

定了一种画法(例如用水彩画),我们就要去学习它,然后才能按照要求画出图形。如果

画法(工具)经常变换,我们就要花大量的时间和精力去学习和掌握它。在这里,画法就

相当于计算机中的图形设备及其驱动程序。我们要想作一幅图,就要掌握我们所用平台的

图形设备和它的驱动程序,调用驱动程序的接口来完成图形的显示。不同图形设备的设备

驱动程序是不一样的,对于程序员来说,要掌握各种不同的驱动程序,工作量就太大了。

因此,Windows 就给我们提供了一个设备描述表,让我们从学生的角色转变为老师的角色,只要下命令去画森林这幅图,由设备描述表 去和设备驱动程序打交道,完成图形的绘制。至于图形的效果,就要由所使用的图形设备来决定了。对于老师来说,只要画出的是森林图像就可以了。对于程序员来 说,充当老师的角色,只需要获取设备描述表的句柄,利用这个句柄去作图就可以了。

:部分内容援引自罗云彬《Windows环境下32位汇编语言程序设计》