图形设备接口
事实上,图形设备接口(Graphics Device Interface,GDI)是指这样的一个可执行程序,它处理来自Windows应用程序的图形函数调用,然后把这些调用传递给合适的设备驱动程序,由设备驱动程序来执行与硬件相关的函数并产生最后的输出结果。GDI可以看作是一个应用程序与输出设备之间的中介,一方面,GDI向应用程序提供了一个设备无关的编程环境,另一方面,它又以设备相关的格式和具体的设备打交道。
经常同图形设备接口相提并论的另一个概念是设备上下文(Device Context,DC)。设备上下文是一种Windows数据结构,它包括了与一个设备(如显示器或打印机)的绘制属性相关的信息。所有的绘制操作通过一个设备上下文对象进行,该对象封装了实现绘制线条、形状和文本的Windows API函数。设备上下文可以用来向屏幕、打印机和图元文件输出结果。
在Windows应用程序中,我们通常在绘制之前调用BeginPaint函数,然后在设备上下文中进行一系列的绘制操作,最后调用EndPaint函数结束绘制。MFC类CPaintDC封装了这一过程。在构造CPaintDC对象的同时,其构造函数自动调用BeginPaint函数;在消毁CPaintDC对象的同时,其析构函数自动调用EndPaint函数。因此前面所讲述的过程可以对应于下面的三个步骤:构造一个CDC对象,进行绘制操作,消毁该CDC对象。在基于文档/视结构的应用程序框架中,这个过程被进一步的简化。回忆前几章中讲述的内容,我们一般在视类的OnDraw成员函数中处理有关重绘的操作。通过OnPrepareDC成员函数,框架自动的向OnDraw成员函数传递一个类型为CPaintDC的设备上下文对象。我们只需简单的通过该对象进行绘制,而不需要关心这一对象的构造和消毁。这一过程由框架自动的完成,而且,隐藏在背后的设备上下文在对OnDraw的调用返回时由框架进行释放。
除了上面的CPaintDC类外,MFC还提供了其它的一些封装不同设备上下文的类。如CClientDC类,它所封装的设备上下文仅代表了一个窗口的客户区。在CClientDC的构造函数中调用的不是BeginPaint函数,而是GetDC函数;相应的,ReleaseDC函数在类CClientDC的析构函数被自动调用。与此对应的还有另一个类CWindowDC,它所封装的设备上下文代表的是整个窗口,不仅包括其客户区,也同时包括窗口的边框及其它非客户区对象。
所有的设备上下文类中比较特殊的是类CMetaFileDC,通过CMetaFileDC对象所进行的绘制操作不是对一个实在的设备来进行的,这些操作都被记录到一个Windows图元文件中。不象自动传递给OnDraw成员函数的CPaintDC对象,如果要在这种情况下使用CMetaFileDC对象的话,我们必须自己调用OnPrepareDC成员函数。
所有的这些设备上下文类都以类CDC作为其基类。
一般情况下,很多绘制操作都是在应用程序的视类的OnDraw成员函数中进行的,前面说到过,当视类窗口收到消息WM_PAINT时,该消息对应的处理函数OnPaint被调用,该处理函数构造一个CPaintDC对象,并将指向该对象的指针传递给OnDraw成员函数。这里我们考虑这样一种情况,如果我们正在编写的是一个通过鼠标在屏幕上绘图的应用程序。这时我很显然需要为鼠标的移动消息添加消息处理函数,而且,我们希望用户在移动鼠标的过程中立即就可以看到所绘制的内容,而不是等到窗口收到WM_PAINT消息(即发生重绘事件)才调用成员函数OnDraw绘制窗口。在这种情况下,我们更倾向于直接在鼠标消息处理函数中进行绘制,这时,就需要创建一个设备上下文对象,然后通过该对象调用一系列的绘制方法。
Windows本身是一个图形界面的操作系统,进行Windows程序设计随时都会同设备上下文打交道,甚至在本书前面的章节中的一些示例程序中我们也已经用到了设备上下文,只不过在当时我们回避了与设备上下文有关的很多复杂东西。本章的目的之一就是系统的讨论这些前面已经用到但没有加以阐述的概念和技巧,并补充一些尚未涉及的内容,这些内容包括:
使用设备上下文进行绘制
绘图对象
直线与曲线
填充形状
字体和文本
颜色
坐标空间及变换
这些概念往往是交织起来的,哪怕是一个很简单的绘制操作,往往都需要用到不只一个绘图对象,因此我们很难将它们人为的分割开来进行讲述。在本章上,各节的标题只代表了本节的侧重点,而对于某一个概念的叙述或使用,则有可能分散在不只一个小节中。事实上,一个应用程序是一个整体,它常常需要很多个部件共同协调工作才可以正常工作。因此,出现这种情况是很自然的。
在MFC应用程序中,绘制操作通常涉及三类对象,一类是输出对象,亦即设备上下文对象,包括CDC及其派生类;一类是绘制工具对象,亦即前面所说的图形对象,如果CFont、CBrush和CPen等;另一类属于Windows编程中需要用到的的基本数据类型,如CPoint、CSize和CRect等。
不同的设备上下文类封装了不同类型的设备上下文类对象,如表1所示。
除了设备上下文以外,在Windows中进行绘制通常还需要各种绘制工具,如用来绘制线条的笔、用来填充一个图形内部的刷子以及用来绘制文本的字体等。这些工具称作图形对象,它们由Windows系统提供,MFC提供的图形对象类对它们进行了封装,表2给出了这些图形对象以及与它们等价的Windows图形设备句柄类型。
表 1 MFC中的设备上下文类
设备上下文类 | 描述 |
CDC | 所有设备上下文类的基类。可用来直接访问整个显示器或如打印机之类的非显示设备上下文。 |
CPaintDC | 在窗口的OnPaint成员函数中使用的一种显示上下文。在其构造过程中自动调用BeginPaint,在其析构过程中自动调用EndPaint。 |
CClientDC | 代表窗口的客户区的显示上下文。通常在需要直接在窗口客户区进行绘制时使用。 |
CWindowDC | 代表整个窗口的显示上下文,包括客户区和非客户区。 |
CMetaFileDC | 代表Windows图元文件的设备上下文。一个Windows图元文件包括一系列的图形设备接口命令,可以通过重放这些命令来创建图形。向CMetaFileDC对象进行的各种绘制操作可以被记录到一个图元文件中。 |
表 2 MFC中的Windows GDI对象类
图形对象类 | 等价的Windows图形设备句柄 | 描述 |
CBrush | HBRUSH | 用来填充正在绘制的对象的内部 |
CPen | HPEN | 用来绘制对象的边线 |
CFont | HFONT | 用来绘制文本 |
CBitmap | HBITMAP | 用来提供操作位图的接口 |
CPalette | HPALETTE | 用作应用程序和色彩输出设备(如显示器)之间的接口 |
1.1 几个与图形绘制有关的简单数据类型
在讲述设备上下文和图形对象之前,我们来介绍几个常用的数据结构类。
(1) CPoint类
CPoint类封装了一个点的坐标。它事实上是从POINT结构派生而来的。结构POINT在Win32 SDK中定义。因此,CPoint也继承了POINT结构的数据成员x和y。CPoint对象可以用在任何使用POINT结构的场合。CPoint对象还可以和另一种简单数据类型CSize或SIZE结构相互进行转换。
CPoint类具有多种形式的构造函数:
CPoint( );
CPoint( int initX, int initY );
CPoint( POINT initPt );
CPoint( SIZE initSize );
CPoint( DWORD dwPoint );
当使用DWORD类型的值来构造CPoint对象时,其低位字将被赋值给CPoint对象的成员x,高位字将被赋值给成员y。
CPoint的成员函数Offset可以设置点的偏移量,同时,在类中定义的一些运算符,如?、?、??和??等大大的简化了对点坐标的各种运算和比较。
(2) CSize类
如果要表示距离以及相对位置,可以使用CSize对象。MFC类CSize事实上是从SIZE派生而来的,因此,CSize继承了SIZE结构的数据成员cx和cy。构造一个CSize对象与用对应的方法构造CPoint对象非常相似,因此我们不需讲述。同样,我们可以使用一个DWORD值来构造CSize对象,这时,其低位字被赋值给CSize对象的成员cx ,高位字被赋值给成员cy。在类Size中定义了六个运算符:?、?、??、??、??和??。
(3) CRect类
CRect类是编程时经常使用的几个简单数据结构之一,它从RECT结构派生,因此,CRect类继承了RECT结构的数据成员left、top、right和bottom。它们是CRect的公有成员。
一个CRect对象可以传递给任何以RECT结构或LPCRECT和LPRECTW指针为参数的函数。
注意:
在指定一个CRect对象时,一般情况下我们需要使它的左边界的坐标小于右边界的坐标和上边界的坐标小于下边界的坐标。我们称满足该条件的矩形为常态矩形。很多函数要求传递给它的CRect对象表示一个常态矩形,否则这些函数将有可能返回一个错误的结果。我们可以通过调用成员函数NormalizeRect来将一个非常态矩形转换为一个常态矩形。在程序中出现非常态矩形并不一定的程序员的疏忽大意。这里举一个例子,如果当前显示上下文的映射模式为MM_LOENGLISH,将一个表示常态矩形的CRect对象传递给成员函数CDC::DPtoLP,将得到一个非常态矩形,该矩形的高度将成为一个负值。这是因为在MM_LOENGLISH映射模式中,纵坐标的方向是向上的。
相比我们在前面所讲述的CPoint类和CSize类来说,类CRect要庞大得多。表列出了在类CRect中定义的成员函数。
表 3 类CRect的成员函数
成员函数 | 描述 |
Width | 计算矩形的宽度 |
Height | 计算矩形的高度 |
Size | 计算矩形的大小 |
续表3
成员函数 | 描述 |
TopLeft | 返回矩形的左上角 |
BottomRight | 返回矩形的右下角 |
CenterPoint | 返回矩形的中点 |
IsRectEmpty | 判断矩形是否为空。空的矩形的宽和高都为0 |
IsRectNull | 判断矩形的top、bottom、left和right成员变量是否全都为0 |
PtInRect | 判断指定点是否的矩形内 |
SetRect | 设置矩形的大小 |
SetRectEmpty | 将矩形设置为空(所有坐标均为0) |
CopyRect | 从源矩形中拷贝维度到矩形中 |
EqualRect | 判断两个矩形是否相等 |
InflateRect | 扩大矩形的宽和高 |
DeflateRect | 减小矩形的宽和高 |
NormalizeRect | 使用矩形的宽和高标准化 |
OffsetRect | 按指定的偏移量移动矩形 |
SubtractRect | 从一个矩形中减去另一个矩形 |
IntersectRect | 设置矩形为两个矩形的交 |
UnionRect | 设置矩形为两个矩形的并 |
LPCRECT | 转换CRect对象为LPCRECT |
LPRECT | 转换CRect对象为LPRECT |
= | 拷贝一个矩形的维度到CRect对象 |
== | 判断两个矩形的维度是否相等 |
!= | 判断两个矩形是否不等 |
+= | 将指定的偏移量添加到CRect对象或扩展CRect对象 |
-= | 从CRect对象中减去指定的偏移量或缩小CRect对象 |
&= | 设置CRect对象为CRect对象和另一矩形的交 |
|= | 设置CRect对象为CRect对象和另一矩形的并 |
+ | 将指定的偏移量添加到CRect对象或扩展CRect对象,并返回一个CRect对象 |
- | 从CRect对象减去指定的偏移量或缩小CRect对象,并返回一个CRect对象 |
续表3
成员函数 | 描述 |
& | 返回CRect对象和另一矩形的共同部分 |
| | 返回CRect对象和另一矩形的并 |
1.2 显示设备上下文
对于在视类的OnDraw成员函数中使用设备上下文进行输出的这种情况,我们已经以前面讲述文档和视时给出了一些示例,因此这里就不再重复叙述,读者可以参考前面所讲述的内容。下面我们来看一下如何自己构造设备上下文,并通过该设备上下文来进行绘制。
在示例程序Caption中,我们通过CWindowDC对象获得包括客户区和非客户区的显示设备上下文,然后将窗口的标题绘制为五彩的。
下面补充说明一下应用程序Caption的创建。由于该应用程序的结构比较简单,因此我们不打算使用AppWizard来创建框架应用程序。这里,我们先创建一个Win32 Application工程,然后添加一个C++ source file,在该源代码文件中输入上面的代码。这个过程已经在本书前面的章节中使用过,因此你应该能够很轻松的完成它。下面我们来分析这个应用程序。首先,我们在类CMyWnd中添加一个成员函数PaintTitleBar。该成员函数用来绘制窗口的标题条,其参数bActive给出了窗口的激活状态。如果当前窗口正处于激活状态,我们使用从红色到绿色再到蓝色的渐变颜色来绘制应用程序的标题条,如果当前窗口正处于非激活状态,我们使用从黑色到灰色再到黑色的渐变色来绘制标题条。
由于我们需要通过设备上下文在窗口的非客户区(这里指窗口的标题条区域)在进行绘制,所以我们选用了CWindowDC类。在类CWindowDC构造函数中自动调用了GetWindowDC函数,在其析构函数中自动调用了ReleaseDC函数。类CWindowDC的构造函数使用了一个指向CWnd对象的指针作为其参数,通过所创建的CWindowDC对象可以在窗口的非客户区进行图形输出。
在创建了类型为CWindowDC的设备上下文对象dc之后,我们调用API函数GetSystemMetrics来获得当前窗口的边框高度和宽度以及标题条的高度。这里我们指出一点,即这些度量值仅适用于具有常规样式的窗口,对于一些特殊的窗口可能不成立,如对于工具条窗口,其标题条高度要小得多。这是上面的应用程序的一个局限,但不可以对一个仅用来讲解CWindowDC对象的使用的示例过于苛求,否则我们就不得不花篇幅去介绍很多完善整个应用程序所需要的额外代码。这种对应用程序的简化的处理方法下面还会遇到。通过使用不同的参数调用GetSystemMetrics函数可以得到不同的系统度量。我们所使用的仅仅是这些度量值中的很少一部分。
如果窗口是处于激活状态,我们使用参数COLOR_ACTIVECAPTION调用API函数GetSysColor得到当前系统颜色设置中激活状态条使用的颜色(我们不能假定用户的Windows窗口在激活时都使用标准的蓝色标题条,因此用户可以很方便的使用控制面板或通过右击桌面选择“属性”来更改这些设置)。然后,我们调用在类CWindowDC的基类CDC中定义的成员函数GetPixel来获得标题条上的每一点的颜色值,如果这一点的颜色值等于当前使用的激活标题条颜色的话,就调用函数SetPixelV将该点的颜色设置为新的值。这种方式不会不正确的擦除当前标题条上的标题文本、应用程序图标以及窗口右上角最大化、最小化和关闭按钮,但是,由于在新的Windows操作系统Windows 98以及Windows NT 5.0中,标题条的颜色在默认情况下是渐变的,因此应用程序将不能正确工作。类CDC的成员函数GetPixel返回一个COLORREF值以指示位于指定坐标的点的颜色值。成员函数SetPixelV将位于指定坐标的点的颜色设置为新的值,另一个成员函数SetPixel可以完成同样的功能,并且更常用。但是,与成员函数SetPixel不同,SetPixelV不需要返回设置的实际颜色值,因此它要比SetPixel快。尽管如此,上面的程序仍只能作为一个示例程序出现,因为这种一个点一个点的描绘的方法实在是太慢,在作者的具有64M内存和K6/200的CPU的计算机上,更新一个常规大小的窗口的标题条的颜色需要大约0.3秒的时间,在这样长的时间段内,用户还是可以清楚的看到标题条一点一点绘制的过程。提高应用程序的绘制速度的一种方案是使用位图来内存中对位图进行变换和处理,然而再使用位图来更新标题条。由于位图的绘制速度要比一个点一个点的描快得多,从而有可能大幅度的提高标题条的重绘速度,但是这种算法要使用到我们在这里不打算深入讲述的一些概念和技巧,为了便于理解,我们还是采用了上面给出的算法。
下面要做的事是处理两个重要的非客户区消息,WM_NCPAINT和WM_NCACTIVATE。第一个消息WM_NCPAINT在非客户区的全部或一部分需要重绘时由操作系统发送。如果试图给窗口绘制特殊的边框或标题条,处理这个消息是必要的。MFC在CWnd中定义了该消息的默认处理函数OnNcPaint,该成员函数对WM_NCPAINT的默认处理绘制了窗口的正常边框。我们在类CMyWnd中重载了该成员函数。由于我们只是绘制了窗口的标题条,因此在此之前有必要调用一下基类中的默认实现绘制窗口的边框。然后,通过判断由API函数GetActiveWindow返回的窗口句柄(它代表了当前激活窗口)和当前窗口的句柄是否相等来以不同的参数调用PaintTitleBar成员函数来绘制窗口处于激活状态和非激活状态的标题条。
另一个必须考虑的事件是当窗口的激活状态发生改变时正确绘制窗口的标题条以反映窗口的新的激活状态。这时窗口将会收到WM_NCACTIVATE消息,该消息所带的参数给出了窗口新的激活状态。MFC在CWnd中定义了该消息的默认处理函数OnNcActivate。我们在类CMyWnd中重载了该成员函数。该函数根据窗口新的激活状态调用了PaintTitleBar来绘制新的窗口标题条以反映激活状态的改变。
整个应用程序使用了典型的MFC的结构,代码也比较简单和清晰,这里我们就不多作的介绍了。你可以根据上面的源代码清单和本书前面章节中讲述的内容来完成该应用程序。
完成上面的步骤之后我们就可以编译并试运行该应用程序了。在编译之前我们需要做一些额外的工作:
1. 单击Project菜单下的Settings命令或按下快捷键Alt+F7,打开工程设置对话框。
2. 在General选项卡中的Microsoft Foundation Classes下拉列表框中选择Use MFC in a Shared DLL或Use MFC in a Static Library (仅适用于Visual C++ 5.0的专业版和企业版)。你需要对应用程序的调试版本和发行版本各重复一次上面的设置过程。如果忽略此步设置的话,在链接应用程序的过程中会出现错误。
现在就可以编译并运行上面的程序了。
上面的程序还有其它的一些局限性。比如当窗口处于非激状态时,如果使用鼠标在窗口上移动其它窗口,窗口的标题条将会变成标准的灰色;还有,如果应用程序通过调用。要完善这些功能需要考虑更多的问题和处理更多的消息。这并不是本书在这里引入上面的示例程序的目的,我们只是为了演示一下CWindowDC对象的使用,而不是编写一个功能完善的应用程序。当然,你可以使用更好和更完善的方法来实现该应用程序并将它用于你的其它应用程序。一个特殊的标题条常常会给程序带来一些吸引人的东西,但是过分花哨的用户界面可能会使用户感到不适应甚至招至用户的反感。
使用CClientDC的应用程序与此类似,只不过我们通常在一些需要直接在窗口的客户区进行绘制的场合创建和使用该设置上下文对象。比如在一些使用鼠标绘图的应用程序中,当用户在客户区中单击鼠标时,我们通常需要直接在客户区中绘制出相应的图形,而不必等到WM_PAINT消息发送。对于这样的应用程序,我们一般在鼠标的移动和单击事件的处理函数中创建类型为CClientDC的设置上下文对象,并通过该设备上下文对象进行绘制。如果使用了AppWizard来生成MFC应用程序的话,我们一般使用ClassWizard来完成添加这些消息处理成员函数和相应的消息映射项。
如前所述,在WM_PAINT消息的处理函数中,我们一般不使用CClientDC对象,而应该使用CPaintDC对象。