事实上,图形设备接口(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对象。
在下面的小节中,我们将讲述MFC中的GDI绘图对象类的使用。
第二节 画笔对象
MFC类CPen封装了GDI中的画笔对象,画笔对象代表了进行绘制时所用的线条。我们一般通过两个步骤来创建画笔对象:首先构造一个CPen对象,再调用对象的CreatePen成员函数。成员函数CreatePen按指定的样式、宽度等属性创建一个逻辑画笔,然后将该画笔与CPen对象相关联。
2.1 创建画笔
成员函数CreatePen有两种形式。第一种形式的如下:
BOOL CreatePen( int nPenStyle, int nWidth, COLORREF crColor );
参数nPenStyle代表了画笔的样式,可以为下列值之一:
PS_SOLID:
创建一个实线画笔
PS_DASH:
创建一个虚线画笔。一个虚线画笔的宽度不能超过一个设备单位。
PS_DOT:
创建一个点线画笔。一个点划线画笔的宽度不能超过一个设备单位。
PS_DASHDOT:
创建一个点划线画笔。同样,这种样式的画笔宽度也不能超过一个设备单位。
PS_DASHDOTDOT:
创建一个双点划线画笔。这种样式的画笔宽度也不能超过一个设备单位
PS_NULL:
创建一个空画笔
PS_INSIDEFRAME:
对于那些指定一个边界矩形的GDI输出函数,具有这种样式的画笔将线条绘制到输出形状框架的内侧。而对于那些没有指定边界矩形的GDI输出函数,这种画笔的绘制区域则不受框架的限制。
参数nWidth以逻辑单位给出画笔的宽度。如果参数nWidth为零,则无论当前使用何种映射模式,所创建和画笔的宽度都为一个象素。参数crColor为画笔的颜色,这里可以使用RGB宏来生成合适的颜色值。
另一种形式的CreatePen函数使用如下的参数:
BOOL CreatePen( int nPenStyle, int nWidth, const LOGBRUSH* pLogBrush,
int nStyleCount = 0, const DWORD* lpStyle = NULL );
这种形式的CreatePen可以创建一个具有指定的宽度、样式和刷子属性的逻辑修饰(cosmetic)或几何(geometric)画笔。参数nPenStyle指定了画笔的样式,它可以为PS_COSMETIC、PS_GEOMETRIC或它们与一些附加属性的组合,详细的说明这里不进行说明,如果需要的话你可以参考联机文档中对构造函数CPen::CPen的说明。nWidth以逻辑单位指定画笔的宽度,如果nPenStyle参数包括了PS_COSMETIC的话,参数nWidth必须为1。参数pLogBrush为指向一个LOGBRUSH结构的指针,该LOGBRUSH结构定义了画笔的刷子属性。最末两个参数nStyleCount和lpStyle定义了画笔的每一划及它们之间的空白的长度。
画笔对象实际上也可以一步创建,这时所使用的构造函数也使用与函数CreatePen相一致的参数。以使用一步创建的方式创建画笔对象时,我们通过捕获一个异常是否发生来判断是否出错。
2.2 使用画笔在设备上下文中进行输出
一旦画笔对象创建成功之后,即可使用CDC类的成员函数SelectObject将其选入设备描述表中进行各种输出。
示例程序Pen演示了画笔对象的使用。
创建工程Pen的方法同在1.2中引入的示例程序Caption相同。您可以参照上一节的讲述来创建工程上面的代码创建了八个画笔对象,每一个画笔对象对应了一种不同的画笔样式。使用这些不同的画笔样式进行输出的结果如图3所示。
注意第二排的第二个矩形,由于所使用的画笔具有PS_NULL,所以实际上什么也没有绘制。再看最后一个矩形,具有PS_GEOMETRIC样式的几何画笔可以具有刷子属性,这样,使用画笔绘制出来的线条就可以不只是单调实心图案或各种虚线和点划线了。
最后需要提醒的是,各种绘图工具对象是有限的系统资源,因此用过之后最好记得使用DelectObject释放为该GDI对象分配的系统资源。此外,在完成绘制工作之后,我们应该恢复设备描述表中的原有GDI对象。体现在上面的示例程序中,我们使用了一个CPen类型的指针来保存原有的GDI对象。类CDC的成员函数是将特定的GDI对象选入设备上下文的同时还返回了设备上下文中的原有对象,以便在以后进行恢复。
注意:
不能删除正被选入设备上下文中的Windows GDI对象。并且,删除Windows GDI对象也不等于删除相关联的C++对象。以上面的例子为例,调用CPen对象的DelectObject (该成员函数在基类CGdiObject中定义)只是删除与之相关联的Windows画笔对象所占用的系统资源,C++语义上的CPen对象并没有被删除。你还可以调用其成员函数CreatePen创建一个新的画笔对象并将它们与CPen对象相关联。在Windows编程中,我们所说的“对象”一词可能指实际的C++对象,也可能指各种Windows对象,在很多情况下两者之间有着密切的联系,但要注意它们并不是同义语。读者可以根据上下文来判断“对象”一词的真正含义。
2.3 图形输出函数
表4给出了在CDC在类中定义的一些图形输出函数。
表 4 在类CDC中定义的图形输出函数
成员函数
描述
GetCurrentPosition
获得当前画笔位置的逻辑坐标
MoveTo
移动当前位置
LineTo
从当前位置向指定绘制一条不包括终点的直线
Arc
绘制一个椭圆弧
ArcTo
绘制一个椭圆弧,该函数与Arc类似,但当前位置会被更新
AngleArc
绘制一条线段和一个椭圆弧,并将当前位置移到椭圆弧的终点
GetArcDirection
返回设备上下文上当前的画弧方向
续表4
成员函数
描述
SetArcDirection
设置当前弧和矩形函数的绘图方向
PolyDraw
绘制一系列直线段和贝塞尔(Bézier)曲线段并更新当前位置
Polyline
绘制连接指定点的一系列直线段
PolyPolyline
绘制多系列的相连直线段,当前位置既不被使用也不被更新
PolylineTo
绘制一条或多条直线,并移动当前位置到最末一条线的终点
PolyBezier
绘制一条或多条贝塞尔曲线,当前位置既不被使用也不被更新
PolyBezierTo
绘制一条或多条贝塞尔曲线,并将当前位置移动到最后一条曲线的末端
FillRect
用指定的刷子填充给定的矩形
FrameRect
绘制矩形的边框
InvertRect
反转矩形的内容
DrawIcon
绘制一个图标
DrawDragRect
在矩形区域被拖动时擦除并重绘它
FillSolidRect
以原色填充一个矩形区域
Draw3dRect
填充一个三维矩形区域
DrawEdge
绘制矩形的边界
DrawFrameControl
绘制一个框架控件
DrawState
显示一幅图象并对图象应用表示其状态的可视效果
Chord
绘制一个“弓形”,一个“弓形”是由一个椭圆弧和一条线段所围成的区域
DrawFocusRect
绘制一个矩形以用来表示其焦点
成员函数
描述
Ellipse
绘制一个椭圆
Pie
绘制一个饼块
Polygon
绘制一个由多条线段连接而成的多边形
PolyPolygon
绘制一个或多个以当前多边形填充模式填充的多边形。这些多边形可能互不相交,也可能互相覆盖
Polyline
绘制一个包括一系列连接指定点的线段的多边形
Rectangle
使用当前笔和刷子绘制并填充一个矩形
RoundRect
使用当前笔和刷子绘制并填充一个圆角矩形
示例程序Draw演示了表4中的一些绘图函数的使用和输出效果。同本章中前面的应用程序一样,示例程序Draw使用了MFC的应用程序框架,但我们没有使用AppWizard来生成它。程序中还涉及了一些我们目前还未作系统阐述的内容,在现阶段并不要求读者理解这些内容,尽管实际上它们并不复杂。
第三节 刷子对象
刷子对象用来在GDI输出时填充一个封闭图形的内部,它事实上定义一个8像素×8像素大小的位图。在绘制时,Windows将多个这样的位图平铺起来填充封闭图形的内部。MFC类CBrush封装了标准的Windows刷子对象。在创建刷子时,我们通常先定义一个CBrush对象,然后调用CreateSolidBrush、CreateaHatchBrush或者CreatePatternBrush之一来定义该刷子对象的属性。CBrush对象可以用作任何使用HBRUSH句柄的函数的参数。
以创建一个刷子之后,我们可以使用CDC类的成员函数SelectObject将它选入当前设备上下文作为当前绘图输出所使用的刷子。
成员函数CreateSolidBrush创建一个原色刷子,它仅带有一个类型为COLORREF的参数,该参数指定了刷子所使用的RGB颜色值。
成员函数CreateHatchBrush创建一个阴影刷子,其原型如下:
BOOL CreateHatchBrush( int nIndex, COLORREF crColor );
参数nIndex指定了刷子的样式,它可以为以下常量之一:
HS_BDIAGONAL:
由左向右下斜45度的阴影线
HS_CROSS:
水平和垂直的交叉线
HS_DIAGCROSS:
45度的斜交叉线
HS_FDIAGONAL:
由左向右上斜45度的阴影线
HS_VERTICAL:
垂直阴影线
参数crColor指定和阴影线所使用的前景色。
成员函数CreatePatternBrush以一个指向CBitmap对象的指针为参数,它使用该CBitmap所代表的位图的左上角8像素×8像素的区域来创建一个图案刷子。
注意:
一个刷子所使用的图案的大小总是8像素´ 8像素大小。即使提供给成员函数CreatePatternBrush的位图大于这个大小,也仅有左上角的8像素´ 8像素被使用。
示例程序Brush演示了各种刷子的使用,在工程包括了两个位图资源IDB_BRUSH1和IDB_BRUSH2。
第四节 字体对象
MFC类CFont封装了Windows图形设备接口中的字体对象。字体对象决定的设备上下文中进行文本输出的字符样式。在使用字体对象的时候,我们一般先创建一个CFont对象,然后调用CreateFont、CreateFontIndirect、CreatePointFont及CreatePointFontIndirect之一的成员函数来对该字体对象进行初始化。
4.1 创建字体对象
创建字体对象的最方便的方法是使用CreatePointFont函数,CreatePointFont函数仅需三个参数,其原型如下:
BOOL CreatePointFont( int nPointSize, LPCTSTR lpszFaceName, CDC* pDC = NULL );
第一个参数nPointSize以十分之一磅为单位设置字体的大小,磅是印刷行业中的常用度是单位,1磅=1/72英寸≈0.03528厘米。磅这个单位在涉及图形和文本输出的Windows应用程序中被大量的使用,因此我们应该熟知它和其它常用度量单位之间的换算关系。在后面的部分中我们还会讨论到在Windows编程中还会使用到的其它度量单位以及它们之间的换算关系。
参数lpszFaceName指定了创建字体对象所使用的字体名,pDC指向一个设备上下文对象,函数CreatePointFont将以磅表示的字体大小转换为pDC所指向的设备上下文中相应的逻辑单位。如果指针pDC为空,函数CreatePointFont将字体大小以设备单位表示。
4.2 LOGFONT结构
在Windows内部,字体是以一个名为LOGFONT的结构来表示的。结构LOGFONT的定义如下:
typedef struct tagLOGFONT { // lf
LONG lfHeight;
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;
各成员的含义如下:
lfHeight:
以逻辑单位指定字体字符元(character cell)或字符的高度。字符高度值为字符元高度值减去内部行距(internal-leading)值。当lfHeight大于0时,字体映射程序将该值转换为设备单位,并将它与可用字体的字符元高度进行匹配;当该参数为0时,字体映射程度将使用一个匹配的默认高度值;如果参数的值小于0,则将其转换为设备单位,对于任何一种情况,字体映射程度最终得到的字体高度值不会超过所指定的值。以MM_TEXT映射模式下,字体高度值和磅值有如下的换算公式:
lfHeight=-MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);
lfWidth:
以逻辑单位指定字体字符的平均宽度。如果lfWidth的值为0,则根据设备的纵横比从可用字体的数字转换纵横中选取最接近的匹配值,该值通过比较两者之间的差异的绝对值得出。
lfEscapement:
以十分之一度为单位指定每一行文本输出时相对于页面底端的角度。
ifOrientation:
以十分之一度为单位指定字符基线相对于页面底端的角度。
lfWeight:
指定字体重量。在Windows中,字体重量这个术语用来指代字体的粗细程度。lfWeight的范围为0到1000,正常情况下的字体重量为400,粗体为700。如果lfWeight为0,则使用默认的字体重量。
lfItalic:
当lfItalic为TRUE时使用斜体
lfUnderline:
当lfUnderline为TRUE时给字体添加下划线
lfStrikeOut:
当lfStrikeOut为TRUE时给字体添加删除线
lfCharSet:
指定字符集。可以使用下面的预定义值:
ANSI_CHARSET
OEM_CHARSET
SYMBOL_CHARSET
UNICODE_CHARSET
其中OEM字符集是与操作系统相关的。
lfOutPrecision:
指定输出精度。输出精度定义了输出与所要求的字体高度、宽度、字符方向等的接近程度。它可以为下面的值之一:
OUT_CHARACTER_PRECIS
OUT_DEFAULT_PRECIS
OUT_STRING_PRECIS
OUT_STROKE_PRECIS
lfClipPrecision:
指定剪辑精度。剪辑精度定义了当字符的一部分超过剪辑区域时对字符的剪辑方式,它可以为下列值之一:
CLIP_CHARACTER_PRECIS
CLIP_DEFAULT_PRECIS
CLIP_STROKE_PRECIS
lfQuality:
定义输出质量。输出质量定义了图形设备接口在匹配逻辑字体属性到实际的物理字体的所使用的方式,它可以为下列值之一:
DEFAULT_QUALITY (默认质量)
DRAFT_QUALITY (草稿质量)
PROOF_QUALITY (正稿质量)
lfPitchAndFamily:
指定字体的字符间距和族。最低两位指定字体的字符间距为以下值之一:
DEFAULT_PITCH
FIXED_PITCH
VARIABLE_PITCH
第4到7位指定字体族为以下值之一:
FF_DECORATIVE
FF_DONTCARE
FF_MODERN
FF_ROMAN
FF_SCRIPT
FF_SWISS
这些值的具体含义可以参考Visual C++中关于结构LOGFONT的文档。
字符间距和字体族可以使用逻辑或(OR)运算符来进行组合。
lfFaceName:
一个指定以NULL结尾的字符串的指针,它指定的所用的字体名。该字符串的长度不得超过32个字符,如果lfFaceName为NULL,图形设备接口将使用默认的字体名。
4.3 使用字体对象和枚举系统中的所有字体
示例程序Font演示了LOGFONT结构和CFont对象的使用。此外,在该示例程序中,我们还演示了如何获得当前系统中已安装的所有可用字体,这些信息是通过API函数EnumFontFamilies和自定义的字体枚举回调函数EnumFontFamProc来得到,并放入程序主窗口内的列表框中的。
我们先来看重绘消息处理函数OnPaint,一开始时,我们调用了CDC类的成员函数GetCurrentFont,该成员函数返回当前设备上下文所使用的字体,其返回值是一个指向CFont对象的指针,然后,我们通过该指针调用CFont类的成员函数GetLogFont,该成员函数将字体的信息填入到一个LOGFONT结构中。在下面的步骤中,我们通过修改该结构的成员来创建新的字体对象。首先,我们创建一个CFont对象font1,font1使用了12个象素大小的宋体字,在程序中它主要用来输出一些标识文本。这里,我们先在LOGFONT结构对象lf在相关成员中填入新的值,再以该结构对象为参数来调用CFont类的成员函数CreateFontIndirect创建相应的GDI字体对象。这里我们将lfCharSet成员修改为134 ,这个值可以通过本程序的运行结果得出,lfWidth成员修改为0,这样将使用默认的字符纵横比得到字符的宽度。
在下面过程中,我选择20个象素的Times New Roman字体,与刚才不同的是,我们将结构对象lf的lfEscapement成员的值设置为400,这样,文本将以向上倾斜40度(lfEscapement的值的单位为1/10度)的角度进行输出。接着,我们将该字体对象通过CDC的成员函数SelectObject选入设备上下文中作为设备上下文的当前字体。CDC类的成员函数GetTextExtent可以在输出一个字符串之前得到该输出字符串的大小,以便于我们可以恰当的安排字符串的输出位置。需要注意的是,通过该成员函数得到的度量值不会受到我们在lfEscapement中设置的值的影响。在程序示例中,我们通过成员函数GetTextExtent得到字符串"lfEscapement= 400"在输出时的长度和宽度,然后根据所得的结果计算得出以40度角输出文本串的合适的起始位置,最后调用CDC类的成员函数TextOut以当前字体输出字符串"lfEscapement= 400"。
注意:
不管当前的lfEscapement值如何,函数TextOut总是以输出字符串的第一个字符的起始位置的坐标作为其前两个参数。
在Windows 95中,lfEscapement和lfOrientation总是具有相同的值,而在Windows NT中,两者在某些情况下可以不相同。
设置lfFaceName时应该使用库函数strcpy,不要犯这样的错误:
lfFaceName="宋体";
此外,如果为lfFaceName设置了新值,同时也应该将lfCharSet的值设置为相匹配的字符集。如果字符集与字体名不相匹配,将会导致设置不起作用。
要使文本向下倾斜输出,我们只需简单的将lfEscapement设置为负值。如下面的代码所示:
// 将字体输出方向更改为向下倾斜 40 度
lf.lfEscapement=-400;
dc.SelectObject(pOldFont);
font2.DeleteObject();
font2.CreateFontIndirect(&lf);
dc.SelectObject(&font2);
// 计算字符串合适的输出位置
dc.TextOut(290-int(sz.cx*cos(DegToRnd(40))), 10, "lfEscapement=-400");
上面的代码摘自应用程序Font。
注意:
在调用font2的CreateFontIndirect成员函数创建新的字体对象之前,应该先调用其成员函数DeleteObject删除该字体对象,而当一个GDI图形对象正为设备上下文所使用时,我们不能删除该图形对象,因此在前面的代码中,我们在删除在font2原有的字体对象之前先将设备上下文的字体对象进行复原。
接着在消息处理函数CMyWnd::OnPaint中,我们又将lfEscapement成员的值设置为0,输出字符串"lfEscapement=0"以示对比。
在随后的代码中,我们演示了lfWeight成员的不同值对字体的笔划粗细的影响。我们先将lfWeight值设置为0,以50象素的宋体字绘制了一个“细”字,然后再将lfWeight值设置为1000,以同样大小和同种字体绘制了一个“粗”字。通过如图7的输出结果,我们看到字体笔划的粗细发生的明显的变化。
在OnPaint函数返回之前,不要忘记恢复设备上下文的原有字体对象,指向该对象的CFont指针在前面被保存到了名为pOldFont的指针变量中。我们仍然使用SelectObject将其选入当前设备上下文。
上面我们来看示例程序Font的另一个主要的功能板块,即枚举当前系统中所安装的所有字符并将它添加到一个列表框中。
首先我们在窗口CMyWnd的WM_CREATE消息的处理函数OnCreate中调用CListBox对象lst (该对象被定义为类CMyWnd的成员变量)的Create成员函数。在Create成员函数中,我们指定了列表框的样式包括了LBS_USETABSTOPS,该样式允许在列表项中使用制表符,这些制表符在显示时会被扩展到指定的位置。
接着,我们将列表框所使用的字体设置为9磅大小的宋体字。这里我们调用的是CFont对象的CreatePointFont成员函数来创建字体。当需要创建的指定磅值大小的某种字体时,使用CreatePointFont成员函数要方便得多,因此该函数仅需要三个参数,并且,第三个参数在很多情况下可以省略。这样,我们就可以避开填写复杂的LOGFONT结构。
在改变列表框字体的同时,我们将列表框中的当前制表位设置为200个对话框单位,对话框单位是一种在控件和对话框使用的度量单位。每4个水平对话框单位等于以系统字体显示的字符的平均宽度,我们还将这个宽度称作对话框基本单位。对话框基本单位的具体量值可以通过API函数GetDialogBaseUnits得到,该函数返回值的低位字代表对话框水平基本单位,高位字代表对话框垂直基本单位。
紧接着我们调用了API函数EnumFontFamilies来枚举系统中的所有可用字体,该函数使用4个参数,第一个参数为枚举所使用设备上下文句柄,我们使用API函数GetDC来得到代表当前窗口的客户区;为了枚举系统中的所有字体,我们将第二个参数设置为NULL;第三个参数为枚举字体回调函数,这里为EnumFontFamProc,我们将在下面定义该回调函数;第四个参数为指向列表框的指针,这个参数将被传递给回调函数,由于我们需要将可用的字体添加到列表框中,因此我们可以很自然的将指向该列表框指针当前应用程序提供的参数传递给枚举字体回调函数。
枚举字体回调函数的原型在程序中被声明为
int WINAPI EnumFontFamProc(const LOGFONTA *lplf,
const TEXTMETRICA *lptm, unsigned long FontType, LPARAM lParam);
注意:
在随Visual C++ 提供的Platform SDK (即Win32 SDK)中所给出的EnumFontFamProc的原型为
int CALLBACK EnumFontFamProc( ENUMLOGFONT FAR *lpelf, // pointer to logical-font data
NEWTEXTMETRIC FAR *lpntm, // pointer to physical-font data
int FontType, // type of font
LPARAM lParam // address of application-defined data
);
然而在本程序中使用上面所给的原型将会在编译时导致类型不匹配。正确的回调函数的原型应该如代码清单中所给的那样。
在本程序中,传递给回调函数的第一个参数lplf为该字体对应的LOGFONTA结构,最后一个参数为指向列表框的CListBox指针。第二个参数和第三个参数在本程序中没有使用。在本程序中,枚举字体回调函数的结构很简单,它只是将字体的字体名和相应的字符集格式化之后添加到列表框中。为了使枚举继续进行,回调函数应该返回真值,如果回调函数返回了FALSE,则枚举的过程将被终止。
示例程序Font的运行结果如图7所示。
在类CDC中定义的字体和文本函数如表所示。
表 5 在类CDC中定义的字体和文本函数
成员函数
描述
TextOut
在指定位置以当前选定字体绘制字符串
ExtTextOut
在指定的矩形区域内使用当前选定字体绘制字符串
TabbedTextOut
以指定的位置绘制字符串,并按指定的制表符位置扩展字符串的制表符
DrawText
在指定的矩形区域内绘制格式化文本
GetTextExtent
使用当前字体中属性设备上下文中计算一行文本的宽度和高度
GetOutputTextExtent
在输出设备上下文中计算一字符串的宽度和高度
GetTabbedTextExtent
在属性设备上下文中计算一字符串的宽度和高度
GetOutputTabbedTextExtent
在输出设备上下文中计算一字符串的宽度和高度
GrayString
在指定位置绘制变灰的文本
GetTextAlign
获得文本对齐标志
SetTextAlign
设置文本对齐标志
GetTextFace
将当前字体的字体名拷贝到缓冲区
GetTextMetrics
从属性设备上下文中获得当前字体的度量值
GetOutputTextMetrics
从输出设备上下文中获得当前字体的度量值
SetTextJustification
在字符串的分隔字符处添加空白
GetTextCharacterExtra
获得字符间空白的当前设置
SetTextCharacterExtra
设置字符间空白的当前设置
GetFontData
从可缩放字体文件中获取字体信息。所获取的信息通过指定字体文件中的偏移量和返回信息的长度来确定
GetKerningPairs
在选定的设备上下文中获得当前选定字体的字距调整字符对
GetOutlineTextMetrics
获得TrueType字体的字体度量信息
GetGlyphOutline
返回当前字体的字符的轮廓曲线或位图
GetCharABCWidths
从当前字体中以逻辑单位返回给定范围的连续字符的宽度
GetCharWidth
从当前字体中返回给定范围的连续字符的相对宽度
GetOutputCharWidth
从输出设备上下文中的当前字体返回连续字符组中若干单个字符的宽度
续表5
成员函数
描述
SetMapperFlags
改变字体映射程序中从逻辑字符到物理字体的映射过程中所使用的算法
GetAspectRatioFilter
获得当前纵横比过滤器的设定
在一些应用程序(如字处理应用程序)中,我们一般需要由用户来指定所使用的字体。这时常使用的方法是弹出一个字体对话框,用户通过该字体对话框来设置应用程序所使用的字体。MFC类CFontDialog封装了标准的Windows字体对话框。在最简单的情况下,我们只需要声明一个类的实例对象CFontDialog,然后通过该对象调用类CFongDialog的成员函数DoModal,如果该成员函数返回IDOK,则通过成员函数GetCurrentFont将用户所选择的字体信息填入一个LOGFONT结构中,在下面的过程中即可通过该结构创建CFont对象。在很多情况下,我们需要为字体对话框设置一些初始值,一种很简单的方式在其构造函数中传递一个指向LOGFONT结构对象的指针。我们可以在创建CFontDialog对象之后,调用DoModal成员之前改变其类型为CHOOSEFONT的成员结构m_cf的各成员的值来为字体对话框进行初始设置。
4.4 创建特殊的字体效果
在一般的应用程序中,我们可以使用SetBkMode和SetBkColor来设置绘制文本所使用的颜色和模式,但是,这两个函数所设置的效果是很有限的。有时候我们可能希望得到一些特殊的文本输出效果。这时我们就应该考虑其它特殊的实现方式。使用路径是其中的一种方法。下面我们讲述一些使用路径得到的特殊的字体效果。
(1) 空心字
在开始一个路径前,我们先调用CDC类的成员函数BeginPath,然后调用一系列的输出函数,在完成绘制之后,我们可以调用CDC类的成员函数EndPath。在完成一个路径之后,我们可以调用StrokePath来绘制该路径。参例EmptyFont.
(2) 渐变字
在完成一个路径之后,如前所述,我们可以调用CDC类的成员函数FillPath、StrokePath或StrokeAndFillPath来绘制和填充路径。这只是最初级的技巧。更进一步,我们可以使用成员函数SelectClipPath将路径选入当前剪辑区域,这样,所有的绘制操作都将只作用于这个剪辑区域。
使用此技巧可以绘制具有渐变颜色效果的字体。在示例中,我们以RGN_COPY方式将路径选作当前剪辑区域,然后,在该剪辑区域上进行一系列的绘制操作,这些绘制操作以不同的颜色绘制了一系列的同心椭圆,这些同心椭圆有视觉上给用户以渐变的感觉。参例VarFont.
(3) 使用不同的光栅模式创建特殊效果
光栅模式决定了画笔的颜色和屏幕上原有的点的颜色值在进行绘制时的组合方式。可以使用类CDC的成员函数SetROP2来设置设备上下文所使用的光栅绘制模式。该操作仅对光栅设备起作用。通过指定不同的光栅模式,我们可以制造一些特殊的效果,比如说反色字等。
例ModeFont演示了16种不同的光栅模式,需要注意的是,即使是同一种光栅模式,在当前画笔的颜色不同时所产生的结果也可能有很大差异。
这里再强调一下,除了R2_BLACK,R2_WHITE,R2_NOP,R2_NOT以外,其作光栅模式的效果都同当前画笔的颜色有关。比如在上面的示例中,如果当前画笔(刷子)的颜色值为背景方格相同,即都为蓝色,那么光栅模式R2_MERGEPENNOT和R2_NOTXORPEN都将屏幕上的蓝色区域绘制为白色,白色区域绘制为蓝色。但是,如果当前画笔(刷子)的颜色不同蓝色,则得到的结果则可能不是这样。
各种不同的光栅模式实际上是将背景颜色和画笔或刷子的颜色进行某种位运算来得到实际输出的颜色值,关于每一种光栅模式的具体含义可以参考Visual C++中关于函数CDC::SetROP2和SetROP2的联机文档。
第五节 映射模式
作为本章的末尾,我们来讲述一个以前一直没有涉及的问题。这就是在设备上下文中绘制时所使用的坐标系统。在前面的示例中,我们在绘图时使用了如图1所示的坐标系统。
图 1 MM_TEXT映射模式
这种映射模式称作MM_TEXT映射模式。在这样的坐标系统中,向右的方向为x轴的正方向,向下的方向为y轴的正方向?N蘼凼莤方向还是y方向,每一个单位长度都代表设备上的一个象素。这种映射模式对于很多绘制操作是合理的,由于它以象素为单位,所以我们可以很直观的控制图形在屏幕上输出的大小。但是,其缺陷也是明显的。考虑不同的设备,如屏幕和打印机,它们所对应的象素的大小是不同的,因此,在不同的设备上输出的相同象素大小的图形,其实际大小可能有很大的差异。
为了解决这个问题,Windows提供了设备无关的映射模式。这些模式使用了诸如1/1000英寸、1/100毫米之类的单位长度。在不同的设备上进行输出的时候,Windows自动将逻辑单位换算成对应的设备单位。这样,你在屏幕上输出的1厘米的正方形和在打印机上输出的1厘米的正方形,它们的实际大小的一样的,尽管由于设备在分辨率上的差异,它们所包括的象素的数目可能不一样。
5.1 预定义的映射模式
对当前映射模式的操作是在设备上下文中进行的,因此,MFC在CDC类中提供了成员函数SetMapMode来改变当前所使用的映射模式。
函数SetMapMode使用一个整型量作为其参数,该参数可以取以下的常量之一:
MM_ANISOTROPIC:
使用自定义的映射模式,这种映射模式在x方向和y方向均使用自定义的单位长度,并且两个方向上的单位长度可以不一样。如果将映射模式设置为MM_ANISOTROPIC,需要调用CDC类的成员函数SetWindowExt和SetViewportExt来设置其单位长度、坐标轴的方向等
MM_HIENGLISH:
以0.001英寸为逻辑单位长度、向右的方向为x轴正方向,向上的方向为y轴正方向
MM_HIMETRIC:
以0.01毫米为逻辑单位长度、向右的方向为x轴正方向,向上的方向为y轴正方向
MM_ISOTROPIC:
使用自定义的映射模式,这种映射模式在x方向和y方向上使用相同的单位长度。如果将映射模式设置为MM_ISOTROPIC,需要调用CDC类的成员函数SetWindowExt和SetViewportExt来设置其单位长度、坐标轴的方向等
MM_LOENGLISH:
以0.01英寸为逻辑单位长度、向右的方向为x轴正方向,向上的方向为y轴正方向
MM_LOMETRIC:
以0.1毫米为逻辑单位长度、向右的方向为x轴正方向,向上的方向为y轴正方向
MM_TEXT:
以1设备象素为逻辑单位长度、向右的方向为x轴正方向,向下的方向为y轴正方向
MM_TWIPS:
以1/20磅(每一磅为1/72英寸)为逻辑单位长度、向右的方向为x正方向,向上的方向为y轴正方向
在上面的映射模式中,除了MM_ANISOTROPIC、MM_ISOTROPIC和MM_TEXT以外,其它映射模式都使用向上的方向为y轴的正方向,而设备上下文的默认映射模式为MM_TEXT,它在向下的方向为y轴的正方向,因此,如果我们在程序中将映射模式更改为其它的映射模式,需要注意应该随y轴的正方向的不同而更改图形输出函数所使用的坐标值的正负。
SetMapMode将映射模式设置为指定的映射模式,同时返回原有的映射模式。
范例Map。该工具在不同的逻辑单位之间进行转换。需要注意的是,这种转换有两种不同的方式,即逻辑的还是物理的。如果当前映射模式为MM_TEXT,这时一英寸所对应的象素值大小是一定的,我们称这种映射方式的逻辑的;如果当前映射模式为MM_HIENGLISH、MM_LOENGLISH等,则屏幕上的一英寸对应的象素值依赖于屏幕的实际分辨率,在这种模式下,800´ 600的屏幕分辨率下一英寸所对应的象素值要比在640´ 480的屏幕分辨率多。工具Map考虑到了这种差异,允许用户指定转换是基于逻辑英寸进行还是基于物理英寸进行。对于实际的转换过程,则通过调用CDC类的成员函数DPtoHIMETRIC来实现的,该成员函数将设备坐标值转换为相应的HIMETRIC度量。
由于使用的方便考虑,我们还在工具Map中添加了其它几种度量单位的转换,这些度量单位包括:磅(point)、英寸(inch)、厘米(centimeter)、水平对话框单位(horizontal dialog units)和垂直对话框单位(vertical dialog units)。这里需要说明的是水平对话框单位和垂直对话框单位。这两种度量单位在对话框模板中用于对话框和控件的度量,此外,在编辑控件和列表框以及组合框控件设置制表位的函数SetTabStops也使用对话框单位。水平对话框单位等于当前系统字体的半角字符的平均宽度的1/4,而垂直对话框单位则等于当前系统字体的字符的高度的1/8。API函数GetDialogBaseUnits返回了当前系统所使用的对话框基本单位,由此可以导出当前使用的水平对话框单位和垂直对话框单位。但是要注意的是,实际使?玫亩曰翱虻ノ灰览涤诘鼻岸曰翱蛩褂玫淖痔濉Dialog类的成员函数MapDialogRect可以将一个以对话框单位表示的矩形转换为相应的屏幕象素单位。在工具Map,对话框单位是通过函数GetDialogBaseUnits的返回值计算得到的,也就是说,该单位是基于默认的系统字体,而不是对话框实际所选用的字体的。
5.2 自定义的坐标系统
在很多情况下使用系统预定义的映射系统非常的恰当,但并非总是这样。假设我们需要编写一个绘制函数图形的应用程序,把整个屏幕映射为一个平面直角坐标系会更加的恰当。在本节的后面我们举了一个这样的例子。
前面提到过,使用下面的函数调用可以自定义设备上下文对象dc的坐标映射。
dc.SetMapMode(MM_ANISOTROPIC);
但是,在使用了上面的代码之后,还需要具体的指定自定义坐标系统在x方向和y方向上的单位长度、坐标轴正方向以及视口左上角的位置在坐标映射中所对应的坐标值等。这个操作是通过调用CDC类的成员函数SetWindowExt和SetViewportExt来实现的。
图 2 设备上下文中窗口和视口的关系
图17说明了在设备上下文中窗口(Window)和视口(ViewportExt)的关系。视口度量的设置以设备象素为单位,而窗口度量的设置则几乎的任意的。所谓的自定义坐标系统即是将由窗口定义的坐标系统映射到由视口定义的设备上下文中的区域。
注意:
上面的术语“窗口”并不是指Windows中的窗口对象,而是在设备上下文中进行坐标映射时使用的一个抽象概念。在本节中,具有上述意义的术语“窗口”有可能和通常意义中的Windows术语“窗口”混用,读者应根据具体的上下文明确其含义。
在调用SetViewportExt设置视口之前必须先调用SetWindowExt设置窗口。
函数SetWindowExt可以使用两个整型量作为其参数,也可以使用一个CSize对象作为其参数。它指定了窗口的横向度量和纵向度量。当所指定的横向度量为负值时,表示x轴的正方向向左;当所指定的纵向度量为负值时,表示y轴的正方向向上。
同时还可以使用函数SetWindowOrg来设置窗口左上角对应的坐标。这样就可以实现坐标平面原点的平移。
在设置了窗口之后,我们还需要调用SetViewportExt函数设置视口。
函数SetViewportExt使用了与SetWindowExt相类似的参数。它们指定了视口的横向度量和纵向度量。类似的,我们还可以使用函数SetViewportOrg来设置视口的左上角的屏幕坐标。这里需要注意的是,在函数SetViewportExt和SetViewportOrg中所使用的参数的量值是以设备象素为单位的。
上面所提到的这些函数都是CDC类的成员函数。
下面所给的示例程序FuncGraphy输出一个函数[示例中是]的图象。为了方便起见,我们使用了自定义的坐标系统。在该坐标系统中,原点的窗口客户区中的中心,横坐标的范围为?31.006到31.006,在这个范围内恰好包括函数的一个完整的图象范围。其纵坐标的范围为1.2到?1.2。
为了节省篇幅,我们这里仅给出应用程序主窗口的OnPaint函数:
// 应用程序主窗口的重绘函数
void CMyWnd::OnPaint()
{
// 获得窗口的客户区设备上下文句柄
CPaintDC dc(this);
// 设置映射模式为 MM_ANISOTROPIC
dc.SetMapMode(MM_ANISOTROPIC);
// 设置窗口左上角的坐标为
dc.SetWindowOrg(-31006, 1200);
// 设置窗口度量
dc.SetWindowExt(int(2000*31.006), -2400);
// 获得客户区矩形
CRect rc;
GetClientRect(rc);
// 设置视口左上角的坐标
// 设置视口度量
dc.SetViewportExt(rc.Width(), rc.Height());
//dc.SetViewportOrg(rc.Width() / 2, rc.Height() /2); 或者将上面一句dc.SetWindowOrg去掉改为这句。
// 创建蓝色实线画笔
CPen pen(PS_SOLID, 1, RGB(0, 0, 255)), *pOldPen;
pOldPen=dc.SelectObject(&pen);
// 创建蓝色斜线刷子
CBrush br(HS_BDIAGONAL, RGB(0, 0, 255)), *pOldBrush;
pOldBrush=dc.SelectObject(&br);
// 开始一个路径
dc.BeginPath();
dc.MoveTo(-31006, 0);
for (double x=-31.006; x<=0; x+=0.02)
{
dc.LineTo(int(1000*x), int(1000*sin(-pow(-x, 1./3))));
}
for (x=0; x<=31.006; x+=0.02)
{
dc.LineTo(int(1000*x), int(1000*sin(pow(x, 1./3))));
}
dc.LineTo(3142, 0);
dc.MoveTo(0, -1200);
dc.LineTo(0, 1200);
dc.EndPath();
// 绘制路径并填充路径的内部
dc.StrokeAndFillPath();
// 恢复设备上下文原有的画笔和刷子
dc.SelectObject(pOldPen);
dc.SelectObject(pOldBrush);
}
完成并编译上面的程序,其运行结果如图所示。
图 3 示例程序FuncGraphy - 使用自定义的坐标系统
技巧:
下面我们讨论在应用程序FuncGraphy中所用到的一些技巧。首先,我们不是直接使用LineTo来一段一段的绘制曲线,而是先创建一个路径,然后再来填充该路径。这至少有两个好处:一是如果曲线需要使用如虚线等线型时,如果还一段一段的使用LineTo来绘制的话,事实上不可能得到虚线(读者可以自行验证这一点),这时我们只能先创建一个路径,再使用StrokePath之类的函数来绘制出这个路径;第二,如果使用路径的话,我们还可以使用当前刷子来填充曲线的内部,如示例代码中所示。
在自定义坐标系统时,一定要记住一点,这就是所有的图形输出函数在指定输出位置的坐标时都使用了整型参量。这说明一点,我们必须使坐标系统的单位长度具有一定的区分度。简单的说,坐标系统的单位长度应该小于一个设备象素的大小,否则你将不能通过该坐标系统准确的定位到输出设备上的每一个设备象素。如果坐标系统的单位长度过大,一个很常见的结果就是导致图形输出发生失真和变形。读者可以使用上面的示例程序FuncGraphy直接验证这一点。一般来说,对于屏幕输出设备,我们只需要使横坐标和纵坐标的跨度在2000以上,就可以有效的避免这个问题。同样的,考虑示例程序FuncGraphy,在绘制输出图形时考虑合理的步长也是很重要的。步长大小将会导致程序进行过多的不必要运算,从而明显的降低曲线的绘制速度,而大太的步长又会导致曲线不够精确。