图形基础 |
|
图形设备接口(GDI:Graphics Device Interface)是Windows的子系统,它负责在视讯显示器和打印机上显示图形。正如您所认为的那样,GDI是Windows非常重要的部分。不只您为Windows编写的应用系统在显示视觉信息时使用GDI,就连Windows本身也使用GDI来显示使用者接口对象,诸如菜单、滚动条、图标和鼠标光标。
不幸的是,如果要对GDI进行全面的讲述,将需要一整本书-当然不是这本书。在本章中,我只是想向您提供画线和填入区域的基本知识,这对于理解下面几章的GDI已经足够了。在后面几章中会讲述GDI支持的位图、metafile以及格式化文字。
从程序写作者的观点来看,GDI由几百个函数呼叫和一些相关的数据型态、宏和结构组成。但是在开始讲述这些函数的细节之前,让我们先从巨观上了解一下GDI的整体结构。
GDI原理
Windows 98和Microsoft Windows NT中的图形主要由GDI32.DLL动态链接库输出的函数来处理。在Windows 98中,这个GDI32.DLL实际是利用16位GDI.EXE动态链接库来执行许多函数。在Windows NT中,GDI.EXE只用于16位的程序。
这些动态链接库呼叫您安装的视讯显示器和任何打印机呼叫驱动程序中的例程。视讯驱动程序存取视讯显示器的硬件,打印机驱动程序将GDI命令转换为各种打印机能够理解的代码或者命令。显然,不同的视讯显示卡和打印机要求不同的设备驱动程序。
因为PC兼容机种上可以连接许多种不同的视讯设备,所以,GDI的主要目的之一是支持与设备无关的图形。Windows程序应该能够毫无困难地在Windows支持的任意一种图形输出设备上执行,GDI通过将您的程序和不同输出设备的特性隔离开来的方法来达到这一目的。
图形输出设备分为两大类:位映像设备和向量设备。大多数PC的输出设备是位映像设备,这意味着它们以图点构成的数组来表示图像,这类设备包括视讯显示卡、点阵打印机和激光打印机。向量设备使用线来绘制图像,通常局限于绘图机。
许多传统的计算机图形程序设计方式都是完全以向量为主的,这意味着使用向量图形系统的程序与硬件有着一定层次的隔离。输出设备用图素表示图形,但是程序与程序接口之间并不是用图素进行沟通的。您当然可以使用Windows GDI作为一个高阶的向量绘制系统,同时也可以将它用于比较低阶的图素操作。
从这方面来看,Windows GDI和传统的图形接口语言之间的关系,就如同C和其它程序设计语言之间的关系一样。C以它在不同操作系统和环境之间的高度可移植性而闻名,然而C也以允许程序写作者进行低阶系统呼叫而闻名,这些呼叫在其它高级语言中通常是不可能的。正如C有时被认为是一种「高级汇编语言」一样,您可以认为GDI是图形设备硬件之间的一种高阶界面。
您已经看到,Windows内定使用图素坐标系统。大多数传统的图形语言使用「虚拟」坐标系,其水平和垂直轴的范围在0到32,767之间。虽然有些图形语言不让您使用图素坐标,但是Windows GDI允许您使用两种坐标系统之一(甚至依据实际度量衡的坐标系)。您可以使用虚拟坐标系以便让程序独立于硬件之外,或者也可以使用设备坐标系而完全迎合硬设备提供的环境。
某些程序写作者认为一旦开始使用操作图素的程序设计方式,就放弃了设备无关性。我们在 上一章看到,这不完全是正确的,其中的诀窍是在与设备无关的方式中使用图素。这要求图形接口语言为程序提供一些方法来确定设备的硬件特征,并进行适当的调节。例如,在SYSMETS程序中,我们根据标准系统字体字符的图素大小来确定屏幕上的文字间距,这种方法允许程序针对分辨率、文字大小和方向比例各不相同的显示卡进行相应的调节。您将在本章看到一些用于确定显示尺寸的其它方法。
早期,许多使用者在单色显示器上执行Windows。即使是几年前,笔记本计算机也还只有灰阶显示。为此,GDI的设计保证了您可以在编写一个程序时不必太担心色彩问题-也就是说,Windows可以将色彩转换为灰阶显示。甚至在今天,Windows 98使用的视讯显示已经具有了不同的色彩能力(16色、256色、「high-Color」以及「true-color」)。虽然,彩色喷墨打印机的成本已经很低了,但是大多数使用者仍然坚持使用黑白打印机。盲目地使用这些设备是可以的,但是您的程序也应该能决定在某种显示设备上有多少色彩可以使用,从而最佳利用硬件功能。
当然,就如同您编写C程序时,为了使它在其它计算机上执行而遇到一些微妙的移植性问题一样,您也可能不小心让设备依赖性溜进您的Windows程序,这就是不与硬件完全隔离的代价。您还应该知道Windows GDI的局限。虽然可以在显示器上到处移动图形对象,但GDI通常是一个静态的显示系统,只有有限的动画支持。如果需要为游戏编写复杂的动画,就应该研究一下Microsoft DirectX,它提供了您需要的支持。
GDI函数呼叫
组成GDI的几百个函数呼叫可以分为几大类:
-
取得(或者建立)和释放(或者清除)设备内容的函数
我们在前面的章节中已经看到过,您在绘图时需要设备内容句柄。GetDC和RealseDC函数让您在非WM_PAINT的消息处理期间来做到这一点,而BeginPaint和EndPaint函数(虽然在技术上它们是USER模块而不是GDI模块的一部分)在进行绘图的WM_PAINT消息处理期间使用。我们马上还会介绍有关设备内容的其它一些函数。
- 取得有关设备内容信息的函数再以第四章中SYSMETS程序为例,我们使用GetTextMetrics函数来取得有关设备内容中目前所选字体的尺寸信息。在本章后面,我们将看到一个取得非常广泛的设备内容信息的DEVCAPS1程序。
- 绘图函数显然,在所有前提条件都得以满足之后,这些函数是真正重要的部分。在上一章中,我们使用TextOut函数在窗口的显示区域显示一些文字。我们将看到,其它GDI函数还可以让您画线、填入区域。在第十四章和第十五章还会看到如何建立位图图像。
- 设定和取得设备内容参数的函数设备内容的「属性」决定有关绘图函数如何工作的细节。例如,用SetTextColor来指定TextOut(或者其它文字输出函数)所绘制的文字色彩。在第四章中SYSMETS程序中,我们使用SetTextAlign来告诉GDI:TextOut函数中的字符串的开始位置应该在字符串的右边而不是内定的左边。设备内容的所有属性都有默认值,取得设备内容时这些默认值就设定好了。对于所有的Set函数,都有相应的Get函数,以允许您取得目前设备内容属性。
-
使用GDI对象的函数GDI在这里变得有点混乱。首先举一个例子:内定时使用GDI绘制的所有直线都是实线并具有一个标准的宽度。您可能希望绘制更细的直线,或者是由一系列的点或短划线组成的直线。这种线的宽度和这种线的画笔样式不是设备内容的属性,而是一个「逻辑画笔」的特征。您可以通过在CreatePen、
CreatePenIndirect或ExtCreatePen函数中指定这些特征来建立一个逻辑画笔,这些函数传回一个逻辑画笔的句柄(虽然这些函数被认为是GDI的一部分,但是和大多数GDI函数呼叫不一样,它们不要求设备内容的句柄)。要使用这个画笔,就要将画笔句柄选进设备内容。我们认为,设备内容中目前选中的画笔就是设备内容的一个属性。这样,您画任何线都使用这个画笔,然后,您可以取消设备内容中的画笔选择,并清除画笔对象。清除画笔对象是必要的,因为画笔定义占用了分配的内存空间。除了画笔以外,GDI对象还用于建立填入封闭区域的画刷、字体、位图以及GDI的其它一些方面。
GDI基本图形
您在屏幕或打印机上显示的图形型态本身可以被分为几类,通常被称为「基本图形」,它们是:
-
直线和曲线线条是所有向量图形绘制系统的基础。GDI支持直线、矩形、椭圆(包括椭圆的子集,也就是我们所说的「圆」)、椭圆圆周上的部分曲线即所谓的「弧」以及贝塞尔曲线(Bezier
spline),我们将在本章中分别对它们进行介绍。所有更复杂的曲线可由折线(polyline)代替,折线通过一组非常短的直线来定义一条曲线。线条用设备内容中选中的目前画笔绘制。
-
填入区域当一系列直线或者曲线封闭了一个区域时,该区域可以使用目前GDI画刷对象进行填图。这个画刷可以是实心色彩、图案(可以是一系列的水平、垂直或者对角标记)或者是在区域内垂直或者水平重复的位图图像。
- 位图位图是位的矩形数组,这些位对应于显示设备上的图素,它们是位映像图形的基础工具。位图通常用于在视讯显示器或者打印机上显示复杂(一般都是真实的)图像。位图还可以用于显示必须快速绘制的小图像(诸如图标、鼠标光标以及在应用工具条中出现的按钮等)。GDI支持两种型态的位图-旧式的(虽然还非常有用)「设备相关」位图,是GDI对象;和新的(如Windows
3.0的)「设备无关」位图(或者DIB),可以储存在磁盘文件中。第十四章和第十五章讨论位图。
- 文字文字的数学味道不像计算机图形的其它方面那样浓。文字和几百年的传统印刷术有关,它被许多印刷工人看作为一门艺术。因此,文字通常不仅是所有的计算机图形系统中最复杂的部分,而且(如果识字还是社会基本要求的话)也是最重要的部分。用于定义GDI字体对象和取得字体信息的数据结构是Windows中最庞大的部分之一。从Windows
3.1开始,GDI开始支持TrueType字体,该字体是在填入轮廓线基础上建立的,这样的填入轮廓线可由其它GDI函数处理。依据兼容性和储存大小的考虑,Windows
98继续支持旧式的点阵字体。我会在第十七章讨论字体。
其它部分
GDI的其它部分无法这么容易地分类,它们是:
-
映像模式和变换虽然内定以图素为单位进行绘图,但是您并非局限于此。GDI映像模式允许您以英寸(或者甚至以几分之一英寸)、毫米或者任何您想使用的单位来绘图(Windows
NT还支持传统的以三乘三矩阵表示的「坐标变换」,
这允许倾斜和旋转图形对象。不幸的是,在Windows
98中不支持坐标变换)。
- MetafileMetafile是以二进制形式储存的GDI命令集合。Metafile主要用于通过剪贴板传输向量图形。第十八章会讨论metafile。
-
绘图区域绘图区域是形状任意的复杂区域,通常定义为较简单的绘图区域组合。在GDI内部,绘图区域除了储存为最初用来定义绘图区域的线条组合以外,还以一系列扫描线的形式储存。您可以将绘图区域用于绘制轮廓、填入图形和剪裁。
-
路径路径是GDI内部储存的直线和曲线的集合。路径可以用于绘图、填入图形和剪裁,还可以转换为绘图区域。
-
剪裁绘图可以限制在显示区域的某一部分中,这就是所谓的剪裁。剪裁区域是不是矩形都可以,剪裁通常是通过区域或者路径来定义的。
- 调色盘自订调色盘通常限于显示256色的显示器。Windows仅保留这些色彩之中的20种以供系统使用,您可以改变其它236种色彩,以准确显示按位图形式储存的真实图像。第十六章会讨论调色盘。
- 打印虽然本章限于讨论视讯显示,但是您在本章中所学到的全部知识都适用于打印。第十三章会讨论打印。
在开始绘图之前,让我们比第四章更精确地讨论一下设备内容。
当您想在一个图形输出设备(诸如屏幕或者打印机)上绘图时,您首先必须获得一个设备内容(或者DC)的句柄。将句柄传回给程序时,Windows就给了您使用设备的权限。然后您在GDI函数中将这个句柄作为一个参数,向Windows标识您想在其上进行绘图的设备。
设备内容中包含许多确定GDI函数如何在设备上工作的目前「属性」,这些属性允许传递给GDI函数的参数只包含起始坐标或者尺寸信息,而不必包含Windows在设备上显示对象时需要的所有其它信息。例如,呼叫TextOut时,您只需要在函数中给出设备内容句柄、起始坐标、文字和文字的长度。您不必指定字体、文字颜色、文字后面的背景色彩以及字符间距,因为这些属性都是设备内容的一部分。当您想改变这些属性之一时,您呼叫一个可以改变设备内容中属性的函数,以后针对该设备内容的TextOut呼叫来使用改变后的属性。
取得设备内容句柄
Windows提供了几种取得设备内容句柄的方法。如果在处理一个消息时取得了设备内容句柄,应该在退出窗口函数之前释放它(或者删除它)。一旦释放了句柄,它就不再有效了。对于打印机设备内容句柄,规则就没有这么严格。在第十三章会讨论打印。
最常用的取得并释放设备内容句柄的方法是,在处理WM_PAINT消息时,使用BeginPaint和EndPaint呼叫:
hdc = BeginPaint (hwnd, &ps) ; 其它行程序 EndPaint (hwnd, &ps) ;
变量ps是型态为PAINTSTRUCT的结构,该结构的hdc字段是BeginPaint传回的设备内容句柄。 PAINTSTRUCT结构又包含一个名为rcPaint的RECT(矩形)结构,rcPaint定义一个包围窗口显示区域无效范围的矩形。使用从BeginPaint获得的设备内容句柄,只能在这个区域内绘图。BeginPaint呼叫使该区域有效。
Windows程序还可以在处理非WM_PAINT消息时取得设备内容句柄:
hdc = GetDC (hwnd) ; 其它行程序 ReleaseDC (hwnd, hdc) ;
这个设备内容适用于窗口句柄为hwnd的显示区域。这些呼叫与BeginPaint和EndPaint的组合之间的基本区别是,利用从GetDC传回的句柄可以在整个显示区域上绘图。当然, GetDC和ReleaseDC不使显示区域中任何可能的无效区域变成有效。
Windows程序还可以取得适用于整个窗口(而不仅限于窗口的显示区域)的设备内容句柄:
hdc = GetWindowDC (hwnd) ; 其它行程序 ReleaseDC (hwnd, hdc) ;
这个设备内容除了显示区域之外,还包括窗口的标题列、菜单、滚动条和框架(frame)。GetWindowDC函数很少使用,如果想尝试用一用它,则必须拦截处理WM_NCPAINT消息,Windows使用该消息在窗口的非显示区域上绘图。
BeginPaint、GetDC和GetWindowDC获得的设备内容都与视讯显示器上的某个特定窗口相关。取得设备内容句柄的另一个更通用的函数是CreateDC:
hdc = CreateDC (pszDriver, pszDevice, pszOutput, pData) ; 其它行程序 DeleteDC (hdc) ;
例如,您可以通过下面的呼叫来取得整个屏幕的设备内容句柄:
hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;
在窗口之外写入画面一般是不恰当的,但对于一些不同寻常的应用程序来说,这样做很方便(您还可通过在呼叫GetDC时使用一个NULL参数,从而取得整个屏幕的设备内容句柄,不过这在文件中已经提到了)。在 第十三章中,我们将使用CreateDC函数来取得一个打印机设备内容句柄。
有时您只是需要取得关于某设备内容的一些信息而并不进行任何绘画,在这种情况下,您可以使用CreateIC来取得一个「信息内容」的句柄,其参数与CreateDC函数相同,例如:
hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;
您不能用这个信息内容句柄往设备上写东西。
使用位图时,取得一个「内存设备内容」有时是有用的:
hdcMem = CreateCompatibleDC (hdc) ; 其它行程序 DeleteDC (hdcMem) ;
您可以将位图选进内存设备内容,然后使用GDI函数在位图上绘画。我将在第十四章讨论这些技术。
前面已经提到过,metafile是一些GDI呼叫的集合,以二进制形式编码。您可以通过取得metafile设备内容来建立metafile:
hdcMeta = CreateMetaFile (pszFilename) ; 其它行程序 hmf = CloseMetaFile (hdcMeta) ;
在metafile设备内容有效期间,任何用hdcMeta所做的GDI呼叫都变成metafile的一部分而不会显示。在呼叫CloseMetaFile之后,设备内容句柄变为无效,函数传回一个指向metafile(hmf)的句柄。我会在 第十八章讨论metafile。
取得设备内容信息
一个设备内容通常是指一个实际显示设备,如视讯显示器和打印机。通常,您需要取得有关该设备的信息,包括显示器的大小(单位为图素或者实际长度单位)和色彩显示能力。您可以通过呼叫GetDeviceCaps(「取得设备功能」)函数来取得这些信息:
iValue = GetDeviceCaps (hdc, iIndex) ;
其中,参数iIndex取值为WINGDI.H表头文件中定义的29个标识符之一。例如,iIndex为HORZRES时将使GetDeviceCaps传回设备的宽度(单位为图素);iIndex为VERTRES时将让GetDeviceCaps传回设备的高度(单位为图素)。如果hdc是打印机设备内容的句柄,则GetDeviceCaps传回打印机显示区域的高度和宽度,它们也是以图素为单位的。
还可以使用GetDeviceCaps来确定设备处理不同型态图形的能力,这对于视讯显示器并不很重要,但是对于打印设备却是非常重要。例如,大多数绘图机不能画位图图像,GetDeviceCaps就可以将这一情况告诉您。
DEVCAPS1程序
程序5-1所示的DEVCAPS1程序显示了以一个视讯显示器的设备内容为参数时,可以从 GetDeviceCaps函数中获得的部分信息(该程序的另一个扩充版本DEVCAPS2将在第十三章给出,用于取得打印机信息)。
DEVCAPS1.C /*------------------------------------------------------------------------ DEVCAPS1.C -- Device Capabilities Display Program No. 1 (c) Charles Petzold, 1998 ----------------------------------------------------------------------*/ #include <windows.h> #define NUMLINES ((int) (sizeof devcaps / sizeof devcaps [0])) struct { int iIndex ; TCHAR *szLabel ; TCHAR *szDesc ; } devcaps [] = { HORZSIZE, TEXT ("HORZSIZE"),TEXT ("Width in millimeters:"), VERTSIZE, TEXT ("VERTSIZE"),TEXT ("Height in millimeters:"), HORZRES, TEXT ("HORZRES"), TEXT ("Width in pixels:"), VERTRES, TEXT ("VERTRES"), TEXT ("Height in raster lines:"), BITSPIXEL, TEXT ("BITSPIXEL"),TEXT ("Color bits per pixel:"), PLANES, TEXT ("PLANES"), TEXT ("Number of color planes:"), NUMBRUSHES, TEXT ("NUMBRUSHES"), TEXT ("Number of device brushes:"), NUMPENS, TEXT ("NUMPENS"), TEXT ("Number of device pens:"), NUMMARKERS, TEXT ("NUMMARKERS"), TEXT ("Number of device markers:"), NUMFONTS, TEXT ("NUMFONTS"), TEXT ("Number of device fonts:"), NUMCOLORS, TEXT ("NUMCOLORS"), TEXT ("Number of device colors:"), PDEVICESIZE, TEXT ("PDEVICESIZE"),TEXT ("Size of device structure:"), ASPECTX, TEXT ("ASPECTX"), TEXT ("Relative width of pixel:"), ASPECTY, TEXT ("ASPECTY"), TEXT ("Relative height of pixel:"), ASPECTXY, TEXT ("ASPECTXY"), TEXT ("Relative diagonal of pixel:"), LOGPIXELSX, TEXT ("LOGPIXELSX"), TEXT ("Horizontal dots per inch:"), LOGPIXELSY, TEXT ("LOGPIXELSY"), TEXT ("Vertical dots per inch:"), SIZEPALETTE, TEXT ("SIZEPALETTE"),TEXT ("Number of palette entries:"), NUMRESERVED, TEXT ("NUMRESERVED"),TEXT ("Reserved palette entries:"), COLORRES, TEXT ("COLORRES"), TEXT ("Actual color resolution:") } ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("DevCaps1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Device Capabilities"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxChar, cxCaps, cyChar ; TCHAR szBuffer[10] ; HDC hdc ; int i ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar= tm.tmAveCharWidth ; cxCaps= (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar= tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) { TextOut ( hdc, 0, cyChar * i, devcaps[i].szLabel, lstrlen (devcaps[i].szLabel)) ; TextOut ( hdc, 14 * cxCaps, cyChar * i, devcaps[i].szDesc, lstrlen (devcaps[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, 14*cxCaps+35*cxChar, cyChar*i, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetDeviceCaps (hdc, devcaps[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
可以看到,这个程序非常类似第四章的SYSMETS1。为了保持程序代码的短小,我没有使用滚动条,因为我知道信息可以在一个画面上显示出来。在256色,640×480的VGA上显示的结果如图5-1所示。
图5-1 256色,640×480VGA上的DEVCAPS1显示 |
假定要绘制边长为1英寸的正方形,您(程序写作者)或Windows(操作系统)需要知道视讯显示上1英寸对应多少图素。使用GetDeviceCaps函数能取得有关如视讯显示器和打印机之类输出设备的实际显示大小信息。
视讯显示器和打印机是两个不同的设备。但也许最不明显的区别是「分辨率」与设备联系起来的方式。对于打印机,我们经常用「每英寸的点数(dpi)」表示分辨率。例如,大多数激光打印机有300或600dpi的分辨率。然而,视讯显示器的分辨率是以水平和垂直的总图素数来表示的,例如,1024×768。大多数人不会告诉您他的打印机在一张纸上水平和垂直打印多少图素或他们的视讯显示器上每英寸有多少图素。
在本书中,我用「分辨率」来严格定义每度量单位(一般为英寸)内的图素数。我使用「图素大小」或「图素尺寸」表示设备水平或垂直显示的总图素数。「度量大小」或「度量尺寸」是以英寸或毫米为单位的设备显示区域的大小。(对于打印机页面,它不是整个页面,只是可打印的区域。)图素大小除以度量大小就得到分辨率。
现在Windows使用的大多数视讯显示器的屏幕都是宽比高多33%。这就表示纵横比为1.33:1或(一般写法)4:3。历史上,该比例可追溯到Thomas Edison制作电影的年代。它一直作为电影的标准纵横比,直到1953年出现各种型态的宽银幕投影机。电视机屏幕的纵横比也是4:3。
然而,Windows应用程序不应假设视讯显示器具有4:3的纵横比。人们进行文字处理时希望视讯显示器与一张纸的长和宽类似。最普通的选择是把4:3变为3:4显示,把标准显示翻转一下。
如果设备的水平分辨率与垂直分辨率相等,就称设备具有「正方形图素」。现在,Windows普遍使用的视讯显示器都具有正方形图素,但也有例外。(应用程序也不应假设视讯显示器总是具有正方形图素。)Windows第一次发表时,标准显示卡卡是IBM Color Graphics Adapter(CGA),它有640×200的图素大小;Enhanced Graphics Adapter(EGA)有640×350的图素大小;Hercules Graphics Card有720×348的图素大小。所有这些显示卡都使用4:3纵横比的显示器,但是水平和垂直图素数的比值都不是4:3。
执行Windows的使用者很容易确定视讯显示器的图素大小。在「控制台」中执行「显示器」,并选择「设定」页面标签。在标有「桌面区域」的字段中,可以看到这些图素尺寸之一:
- 640×480图素
- 800×600图素
- 1024×768图素
- 1280×1024图素
- 1600×1200图素
所有这些都是4:3。(除了1280×1024图素大小。这不但有些不好,还有些令人反感。所有这些图素尺寸都认为在4:3的显示器上会产生正方形的图素。)
Windows应用程序可以使用SM_CXSCREEN和SM_CYSCREEN参数从GetSystemMetrics得到图素尺寸。从DEVCAPS1程序中您会注意到,程序可以用HORZRES(水平分辨率)和VERTRES参数从GetDeviceCaps中得到同样的值。这里「分辨率」指的是图素大小而不是每度量单位的图素数。
这些是设备大小的简单部分,现在开始复杂的部分。
前两个设备能力,HORZSIZE和VERTSIZE,文件中称为「以毫米计的实际屏幕的宽度」及「以毫米计的实际屏幕的高度」(在/Platform SDK/Graphics和Multimedia Services/GDI/Device Contexts/Device Context Reference/Device Context Functions/GetDeviceCaps中)。这些看起来更像直接的定义。例如,给出视讯显示卡和显示器的接口特性,Windows如何真正知道显示器的大小呢?如果您有台膝上型计算机(它的视讯驱动程序能知道准确的屏幕大小)并且连接了外部显示器,又是哪种情况呢?如果把视讯投影机连接到计算机上呢?
在Windows的16位版本中(及在Windows NT中),Windows为HORZSIZE和VERTSIZE使用「标准」的显示大小。然而,从Windows 95开始,HORZSIZE和VERTSIZE值是从HORZRES、VERTRES、LOGPIXELSX和LOGPIXELSY值中衍生出来的。这是它的工作方式。
当您在「控制台」中使用「显示器」程序选择显示的图素大小时,也可以选择系统字体的大小。这个选项的原因是用于640×480显示的字体在提升到1024×768或更大时字太小,而您可能想要更大的系统字体。这些系统字体大小指「显示器」程序的「设定」页面卷标中的「小字体」和「大字体」。
在传统的排版中,字体的字母大小由「点」表示。1点大约1/72英寸,在计算机排版中1点正好为1/72英寸。
理论上,字体的点值是从字体中最高的字符顶部到例如j、p、q和y等字母下部的字符底部的距离,其中不包括重音符号。例如,在10点的字体中此距离是10/72英寸。根据TEXTMETRIC结构,字体的点值等于tmHeight字段减去tmInternalLeading字段,如图5-2所示(该图与上一章的图4-3一样)。
图5-2 小字体和TEXTMETRIC字段。 |
在真正的排版中,字体的点值与字体字母的实际大小并不正好相等。字体的设计者做出的实际字符比点值指示的要大一些或小一些。毕竟,字体设计是一种艺术而不是科学。
TEXTMETRIC结构的tmHeight字段指出文字的连续行在屏幕或打印机上间隔的方式。这也可以用点来测量。例如,12点的行距指出文字连续行的基准线应该间隔12/72(或1/6)英寸。不应该为10点字体使用10点行距,因为文字的连续行会碰到一起。
10点字体读起来很舒服。小于10点的字体不益于长时间阅读。
Windows系统字体-不考虑是大字体还是小字体,也不考虑所选择的视频图素大小-固定假设为10点字体和12点行距。这听起来很奇怪,如果字体都是10点,为什么还把它们称为大字体和小字体呢?
解答是:当您在「控制台」的「显示」程序上选择小字体或大字体时,实际上是选择了一个假定的视讯显示分辨率,单位是每英寸的点数 。当选择小字体时,即要Windows假定视讯显示分辨率为每英寸96点。当选择大字体时,即要Windows假定视讯显示分辨率为每英寸120点。
再看看图5-2。那是小字体,它依据的显示分辨率为每英寸96点。我说过它是10点字体。10点即是10/72英寸,如果乘以96点,每英寸大概就为13图素。这即是tmHeight减去tmInternalLeading的值。行距是12点,或12/72英寸,它乘以96点,每英寸就为16图素。这即是tmHeight的值。
图5-3显示大字体。这是依据每英寸120点的分辨率。同样,它是10点字体,10/72乘以120点,每英寸等于16图素,即是tmHeight减tmInternalLeading的值。12点行距等于20图素,即是tmHeight的值。(像第四章一样,再次强调所显示的是实际的度量大小,因此您可以理解它工作的方式。不要在您的程序中对此写作程序。)
图5-3 大字体和FONTMETRIC字段 |
在Windows程序中,您可以使用GetDeviceCaps函数取得使用者在「控制台」的「显示器」程序中选择的以每英寸的点数为单位的假定分辨率。要得到这些值(如果视讯显示器不具有正方形图素,在理论上这些值是不同的),可以使用索引LOGPIXELSX和LOGPIXELSY。LOGPIXELS指逻辑图素,它的基本意思是「以每英寸的图素数为单位的非实际分辨率」。
用HORZSIZE和VERTSIZE索引从GetDeviceCaps得到的设备能力,在文件上称为「实际屏幕的宽度,单位毫米」及「实际屏幕的高度,单位毫米」。因为这些值是从HORZRES、VERTRES、LOGPIXELSX和LOGPIXELSY值中衍生出来的,所以它们应该称为「逻辑宽度」和「逻辑高度」。公式是:
常数25.4用于把英寸转变为毫米。
这看起来是种不合逻辑的退步。毕竟,视讯显示器是可以用尺以毫米为单位的大小(至少是近似的)衡量的。但是Windows 98并不关心这个大小。相反,它以使用者选择的显示图素大小和系统字体大小为基础计算以毫米为单位的显示大小。更改显示的图素大小并根据GetDeviceCaps更改度量大小。这有什么意义呢?
这非常有意义。假定有一个17英寸的显示器。实际的显示大小大约是12英寸乘9英寸。假定在最小要求的640×480图素大小下执行Windows。这意味着实际的分辨率是每英寸53点。10点字体(在纸上便于阅读)在屏幕上从A的顶部到q的底部只有7个图素。这样的字体很难看而且不易读。(可问问那些在旧的Color Graphics Adapter上执行Windows的人们。)
现在,把您的计算机接上视讯投影机。投影的视讯显示器是4英尺宽,3英尺高。同样的640×480图素大小现在是大约每英寸13点的分辨率。在这种条件下试图显示10点的字体是很可笑的。
10点字体在视讯显示器上应是可读的,因为它在打印时是肯定可读的。所以10点字体就成为一个重要的参照。当Windows应用程序确保10点屏幕字体为平均大小时,就能够使用8点字体显示较小的文字(仍可读),或用大于10点的字体显示较大的文字。因而,视频分辨率(以每英寸的点数为单位)由10点字体的图素大小来确定是很有意义的。
然而,在Windows NT中,用老的方法定义HORZSIZE和VERTSIZE值。这种方法与Windows的16位版本一致。HORZRES和VERTRES值仍然表示水平和垂直图素的数值,LOGPIXELSX和LOGPIXELSY仍然与在「控制台」的「显示器」程序中选择的字体有关。在Windows 98中,LOGPIXELSX和LOGPIXELSY的典型值是96和120 dpi,这取决于您选择的是小字体还是大字体。
在Windows NT中的区别是HORZSIZE和VERTSIZE值固定表示标准显示器大小。对于普通的显示卡,取得的HORZSIZE和VERTSIZE值分别是320和240毫米。这些值是相同的,与选择的图素大小无关。因此,这些值与用HORZRES、VERTRES、LOGPIXELSX和LOGPIXELSY索引从GetDeviceCaps中得到的值不同。然而,可以用前面的公式计算在Windows 98下的HORZSIZE和VERTSIZE值。
如果程序需要实际的视讯显示大小该怎么办?也许最好的解决方法是用对话框让使用者输入它们。
最后,来自GetDeviceCaps的另三个值与视讯大小有关。ASPECTX、ASPECTY和ASPECTXY值是每一个图素的相对宽度、高度和对角线大小,四舍五入到整数。对于正方形图素,ASPECTX和ASPECTY值相同。无论如何,ASPECTXY值应等于ASPECTX与ASPECTY平方和的平方根,就像直角三角形一样。
关于色彩
如果视讯显示卡仅显示黑色图素和白色图素,则每个图素只需要内存中的一位。彩色显示器中每个图素需要多个位。位数越多,色彩越多,或者更具体地说,可以同时显示的不同色彩的数目等于2的位数次方。
「Full-Color」视讯显示器的分辨率是每个图素24位-8位红色、8位绿色以及8位蓝色。红、绿、蓝即「色光三原色」。混合这三种基本颜色可以生成许多其它的颜色,您通过放大镜看显示屏,就可以看出来。
「High-Color」显示分辨率是每个图素16位-5位红色、6位绿色以及5位蓝色。绿色多一位是因为人眼对绿色更敏感一些。
显示256种颜色的显示卡每个图素需要8位。然而,这些8位的值一般由定义实际颜色的调色盘组织的。我会在 第十六章详细地讨论它们。
最后,显示16种颜色的显示卡每个图素需要4位。这16种颜色一般固定分为暗的或亮的红、黑、蓝、青、紫、黄、两种灰色。这16种颜色要回溯到老式的IBM CGA。
祇有在某些怪异的程序中才需要知道视讯显示卡上的内存是如何组织的,但是GetDeviceCaps使程序写作者可以知道显示卡的储存组织以及它能够表示的色彩数目,下面的呼叫传回色彩平面的数目:
iPlanes = GetDeviceCaps (hdc, PLANES) ;
下面的呼叫传回每个图素的色彩位数:
iBitsPixel = GetDeviceCaps (hdc, BITSPIXEL) ;
大多数彩色图形显示设备使用多个色彩平面或每图素有多个色彩位的设计,但是不能同时一齐使用这两种方式;换句话说,这两个呼叫必有一个传回1。显示卡能够表示的色彩数可以用如下公式来计算:
iColors = 1 << (iPlanes * iBitsPixel) ;
这个值与用NUMCOLORS参数得到的色彩数值可能一样,也可能不一样:
iColors = GetDeviceCaps (hdc, NUMCOLORS) ;
我提到过,256色的显示卡使用色彩调色盘。在那种情况下,以NUMCOLORS为参数时,GetDeviceCaps传回由Windows保留的色彩数,值为20,剩余的236种颜色可以由Windows程序用调色盘管理器设定。对于High-Color和True-Color显示分辨率,带有NUMCOLORS参数的GetDeviceCaps通常传回-1,这样就无法得到需要的信息,因此应该使用前面所示的带有PLANES和BITSPIXEL值的iColors公式。
在大多数GDI函数呼叫中,使用COLORREF值(只是一个32位的无正负号长整数)来表示一种色彩。COLORREF值按照红、绿和蓝色的亮度指定了一种颜色,通常叫做「RGB色彩」 。32位的COLORREF值的设定如图5-4所示。
图5-4 32位COLORREF值 |
注意最前面是标为0的8个位,并且每种原色都指定为一个8位的值。理论上,COLORREF可以指定二的二十四次方种或一千六百万种色彩。
这个无正负号长整数常常称为一个「RGB色彩」。Windows表头文件WINGDI.H提供了几种使用RGB色彩值的宏。RGB宏要求三个参数分别代表红、绿和蓝值,然后将它们组合为一个无正负号长整数:
#define RGB(r,g,b) ((COLORREF)(((BYTE)(r) | \ ((WORD)((BYTE)(g)) << 8)) | \ (((DWORD)(BYTE)(b)) << 16)))
注意三个参数的顺序是红、绿和蓝。因此,值:
RGB (255, 255, 0)
是0x0000FFFF,或黄色(红色和绿色的合成)。当所有三个参数设定为0时,色彩为黑色;当所有参数设定为255时,色彩为白色。GetRValue、GetGValue和GetBValue宏从COLORREF值中抽取出原色值。当您在使用传回RGB色彩值的Windows函数时,这些宏有时会很方便。
在16色或256色显示卡上,Windows可以使用「混色」来模拟设备能够显示的颜色之外的色彩。混色利用了由多种色彩的图素组成的图素图案。可以呼叫GetNearestColor来决定与某一色彩最接近的纯色:
crPureColor = GetNearestColor (hdc, crColor) ;
设备内容属性
前面已经提到过,Windows使用设备内容来保存控制GDI函数在显示器上如何操作的「属性」。例如,在用TextOut函数显示文字时,程序写作者不必指定文字的色彩和字体,Windows从设备内容取得这个信息。
程序取得一个设备内容的句柄时,Windows用默认值设定所有的属性(在下一节会看到如何取代这种设定)。表5-1列出了Windows 98支持的设备内容属性,程序可以改变或者取得任何一种属性。
表5-1 |
设备内容属性 |
默认值 |
修改该值的函数 |
取得该值的函数 |
Mapping Mode |
MM_TEXT |
SetMapMode |
GetMapMode |
Window Origin |
(0, 0) |
SetWindowOrgEx OffsetWindowOrgEx |
GetWindowOrgEx |
Viewport Origin |
(0, 0) |
SetViewportOrgEx OffsetViewportOrgEx |
GetViewportOrgEx |
Window Extents |
(1, 1) |
SetWindowExtEx SetMapMode ScaleWindowExtEx |
GetWindowExtEx |
Viewport Extents |
(1, 1) |
SetViewportExtEx SetMapMode ScaleViewportExtEx |
GetViewportExtEx |
Pen |
BLACK_PEN |
SelectObject |
SelectObject |
Brush |
WHITE_BRUSH |
SelectObject |
SelectObject |
Font |
SYSTEM_FONT |
SelectObject |
SelectObject |
Bitmap |
None |
SelectObject |
SelectObject |
Current Position |
(0, 0) |
MoveToEx LineTo PolylineTo PolyBezierTo |
GetCurrentPositionEx |
Background Mode |
OPAQUE |
SetBkMode |
GetBkMode |
Background Color |
White |
SetBkColor |
GetBkColor |
Text Color |
Black |
SetTextColor |
GetTextColor |
Drawing Mode |
R2_COPYPEN |
SetROP2 |
GetROP2 |
Stretching Mode |
BLACKONWHITE |
SetStretchBltMode |
GetStretchBltMode |
Polygon Fill Mode |
ALTERNATE |
SetPolyFillMode |
GetPolyFillMode |
Intercharacter Spacing |
0 |
SetTextCharacterExtra |
GetTextCharacterExtra |
Brush Origin |
(0, 0) |
SetBrushOrgEx |
GetBrushOrgEx |
Clipping Region |
None |
SelectObject SelectClipRgn IntersectClipRgn OffsetClipRgn ExcludeClipRect SelectClipPath |
GetClipBox |
保存设备内容
通常,在您呼叫GetDC或BeginPaint时,Windows用默认值建立一个新的设备内容,您对属性所做的一切改变在设备内容用ReleaseDC或EndPaint呼叫释放时,都会丢失。如果您的程序需要使用非内定的设备内容属性,则您必须在每次取得设备内容句柄时初始化设备内容:
case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; 设备内容属性 绘制窗口显示区域 EndPaint (hwnd, &ps) ; return 0 ;
虽然在通常情况下这种方法已经很令人满意了,但是您还可能想要在释放设备内容之后,仍然保存程序中对设备内容属性所做的改变,以便在下一次呼叫GetDC和BeginPaint时它们仍然能够起作用。为此,可在登录窗口类别时,将CS_OWNDC旗标纳入窗口类别的一部分:
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC ;
现在,依据这个窗口类别所建立的每个窗口都将拥有自己的设备内容,它一直存在,直到窗口被删除。如果使用了CS_OWNDC风格,就只需初始化设备内容一次,可以在处理WM_CREATE消息处理期间完成这一操作:
case WM_CREATE: hdc = GetDC (hwnd) ; 初始化设备内容属性 ReleaseDC (hwnd, hdc) ;
这些属性在改变之前一直有效。
CS_OWNDC风格只影响GetDC和BeginPaint获得的设备内容,不影响其它函数(如GetWindowDC)获得的设备内容。以前不提倡使用CS_OWNDC风格,因为它需要内存;现在,在处理大量图形的Windows NT应用程序中,它可以提高性能。即使用了CS_OWNDC,您仍然应该在退出窗口消息处理程序之前释放设备内容。
某些情况下,您可能想改变某些设备内容属性,用改变后的属性进行绘图,然后恢复原来的设备内容。要简化这一过程,可以通过如下呼叫来保存设备内容的状态:
idSaved = SaveDC (hdc) ;
现在,可以改变一些属性,在想要回到呼叫SaveDC前存在的设备内容时,呼叫:
RestoreDC (hdc, idSaved) ;
您可以在呼叫RestoreDC之前呼叫SaveDC数次。
大多数程序写作者以不同的方式使用SaveDC和RestoreDC。然而,更像汇编语言中的PUSH和POP指令,当您呼叫SaveDC时,不需要保存传回值:
SaveDC (hdc) ;
然后,您可以更改某些属性并再次呼叫SaveDC。要将设备内容恢复到一个已经保存的状态,呼叫:
RestoreDC (hdc, -1) ;
这就将设备内容恢复到最近由SaveDC函数保存的状态中。
在第一章,我们谈论过Windows图形设备接口将图形输出设备的设备驱动程序与计算机连在一起的方式。在理论上,只要提供SetPixel和GetPixel函数,就可以使用图形设备驱动程序绘制一切东西了。其余的一切都可以使用GDI模块中实作的更高阶的例程来处理。例如,画线时,只需GDI呼叫SetPixel数次,并适当地调整x和y坐标。
在实际情况中,也的确可以仅使用SetPixel和GetPixel函数进行您需要的任何绘制。您也可以在这些函数的基础上设计出简洁和构造良好的图形编程系统。唯一的问题是启能。如果一个函数通过几次呼叫才能到达SetPixel函数,那么它执行起来会非常慢。如果一个图形系统画线和进行其它复杂的图形操作是在设备驱动程序的层次上,它就会更有效得多,因为设备驱动程序对完成这些操作的程序代码进行了最佳化。此外,一些显示卡包含了图形协处理器,它允许视讯硬件自己绘制图形。
设定图素
即使Windows GDI包含了SetPixel和GetPixel函数,但很少使用它们。在本书,仅在第七章的CONNECT程序中使用了SetPixel函数,仅在第八章的WHATCLR程序中使用了GetPixel函数。尽管如此,由它们开始来研究图形仍是非常方便。
SetPixel函数在指定的x和y坐标以特定的颜色设定图素:
SetPixel (hdc, x, y, crColor) ;
如同在任何绘图函数中一样,第一个参数是设备内容的句柄。第二个和第三个参数指明了坐标位置。通常要获得窗口显示区域的设备内容,并且x和y相对于该显示区域的左上角。最后一个参数是COLORREF型态指定了颜色。如果在函数中指定的颜色视讯显示器不支持,则函数将图素设定为最接近的纯色并从函数传回该值。
GetPixel函数传回指定坐标处的图素颜色:
crColor = GetPixel (hdc, x, y) ;
直线
Windows可以画直线、椭圆线(椭圆圆周上的曲线)和贝塞尔曲线。Windows 98支援的7个画线函数是:
- LineTo 画直线。
- Polyline和PolylineTo 画一系列相连的直线。
- PolyPolyline 画多组相连的线。
- Arc 画椭圆线。
- PolyBezier和PolyBezierTo 画贝塞尔曲线。
另外,Windows NT还支持3种画线函数:
- ArcTo和AngleArc 画椭圆线。
- PolyDraw 画一系列相连的线以及贝塞尔曲线。
这三个函数Windows 98不支援。
在本章的后面我将介绍一些既画线也填入所画图形的封闭区域的函数,这些函数是:
- Rectangle 画矩形。
- Ellipse 画椭圆。
- RoundRect 画带圆角的矩形。
- Pie 画椭圆的一部分,使其看起来像一个扇形。
- Chord 画椭圆的一部分,以呈弓形。
设备内容的五个属性影响着用这些函数所画线的外观:目前画笔的位置(仅用于LineTo、PolylineTo、PolyBezierTo和ArcTo )、画笔、背景方式、背景色和绘图模式。
画一条直线,必须呼叫两个函数。第一个函数指定了线的开始点,第二个函数指定了线的终点:
MoveToEx (hdc, xBeg, yBeg, NULL) ; LineTo (hdc, xEnd, yEnd) ;
MoveToEx实际上不会画线,它只是设定了设备内容的「目前位置」属性。然后LineTo函数从目前的位置到它所指定的点画一条直线。目前位置只是用于其它几个GDI函数的开始点。在内定的设备内容中,目前位置最初设定在点(0,0)。如果在呼叫LineTo之前没有设定目前位置,那么它将从显示区域的左上角开始画线。
小历史:
Windows的16位版本中,用来改变目前位置的函数是MoveTo。该函数只调整三个参数-设备内容句柄、x和y坐标。函数通过两个16位数拼成的32位无正负号长整数传回先前的目前位置。然而,在Windows的32位版本中,坐标是32位的数值,而C的32位版本中又没有定义64位的整数数据型态,因此这种改变意味着MoveTo在其传回值中不再指出先前的目前位置。在实际的程序写作中,由MoveTo传回的值几乎从来不用,因此就需要一个新函数,这就是MoveToEx。
MoveToEx的最后一个参数是指向POINT结构的指针。从该函数传回后,POINT结构的x和y字段指出了先前的目前位置。如果您不需要这种信息(通常如此),可以简单地如上面的例子所示的那样将最后一个参数设定为NULL。
警告:
尽管Windows 98中的坐标值看起来是32位的,实际上却只用到了低16位,坐标值实际上被限制在-32,768到32,767之间。在Windows NT中,使用完整的32位值。
如果您需要目前位置,就可以通过以下呼叫获得:
GetCurrentPositionEx (hdc, &pt) ;
其中,pt是POINT结构的。
下面的程序代码从窗口的左上角开始,在显示区域中画一个网格,线与线之间相隔100个图素,其中hwnd是窗口句柄,hdc是设备内容句柄,而x和y是整数:
GetClientRect (hwnd, &rect) ; for ( x = 0 ; x < rect.right ; x+= 100) { MoveToEx (hdc, x, 0, NULL) ; LineTo (hdc, x, rect.bottom) ; } for (y = 0 ; y < rect.bottom ; y += 100) { MoveToEx (hdc, 0, y, NULL) ; LineTo (hdc, rect.right, y) ; }
虽然用两个函数来画一条直线显得有些麻烦,但是在希望画一组相连的直线时,目前画笔位置属性又会变得很有用。例如,您可能想定义一个包含5个点(10个值)的数组,来画一个矩形的边界框:
POINT apt[5] = { 100, 100, 200, 100, 200, 200, 100, 200, 100, 100 } ;
注意,最后一个点与第一个点相同。现在,只需要使用MoveToEx移到第一个点,并对后面的点使用LineTo:
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; for ( i = 1 ; i < 5 ; i++) LineTo (hdc, apt[i].x, apt[i].y) ;
由于LineTo从目前位置画到(但不包括)LineTo函数中给出的点,所以这段程序代码没有在任何坐标处画两次。虽然在显示器上多输出几次不存在问题,但是在绘图机上或者在其它绘图方式(下面马上会讲到)下,视觉效果就不太好了。
当您要将数组中的点连接成线时,使用Polyline函数要简单得多。下面这条叙述画出与上面一段程序代码相同的矩形:
Polyline (hdc, apt, 5) ;
最后一个参数是点的数目。我们还可以使用(sizeof (apt) / sizeof (POINT))来表示这个值。Polyline与一个MoveToEx函数后面加几个LineTo函数的效果相同,但是,Polyline既不使用也不改变目前位置。PolylineTo有些不同,这个函数使用目前位置作为开始点,并将目前位置设定为最后一根线的终点。下面的程序代码画出与上面所示一样的矩形:
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; PolylineTo (hdc, apt + 1, 4) ;
您可以对几条线使用Polyline和PolylineTo,这些函数在绘制复杂曲线最有用了。您使用由几百甚至几千条线组成的极短线段,把它们连在一起就像一条曲线一样。例如,画正弦波就是这样的,程序5-2所示的SINEWAVE程序显示了如何做到这一点。
程序5-2 SINEWAVE SINEWAVE.C /*------------------------------------------------------------------- SINEWAVE.C -- Sine Wave Using Polyline (c) Charles Petzold, 1998 ---------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #define NUM 1000 #define TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SineWave") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Sine Wave Using Polyline"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; int i ; PAINTSTRUCT ps ; POINT apt [NUM] ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; MoveToEx (hdc, 0, cyClient / 2, NULL) ; LineTo (hdc, cxClient, cyClient / 2) ; for (i = 0 ; i < NUM ; i++) { apt[i].x = i * cxClient / NUM ; apt[i].y = (int) (cyClient / 2 * (1 - sin (TWOPI * i / NUM))) ; } Polyline (hdc, apt, NUM) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
这个程序有一个含有1000个POINT结构的数组。随着for循环从0增加到999,结构的x成员设定为从0递增到数值cxClient。结构的y成员设定为一个周期的正弦曲线值,并被放大以填满显示区域。整个曲线的绘制仅仅使用了一个Polyline呼叫。因为Polyline函数是在设备驱动程序层次上实作的,因此它要比呼叫1000次LineTo快得多,结果如图5-5所示。
图5-5 SINEWAVE显示 |
边界框函数
下面我想讨论的是Arc函数,它绘制椭圆曲线。然而,如果不先讨论一下Ellipse函数,那么Arc函数将难以理解;而如果不先讨论Rectangle函数,那么Ellipse函数又将难以理解;而如果讨论Ellipse和Rectangle函数,那么我又会讨论RoundRect、Chord和Pie函数。
问题在于,Rectangle、Ellipse、RoundRect、Chord和Pie函数严格来说不是画线函数。没错,这些函数是在画线,但它们同时又填入画刷填入一个封闭区域。这个画刷内定为白色,因此当您第一次使用这些函数时,您可能不会注意到它们不只是画线。严格地说,这些函数属于后面「填入区域」的小节,不过,我还是在这里讨论它们。
上面提到的函数有一个共同特性,即它们都是依据一个矩形边界框来绘图的。您定义一个包含该对象的框,即「边界框(bounding box)」;Windows就在这个框内画出该物件。
这些函数中最简单的就是画一个矩形:
Rectangle (hdc, xLeft, yTop, xRight, yBottom) ;
点(xLeft, yTop)是矩形的左上角,(xRight, yBottom)是矩形的右下角。用函数Rectangle画出的图形如图5-6所示,矩形的边总是平行于显示器的水平和垂直边。
图5-6 使用Rectangle函数画出的图形 |
以前写过图形程序的程序写作者熟悉图素偏差的问题。有些图形系统画出的图形包含右坐标和底坐标,而有些则只画到(而不包含)右坐标和底坐标。Windows采用后一种方法,不过有一种更简单的方法来思考这个问题。
考虑下面的函数呼叫:
Rectangle (hdc, 1, 1, 5, 4) ;
上面我们提到,Windows在边界框内画图。可以将显示器想象成一个网格,其中,每个图素都在一个网格单元内。边界框画在网格上,然后在边界框内画矩形,下面说明了图形画出来时的样子:
将矩形和显示区域左上角分开的区域有l个图素宽。 |
我以前提到过,Rectangle严格地说不是画线函数,GDI也填入封闭区域。然而,因为内定用白色填入区域,因此GDI填入区域并不明显。
您知道了如何画矩形,也就知道了如何画椭圆,因为它们使用的参数都是相同的:
Ellipse (hdc, xLeft, yTop, xRight, yBottom) ;
用Ellipse函数画出的图形如图5-7所示(加上了虚线构成的边界框)。
图5-7 用Ellipse函数画出的图形 |
画圆角矩形的函数使用与函数Rectangle及Ellipse函数相同的边界框,还包含另外两个参数:
RoundRect (hdc, xLeft, yTop, xRight, yBottom, xCornerEllipse, yCornerEllipse) ;
用这个函数画出的图形如5-8所示。
图5-8 用RoundRect函数画出的图形 |
Windows使用一个小椭圆来画圆角,这个椭圆的宽为xCornerEllipse,高为yCornerEllipse。可以想象这个小椭圆分为了四个部分,一个象限一个,每个刚好用在矩形的一个角上。xCornerEllipse和yCornerEllipse的值越大,角就越明显。如果xCornerEllipse等于xLeft与xRight的差,且yCornerEllipse等于yTop与yBottom的差,那么RoundRect函数将画出一个椭圆。
在绘制图5-8所示的圆角矩形时,用了下面的公式来计算角上椭圆的尺寸。
xCornerEllipse = (xRight - xLeft) / 4 ; yCornerEllipse = (yBottom- yTop) / 4 ;
这是一种简单的方法,但是结果看起来有点不对劲,因为角的弯曲部分在矩形长的一边要大些。要矫正这一问题,您可以让xCornerEllipse与yCornerEllipse的值相等。
Arc、Chord和Pie函数都只要相同的参数:
Arc(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ; Chord (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ; Pie(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ;
用Arc函数画出的线如图5-9所示;用Chord和Pie函数画出的线分别如图5-10和5-11所示。Windows用一条假想的线将(xStart, yStart)与椭圆的中心连接,从该线与边界框的交点开始,Windows按反时针方向,沿着椭圆画一条弧。Windows还用另一条假想的线将(xEnd,yEnd)与椭圆的中心连接,在该线与边界框的交点处,Windows停止画弧。
图5-9 Arc函数画出的线 |
图5-10 Chord函数画出的线 |
图5-11 Pie函数画出的线 |
对于Arc函数,这样就结束了。因为弧只是一条椭圆形的线而已,而不是一个填入区域。对于Chord函数,Windows连接弧线的端点。而对于Pie函数,Windows将弧的两个端点与椭圆的中心相连接。弦与扇形图的内部以目前画刷填入。
您可能不太明白在Arc、Chord和Pie函数中开始和结束位置的用法,为什么不简单地在椭圆的周在线指定开始和结束点呢?是的,您可以这么做,但是您将不得不算出这些点。Windows的方法在不要求这种精确性的条件下,却完成了相同的工作。
程序5-3 LINEDEMO画一个矩形、一个椭圆、一个圆角矩形和两条线段,不过不是按这一顺序。程序表明了定义封闭区域的函数实际上对这些区域进行了填入,因为在椭圆后面的线被遮住了,结果如图5-12中所示。
程序5-3 LINEDEMO LINEDEMO.C /*--------------------------------------------------------- LINEDEMO.C -- Line-Drawing Demonstration Program (c) Charles Petzold, 1998 ----------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("LineDemo") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Line Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rectangle (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, cxClient, cyClient) ; MoveToEx (hdc, 0, cyClient, NULL) ; LineTo (hdc, cxClient, 0) ; Ellipse (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; RoundRect (hdc, cxClient / 4, cyClient / 4, 3 * cxClient / 4, 3 * cyClient / 4, cxClient / 4, cyClient / 4) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
图5-12 LINEDEMO显示 |
贝塞尔曲线
「曲尺」这个词从前指的是一片木头、橡皮或者金属,用来在纸上画曲线。比如说,如果您有一些不同图点,您想要在它们之间画一条曲线(内插或者外插),您首先将这些点描在绘图纸上,然后,将曲尺定在这些点上,并用铅笔沿着曲尺绕着这些点弯曲的方向画曲线。
当然,时至今日,曲尺已经数学公式化了。有很多种不同的曲尺公式,它们各有千秋。贝塞尔曲线是计算机程序设计中用得最广的曲尺公式之一,它是直到最近才加到操作系统层次的图形支持中的。在六十年代Renault汽车公司进行了由手工设计车体(要用到粘土)到计算机辅助设计的转变。他们需要一些数学工具,而Pierm Bezier找到了一套公式,最后显示出这套公式应付这样的工作非常有用。
此后,二维的贝塞尔曲线成了计算机图学中最有用的曲线(在直线和椭圆之后)。在PostScript中,所有曲线都用贝塞尔曲线表示-椭圆线用贝塞尔曲线来逼近。贝塞尔曲线也用于定义PostScript字体的字符轮廓(TrueType使用一种更简单更快速的曲尺公式)。
一条二维的贝塞尔曲线由四个点定义-两个端点和两个控制点。曲线的端点在两个端点上,控制点就好像「磁石」一样把曲线从两个端点间的直线处拉走。这一点可以由底下的BEZIER互动交谈程序做出最好的展示,如程序5-4所示。
程序5-4 BEZIER BEZIER.C /*----------------------------------------------------------------------- BEZIER.C -- Bezier Splines Demo (c) Charles Petzold, 1998 ------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Bezier") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Bezier Splines"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBezier (HDC hdc, POINT apt[]) { PolyBezier (hdc, apt, 4) ; MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; LineTo (hdc, apt[1].x, apt[1].y) ; MoveToEx (hdc, apt[2].x, apt[2].y, NULL) ; LineTo (hdc, apt[3].x, apt[3].y) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static POINT apt[4] ; HDC hdc ; int cxClient, cyClient ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; apt[0].x = cxClient / 4 ; apt[0].y = cyClient / 2 ; apt[1].x = cxClient / 2 ; apt[1].y = cyClient / 4 ; apt[2].x = cxClient / 2 ; apt[2].y = 3 * cyClient / 4 ; apt[3].x = 3 * cxClient / 4 ; apt[3].y = cyClient / 2 ; return 0 ; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MOUSEMOVE: if (wParam & MK_LBUTTON || wParam & MK_RBUTTON) { hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (WHITE_PEN)) ; DrawBezier (hdc, apt) ; if (wParam & MK_LBUTTON) { apt[1].x = LOWORD (lParam) ; apt[1].y = HIWORD (lParam) ; } if (wParam & MK_RBUTTON) { apt[2].x = LOWORD (lParam) ; apt[2].y = HIWORD (lParam) ; } SelectObject (hdc, GetStockObject (BLACK_PEN)) ; DrawBezier (hdc, apt) ; ReleaseDC (hwnd, hdc) ; } return 0 ; case WM_PAINT: InvalidateRect (hwnd, NULL, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; DrawBezier (hdc, apt) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
由于这个程序要用到一些在第七章才讲的鼠标处理方式,所以我不在这里讨论它的内部运作(不过,这也是简单的),而是用这个程序来实验性地操纵贝塞尔曲线。在这个程序中,两个顶点设定在显示区域的上下居中、左右位于1/4和3/4处的位置;两个控制点可以改变,按住鼠标左键或右键并拖动鼠标可以分别改动两个控制点之一。图5-13是一个典型的例子。
除了贝塞尔曲线本身,程序还从第一个控制点向左边的第一个端点(也叫做开始点)画一条直线,并从第二个控制点向右边的端点画一条直线。
由于下面几个特点,贝塞尔曲线在计算机辅助设计中非常有用。首先,经过少量练习,就可以把曲线调整到与想要的形状非常接近。
图5-13 BEZIER程序的显示 |
其次,贝塞尔曲线非常好控制。对于有的曲尺种类来说,曲线不经过任何一个定义该曲线的点。贝塞尔曲线总是由其两个端点开始和结束的(这是在推导贝塞尔公式时所做的假设之一)。另外,有些形式的曲尺公式有奇异点,在这些点处曲线趋向无穷远,这在计算机辅助设计中通常是很不合适的。事实上,贝塞尔曲线总是受限于一个四边形(叫做「凸包」),这个四边形由端点和控制点连接而成。
第三个特点涉及端点和控制点之间的关系。曲线总是与第一个控制点到起点的直线相切,并保持同一方向;同时,也与第二个控制点到终点的直线相切,并保持同一方向。这是用于推导贝塞尔公式时所做的另外两个假设。
第四,贝塞尔曲线通常比较具有美感。我知道这是一个主观评价的问题,不过,并非只有我才这样想。
在32位的Windows版本之前,您必须利用Polyline来自己建立贝塞尔曲线,并且还需要知道下面的贝塞尔曲线的参数方程。起点是( x0,y0),终点是( x3,y3),两个控制点是(x1,y1)和(x2,y2),随着t的值从0到1的变化,就可以画出曲线:
x(t) = (1 - t)3 x0 + 3t (1 - t)2 x1 + 3t2 (1 - t) x2 + t3 x3 y(t) = (1 - t)3 y0 + 3t (1 - t)2 y1 + 3t2 (1 - t) y2 + t3 y3
在Windows 98中,您不需要知道这些公式。要画一条或多条连接的贝塞尔曲线,只需呼叫:
PolyBezier (hdc, apt, iCount) ;
或
PolyBezierTo (hdc, apt, iCount) ;
两种情况下,apt都是POINT结构的数组。对PolyBezier,前四个点(按照顺序)给出贝塞尔曲线的起点、第一个控制点、第二个控制点和终点。此后的每一条贝塞尔曲线只需给出三个点,因为后一条贝塞尔曲线的起点就是前一条贝塞尔曲线的终点,如此类推。iCount参数等于1加上您所绘制的这些首尾相接曲线条数的三倍。
PolyBezierTo函数使用目前点作为第一个起点,第一条以及后续的贝塞尔曲线都只需要给出三个点。当函数传回时,目前点设定为最后一个终点。
一点提示:在画一系列相连的贝塞尔曲线时,只有当第一条贝塞尔曲线的第二个控制点、第一条贝塞尔曲线的终点(也就是第二条曲线的起点)和第二条贝塞尔曲线的第一个控制点线性相关时,也就是说这三个点在同一条直线上时,曲线在连接点处才是光滑的。
使用现有画笔(Stock
Pens)
当您呼叫这一节中讨论的任何画线函数时,Windows使用设备内容中目前选中的「画笔」来画线。画笔决定线的色彩、宽度和画笔样式,画笔样式可以是实线、点划线或者虚线,内定设备内容中画笔为BLACK_PEN。不管映像方式是什么,这种画笔都画出一个图素宽的黑色实线来。BLACK_PEN是Windows提供的三种现有画笔之一,其它两种是WHITE_PEN和NULL_PEN,NULL_PEN什么都不画。您也可以自己自订画笔。
Windows程序以句柄来使用画笔。 Windows表头文件WINDEF.H中包含一个叫做HPEN的型态定义,即画笔的句柄,可以定义这个型态的变量(例如hPen):
HPEN hPen ;
呼叫GetStockObject,可以获得现有画笔的句柄。例如,假设您想使用名为WHITE_PEN的现有画笔,可以如下取得画笔的句柄:
hPen = GetStockObject (WHITE_PEN) ;
现在必须将画笔选进设备内容:
SelectObject (hdc, hPen) ;
目前的画笔是白色。在这个呼叫后,您画的线将使用WHITE_PEN,直到您将另外一个画笔选进设备内容或者释放设备内容句柄为止。
您也可以不定义hPen变量,而将GetStockObject和SelectObject呼叫合并成一个叙述:
SelectObject (hdc, GetStockObject (WHITE_PEN)) ;
如果想恢复到使用BLACK_PEN的状态,可以用一个叙述取得这种画笔的句柄,并将其选进设备内容:
SelectObject (hdc, GetStockObject (BLACK_PEN)) ;
SelectObject的传回值是此呼叫前设备内容中的画笔句柄。如果启动一个新的设备内容并呼叫
hPen = SelectObject (hdc, GetStockobject (WHITE_PEN)) ;
则设备内容中的目前画笔将为WHITE_PEN,变量hPen将会是BLACK_PEN的句柄。以后通过呼叫
SelectObject (hdc, hPen) ;
就能够将BLACK_PEN选进设备内容。
画笔的建立、选择和删除
尽管使用现有画笔非常方便,但却受限于实心的黑画笔、实心的白画笔或者没有画笔这三种情况。如果想得到更丰富多彩的效果,就必须建立自己的画笔。
这一过程通常是:使用函数CreatePen或CreatePenIndirect建立一个「逻辑画笔」,这仅仅是对画笔的描述。这些函数传回逻辑画笔的句柄;然后,呼叫SelectObject将画笔选进设备内容。现在,就可以使用新的画笔来画线了。在任何时候,都只能有一种画笔选进设备内容。在释放设备内容(或者在选择了另一种画笔到设备内容中)之后,就可以呼叫DeleteObject来删除所建立的逻辑画笔了。在删除后,该画笔的句柄就不再有效了。
逻辑画笔是一种「GDI对象」,它是您可以建立的六种GDI对象之一,其它五种是画刷、位图、区域、字体和调色盘。除了调色盘之外,这些对象都是通过SelectObject选进设备内容的。
在使用画笔等GDI对象时,应该遵守以下三条规则:
- 最后要删除自己建立的所有GDI对象。
- 当GDI对象正在一个有效的设备内容中使用时,不要删除它。
- 不要删除现有对象。
这些规则当然是有道理的,而且有时这道理还挺微妙的。下面我们将举些例子来帮助理解这些规则。
CreatePen函数的语法形如:
hPen = CreatePen (iPenStyle, iWidth, crColor) ;
其中,iPenStyle参数确定画笔是实线、点线还是虚线,该参数可以是WINGDI.H表头文件中定义的以下标识符,图5-14显示了每种画笔产生的画笔样式。
图5-14 七种画笔样式 |
对于PS_SOLID、PS_NULL和PS_INSIDEFRAME画笔样式,iWidth参数是画笔的宽度。iWidth值为0则意味着画笔宽度为一个图素。现有画笔是一个图素宽。如果指定的是点划线或者虚线式画笔样式,同时又指定一个大于1的实际宽度,那么Windows将使用实线画笔来代替。
CreatePen的crColor参数是一个COLORREF值,它指定画笔的颜色。对于除了PS_INSIDEFRAME之外的画笔样式,如果将画笔选入设备内容中,Windows会将颜色转换为设备所能表示的最相近的纯色。PS_INSIDEFRAME是唯一一种可以使用混色的画笔样式,并且只有在宽度大于1的情况下才如此。
在与定义一个填入区域的函数一起使用时,PS_INSIDEFRAME画笔样式还有另外一个奇特之处:对于除了PS_INSIDEFRAME以外的所有画笔样式来说,如果用来画边界框的画笔宽度大于1个图素,那么画笔将居中对齐在边界框在线,这样边界框线的一部分将位于边界框之外;而对于PS_INSIDEFRAME画笔样式来说,整条边界框线都画在边界框之内。
您也可以通过建立一个型态为LOGPEN(「逻辑画笔」)的结构,并呼叫CreatePenIndirect来建立画笔。如果您的程序使用许多能在原始码中初始化的画笔,那么使用这种方法将有效得多。
要使用CreatePenIndirect,首先定义一个LOGPEN型态的结构:
LOGPEN logpen ;
此结构有三个成员:lopnStyle(无正负号整数或UINT)是画笔样式,lopnWidth(POINT结构)是按逻辑单位度量的画笔宽度,lopnColor (COLORREF)是画笔颜色。Windows只使用lopnWidth结构的x值作为画笔宽度,而忽略y值。
将结构的地址传递给CreatePenIndirect结构就可以建立画笔了:
hPen = CreatePenIndirect (&logpen) ;
注意,CreatePen和CreatePenIndirect函数不需要设备内容句柄作为参数。这些函数建立与设备内容没有联系的逻辑画笔。直到呼叫SelectObject之后,画笔才与设备内容发生联系。因此,可以对不同的设备(如屏幕和打印机)使用相同的逻辑画笔。
下面是建立、选择和删除画笔的一种方法。假设您的程序使用三种画笔-一种宽度为1的黑画笔、一种宽度为3的红画笔和一种黑色点式画笔,您可以先定义三个变量来存放这些画笔的句柄:
static HPEN hPen1, hPen2, hPen3 ;
在处理WM_CREATE期间,您可以建立这三种画笔:
hPen1 = CreatePen (PS_SOLID, 1, 0) ; hPen2 = CreatePen (PS_SOLID, 3, RGB (255, 0, 0)) ; hPen3 = CreatePen (PS_DOT, 0, 0) ;
在处理WM_PAINT期间,或者是在拥有一个设备内容有效句柄的任何时间里,您都可以将这三个画笔之一选进设备内容并用它来画线:
SelectObject (hdc, hPen2) ;
画线函数
SelectObject (hdc, hPen1) ;
其它画线函数
在处理WM_DESTROY期间,您可以删除您建立的三种画笔:
DeleteObject (hPen1) ; DeleteObject (hPen2) ; DeleteObject (hPen3) ;
这是建立、选择和删除画笔最直接的方法。但是您的程序必须知道执行期间需要哪些逻辑画笔,为此,您可能想要在每个WM_PAINT消息处理期间建立画笔,并在呼叫EndPaint之后删除它们(您可以在呼叫EndPaint之前删除它们,但是要小心,不要删除设备内容中目前选择的画笔)。
您可能还希望随时建立画笔,并将CreatePen和SelectObject呼叫组合到同一个叙述中:
SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
现在再开始画线,您将使用一个红色虚线画笔。在画完红色虚线之后,可以删除画笔。糟了!由于没有保存画笔句柄,怎么才能删除这些画笔呢?不要紧,请记住,SelectObject将传回设备内容中上一次选择的画笔句柄。所以,您可以通过呼叫SelectObject将BLACK_PEN选进设备内容,并删除从SelectObject传回的值:
DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ;
下面是另一种方法,在将新建立的画笔选进设备内容时,保存SelectObject传回的画笔句柄:
hPen = SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
现在hPen是什么呢?如果这是在取得设备内容之后第一次呼叫SelectObject,则hPen是BLACK_PEN对象的句柄。现在,可以将hPen选进设备内容,并删除所建立的画笔(第二次SelectObject呼叫传回的句柄),只要一道叙述即可:
DeleteObject (SelectObject (hdc, hPen)) ;
如果有一个画笔的句柄,就可以通过呼叫GetObject取得LOGPEN结构各个成员的值:
GetObject (hPen, sizeof (LOGPEN), (LPVOID) &logpen) ;
如果需要目前选进设备内容的画笔句柄,可以呼叫:
hPen = GetCurrentObject (hdc, OBJ_PEN) ;
在第十七章将讨论另一个建立画笔的函数ExtCreatePen。
填入空隙
使用点式画笔和虚线画笔会产生一个有趣的问题:点和虚线之间的空隙会怎样呢?您所需要的是什么?
空隙的着色取决于设备内容的两个属性-背景模式和背景颜色。内定背景模式为OPAQUE,在这种方式下,Windows使用背景色来填入空隙,内定的背景色为白色。这与许多程序在窗口类别中用WHITE_BRUSH来擦除窗口背景的做法是一致的。
您可以通过如下呼叫来改变Windows用来填入空隙的背景色:
SetBkColor (hdc, crColor) ;
与画笔色彩所使用的crColor参数一样,Windows将这里的背景色转换为纯色。可以通过用GetBkColor来取得设备内容中定义的目前背景色。
通过将背景模式转换为TRANSPARENT,可以阻止Windows填入空隙:
SetBkMode (hdc, TRANSPARENT) ;
此后,Windows将忽略背景色,并且不填入空隙,可以通过呼叫GetBkMode来取得目前背景模式(TRANSPARENT或者OPAQUE)。
绘图方式
设备内容中定义的绘图方式也影响显示器上所画线的外观。设想画这样一条直线,它的色彩由画笔色彩和画线区域原来的色彩共同决定。设想用同一种画笔在白色表面上画出黑线而在黑色表面上画出白线,而且不用知道表面是什么色彩。这样的功能对您有用吗?通过绘图方式的设定,这些都可以实作。
当Windows使用画笔来画线时,它实际上执行画笔图素与目标位置处原来图素之间的某种位布尔运算。图素间的位布尔运算叫做「位映像运算」,简称为「ROP」。由于画一条直线只涉及两种图素(画笔和目标),因此这种布尔运算又称为「二元位映像运算」,简记为「ROP2」。Windows定义了16种ROP2代码,表示Windows组合画笔图素和目标图素的方式。在内定设备内容中,绘图方式定义为R2_COPYPEN,这意味着Windows只是将画笔图素复制到目标图素,这也是我们通常所熟知的。此外,还有15种ROP2码。
16种不同的ROP2码是怎样得来的呢?为了示范的需要,我们假设使用单色系统,目标色(窗口显示区域的色彩)为黑色(用0来表示)或者白色(用1来表示),画笔也可以为黑色或者白色。用黑色或者白色画笔在黑色或者白色目标上画图有四种组合:白笔与白目标、白笔与黑目标、黑笔与白目标、黑笔与黑目标。
画笔在目标上绘制后会得到什么呢?一种可能是不管画笔和目标的色彩,画出的线总是黑色的,这种绘图方式由ROP2代码R2_BLACK表示。另一种可能是只有当画笔与目标都为黑色时,画出的结果才是白色,其它情况下画出的都是黑色。尽管这似乎有些奇怪,Windows还是为这种方式起了一个名字,叫做R2_NOTMERGEPEN。Windows执行目标图素与画笔图素的位「或」运算,然后翻转所得色彩。
表5-2显示了所有16种ROP2绘图方式,表中指示了画笔色彩(P)与目标色彩(D)是如何组合而成结果色彩的。在标有「布尔操作」的那一栏中,用C语言的表示法给出了目标图素与画笔图素的组合方式。
表5-2 |
画笔(P):目标(D): |
1 1 |
1 0 |
0 1 |
0 0 |
布尔操作 |
绘图模式 |
结果: |
0 |
0 |
0 |
0 |
0 |
R2_BLACK |
0 |
0 |
0 |
1 |
~(P | D) |
R2_NOTMERGEPEN |
|
0 |
0 |
1 |
0 |
~P & D |
R2_MASKNOTPEN |
|
0 |
0 |
1 |
1 |
~P |
R2_NOTCOPYPEN |
|
0 |
1 |
0 |
0 |
P & ~D |
R2_MASKPENNOT |
|
0 |
1 |
0 |
1 |
~D |
R2_NOT |
|
0 |
1 |
1 |
0 |
P ^ D |
R2_XORPEN |
|
0 |
1 |
1 |
1 |
~(P & D) |
R2_NOTMASKPEN |
|
1 |
0 |
0 |
0 |
P & D |
R2_MASKPEN |
|
1 |
0 |
0 |
1 |
~(P ^ D) |
R2_NOTXORPEN |
|
1 |
0 |
1 |
0 |
D |
R2_NOP |
|
1 |
0 |
1 |
1 |
~P | D |
R2_MERGENOTPEN |
|
1 |
1 |
0 |
0 |
P |
R2_COPYPEN(内定) |
|
1 |
1 |
0 |
1 |
P | ~D |
R2_MERGEPENNOT |
|
1 |
1 |
1 |
0 |
P | D |
R2_MERGEPEN |
|
1 |
1 |
1 |
1 |
1 |
R2_WHITE |
可以通过以下呼叫在设备内容中设定新的绘图模式:
SetROP2 (hdc, iDrawMode) ;
iDrawMode参数是表中「绘图模式」一栏中给出的值之一。您可以用函数:
iDrawMode = GetROP2 (hdc) ;
来取得目前绘图方式。设备内容中的内定设定为R2_COPYPEN,它用画笔色彩替代目标色彩。在R2_NOTCOPYPEN方式下,若画笔为黑色,则画成白色;若画笔为白色,则画成黑色。R2_BLACK方式下,不管画笔和背景色为何种色彩,总是画成黑色。与此相反,R2_WHITE方式下总是画成白色。R2_NOP方式就是「不操作」,让目标保持不变。
现在,我们已经讨论了单色系统。然而,大多数系统是彩色的。在彩色系统中,Windows为画笔和目标图素的每个颜色位执行绘图方式的位运算,并再次使用上表描述的16种ROP2代码。R2_NOT绘图方式总是翻转目标色彩来决定线的颜色,而不管画笔的色彩是什么。例如,在青色目标上的线会变成紫色。R2_NOT方式总是产生可见的画笔,除非画笔在中等灰度的背景上绘图。我将在 第七章的BLOKOUT程序中展示R2_NOT绘图方式的使用。
现在再更进一步,从画线到画图形。Windows中七个用来画带边缘的填入图形的函数列于表5-3中。
表5-3 |
函数 |
图形 |
Rectangle |
直角矩形 |
Ellipse |
椭圆 |
RoundRect |
圆角矩形 |
Chord |
椭圆周上的弧,两端以弦连接 |
Pie |
椭圆上的饼图 |
Polygon |
多边形 |
PolyPolygon |
多个多边形 |
Windows用设备内容中选择的目前画笔来画图形的边界框,边界框还使用目前背景方式、背景色彩和绘图方式,这跟Windows画线时一样。关于直线的一切也适用于这些图形的边界框。
图形以目前设备内容中选择的画刷来填入。内定情况下,使用现有对象,这意味着图形内部将画为白色。Windows定义六种现有画刷:WHITE_BRUSH、LTGRAY_BRUSH、GRAY_BRUSH、DKGRAY_BRUSH、BLACK_BRUSH和NULL_BRUSH (也叫HOLLOW_BRUSH)。您可以将任何一种现有画刷选入您的设备内容中,就和您选择一种画笔一样。Windbws将HBRUSH定义为画刷的句柄,所以可以先定义一个画刷句柄变量:
HBRUSH hBrush ;
您可以通过呼叫GetStockObject来取得GRAY_BRUSH的句柄:
hBrush = GetStockObject (GRAY_BRUSH) ;
您可以呼叫SelectObject将它选进设备内容:
SelectObject (hdc, hBrush) ;
现在,如果您要画上表中的任一个图形,则其内部将为灰色。
如果您想画一个没有边界框的图形,可以将NULL_PEN选进设备内容:
SelectObject (hdc, GetStockObject (NULL_PEN)) ;
如果您想画出图形的边界框,但不填入内部,则将NULL_BRUSH选进设备内容:
SelectObject (hdc, GetStockobject (NULL_BRUSH) ;
您也可以自订画刷,就如同您自订画笔一样。我们将马上谈到这个问题。
Polygon函数和多边形填入方式
我已经讨论过了前五个区域填入函数,Polygon是第六个画带边界框的填入图形的函数,该函数的呼叫与Polyline函数相似:
Polygon (hdc, apt, iCount) ;
其中,apt参数是POINT结构的一个数组,iCount是点的数目。如果该数组中的最后一个点与第一个点不同,则Windows将会再加一条线,将最后一个点与第一个点连起来(在Polyline函数中,Windows不会这么做)。PolyPolygon函数如下所示:
PolyPolygon (hdc, apt, aiCounts, iPolyCount) ;
该函数绘制多个多边形。最后一个参数给出了所画的多边形的个数。对于每个多边形,aiCounts数组给出了多边形的端点数。apt数组具有全部多边形的所有点。除传回值以外,PolyPolygon在功能上与下面的代码相同:
for (i = 0, iAccum = 0 ; i < iPolyCount ; i++) { Polygon (hdc, apt + iAccum, aiCounts[i]) ; iAccum += aiCounts[i] ; }
对于Polygon和PolyPolygon函数,Windows使用定义在设备内容中的目前画刷来填入这个带边界的区域。至于填入内部的方式,则取决于多边形填入方式,您可以用SetPolyFillMode函数来设定:
SetPolyFillMode (hdc, iMode) ;
内定情况下,多边形填入方式是ALTERNATE,但是您可以将它设定为WINDING。两种方式的区别参见图5-15所示。
图5-15 用两种多边形填入方式画出的图:ALTERNATE(左)和WINDING(右) |
首先,ALTERNATE和WINDING方式之间的区别很容易察觉。对于ALTERNATE方式,您可以设想从一个无穷大的封闭区域内部的点画线,只有假想的线穿过了奇数条边界线时,才填入封闭区域。这就是填入了星的角而中心没被填入的原因。
五角星的例子使得WINDING方式看起来比实际上更简单一些。在绘制单个的多边形时,大多数情况下,WINDING方式会填入所有封闭的区域。但是也有例外。
在WINDING方式下要确定一个封闭区域是否被填入,您仍旧可以设想从那个无穷大的区域画线。如果假想的线穿过了奇数条边界线,区域就被填入,这和ALTERNATE方式一样。如果假想的线穿过了偶数条边界线,则区域可能被填入也可能不被填入。如果一个方向(相对于假想线)的边界线数与另一个方向的边界线数不相等,就填入区域。
例如,考虑图5-16中的物体。在线的箭头指出了画线的方向。两种方式都会填入三个封闭的L形区域,号码从1到3。号码为4和5的两个小内部区域,在ALTERNATE方式下不会被填入。但是,在WINDING方式下,号码为5的区域会被填入,因为从区域内必须穿过两条相同方向的线才能到达图形外部。号码为4的区域不会被填入,因为必须穿过两条方向相反的线。
如果您怀疑Windows没有这么聪明,那么程序5-5 ALTWIND会展示给您看。
图5-16 WINDING方式不能填入所有内部区域的图形 |
程序5-5 ALTWIND ALTWIND.C /*------------------------------------------------------------------- ALTWIND.C -- Alternate and Winding Fill Modes (c) Charles Petzold, 1998 -------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("AltWind") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Alternate and Winding Fill Modes"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static POINT aptFigure [10] = {10,70, 50,70, 50,10, 90,10, 90,50, 30,50, 30,90, 70,90, 70,30, 10,30 }; static int cxClient, cyClient ; HDC hdc ; int i ; PAINTSTRUCT ps ; POINT apt[10] ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (GRAY_BRUSH)) ; for (i = 0 ; i < 10 ; i++) { apt[i].x = cxClient * aptFigure[i].x / 200 ; apt[i].y = cyClient * aptFigure[i].y / 100 ; } SetPolyFillMode (hdc, ALTERNATE) ; Polygon (hdc, apt, 10) ; for (i = 0 ; i < 10 ; i++) { apt[i].x += cxClient / 2 ; } SetPolyFillMode (hdc, WINDING) ; Polygon (hdc, apt, 10) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
图形的坐标(划分为100×100个单位)储存在aptFigure数组中。这些坐标是依据显示区域的宽度和高度划分的。程序显示图形两次,一次使用ALTERNATE填入方式,另一次使用WINDING方式。结果见图5-17。
图5-17 ALTWIND的显示 |
用画刷填入内部
Rectangle、RoundRect、Ellipse、Chord、Pie、Polygon和PolyPolygon图形的内部是用选进设备内容的目前画刷(也称为「图样」)来填入的。画刷是一个8×8的位图,它水平和垂直地重复使用来填入内部区域。
当Windows用混色的方法来显示多于可从显示器上得到的色彩时,实际上是将画刷用于色彩。在单色系统上,Windows能够使用黑色和白色图素的混色建立64种不同的灰色,更精确地说,Windows能够建立64种不同的单色画刷。对于纯黑色,8×8位图中的所有位均为0。第一种灰色有一位为1,第二种灰色有两位为1,以此类推,直到8×8位图中所有位均为1,这就是白色。在16色或256色显示系统上,混色也是位图,并且可以得到更多的色彩。
Windows还有五个函数,可以让您建立逻辑画刷,然后就可使用SelectObject将画刷选进设备内容。与逻辑画笔一样,逻辑画刷也是GDI对象。您建立的所有画刷都必须被删除,但是当它还在设备内容中时不能将其删除。
下面是建立逻辑画刷的第一个函数:
hBrush = CreateSolidBrush (crColor) ;
函数中的Solid并不是指画刷为纯色。在将画刷选入设备内容中时,Windows建立一个混色色的位图,并为画刷使用该位图。
您还可以使用由水平、垂直或者倾斜的线组成的「影线标记(hatch marks)」来建立画刷,这种风格的画刷对着色条形图的内部和在绘图机上进行绘图最有用。建立影线画刷的函数为:
hBrush = CreateHatchBrush (iHatchStyle, crColor) ;
iHatchStyle参数描述影线标记的外观。图5-18显示了六种可用的影线标记风格。
图5-18 六种影线画刷风格 |
CreateHatchBrush中的crColor参数是影线的色彩。在将画刷选进设备内容时,Windows将这种色彩转换为与之最相近的纯色。影线之间的区域根据设备内容中定义的背景方式和背景色来着色。如果背景方式为OPAQUE,则用背景色(它也被转换为纯色)来填入线之间的空间。在这种情况下,影线和填入色都不能是混色而成的颜色。如果背景方式为TRANSPARENT,则Windows只画出影线,不填入它们之间的区域。
您也可以使用CreatePatternBrush和CreateDIBPatternBrushPt建立自己的位图画刷。
建立逻辑画刷的第五个函数包含其它四个函数:
hBrush = CreateBrushIndirect (&logbrush) ;
变量logbrush是一个型态为LOGBRUSH(「逻辑画刷」)的结构,该结构的三个字段如表5-4所示,lbStyle字段的值确定了Windows如何解释其它两个字段的值:
表5-4 |
lbStyle (UINT) |
lbColor (COLORREF) |
lbHatch (LONG) |
BS_SOLID |
画刷的色彩 |
忽略 |
BS_HOLLOW |
忽略 |
忽略 |
BS_HATCHED |
影线的色彩 |
影线画刷风格 |
BS_PATTERN |
忽略 |
位图的句柄 |
BS_DIBPATTERNPT |
忽略 |
指向DIB的指标 |
前面我们用SelectObject将逻辑画笔选进设备内容,用DeleteObject删除画笔,用GetObject来取得逻辑画笔的信息。对于画刷,同样能使用这三个函数。一旦您取得到了画刷句柄,就可以使用SelectObject将该画刷选进设备内容:
SelectObject (hdc, hBrush) ;
然后,您可以使用DeleteObject函数删除所建立的画刷:
DeleteObject (hBrush) ;
但是,不要删除目前选进设备内容的画刷。
如果您需要取得画刷的信息,可以呼叫GetObject:
GetObject (hBrush, sizeof (LOGBRUSH), (LPVOID) &logbrush) ;
其中,logbrush是一个型态为LOGBRUSH的结构。
到目前为止,所有的程序都是相对于显示区域的左上角,以图素为单位绘图的。这是内定情况,但不是唯一选择。事实上,「映像方式」是一种几乎影响任何显示区域绘图的设备内容属性。另外有四种设备内容属性-窗口原点、视端口原点、窗口范围和视端口范围-与映像方式密切相关。
大多数GDI绘图函数需要坐标值或大小。例如,下面是TextOut函数:
TextOut (hdc, x, y, psText, iLength) ;
参数x和y分别表示文字的开始位置。参数x是在水平轴上的位置,参数y是在垂直轴上的位置,通常用(x,y)来表示这个点。
在TextOut中,以及在几乎所有GDI函数中,这些坐标值使用的都是一种「逻辑单位」。Windows必须将逻辑单位转换为「设备单位」,即图素。这种转换是由映像方式、窗口和视端口的原点以及窗口和视端口的范围所控制的。映像方式还指示着x轴和y轴的方向(orientation);也就是说,它确定了当您在向显示器的左或者右移动时x的值是增大还是减小,以及在上下移动时y的值是增大还是减小。
Windows定义了8种映像方式,它们在WINGDI.H中相应的标识符和含义如表5-5所示。
表5-5 |
映像方式 |
逻辑单位 |
增加值 |
|
x值 |
y值 |
||
MM_TEXT |
图素 |
右 |
下 |
MM_LOMETRIC |
0.1 mm |
右 |
上 |
MM_HIMETRIC |
0.01 mm |
右 |
上 |
MM_LOENGLISH |
0.01 in. |
右 |
上 |
MM_HIENGLISH |
0.001 in. |
右 |
上 |
MM_TWIPS |
1/1440 in. |
右 |
上 |
MM_ISOTROPIC |
任意(x = y) |
可选 |
可选 |
MM_ANISOTROPIC |
任意(x != y) |
可选 |
可选 |
METRIC和ENGLISH指一般通行的度量衡系统,点是印刷的测量单位,约等于1/72英寸,但在图形程序设计中假定为正好1/72英寸。「Twip」等于1/20点,也就是1/1440英寸。「Isotropic」和「anisotropic」是真正的单字,意思是「等方性」(同方向)和「异方性」(不同方向)。
您可以使用下面的叙述来设定映射方式:
SetMapMode (hdc, iMapMode) ;
其中,iMapMode是8个映像方式标识符之一。您可以通过以下呼叫取得目前的映像方式:
iMapMode = GetMapMode (hdc) ;
内定映像方式为MM_TEXT。在这种映像方式下,逻辑单位与实际单位相同,这样我们可以直接以图素为单位进行操作。在TextOut呼叫中,它看起来像这样:
TextOut (hdc, 8, 16, TEXT ("Hello"), 5) ;
文字从距离显示区域左端8图素、上端16图素的位置处开始。
如果映像方式设定为MM_LOENGLISH:
SetMapMode (hdc, MM_LOENGLISH) ;
则逻辑单位是百分之一。现在,TextOut呼叫如下:
TextOut (hdc, 50, -100, TEXT ("Hello"), 5) ;
文字从距离显示区域左端0.5英寸、上端1英寸的位置处开始。至于y坐标前面的负号,随着我们对映像方式更详细的讨论,将逐渐清楚。其它映像方式允许程序按照毫米、打印机的点大小或者任意单位的坐标轴来指定坐标。
如果您认为使用图素进行工作很合适,那么就不要使用内定的MM_TEXT方式外的任何映像方式。如果需要以英寸或者毫米尺寸显示图像,那么可以从GetDeviceCaps中取得所需要的信息,自己再进行缩放。其它映像方式都是避免您自己进行缩放的一个方便途径而已。
虽然您在GDI函数中指定的坐标是32位的值,但是仅有Windows NT能够处理全32位。在Windows 98中,坐标被限制为16位,范围从-32,768到32,767。一些使用坐标表示矩形的开始点和结束点的Windows函数也要求矩形的宽和高小于或者等于32,767。
设备坐标和逻辑坐标
您也许会问:如果使用MM_LOENGLISH映射方式,是不是将会得到以百分之一英寸为单位的WM_SIZE消息呢?绝对不会。Windows对所有消息(如WM_MOVE、WM_SIZE和WM_MOUSEMOVE),对所有非GDI函数,甚至对一些GDI函数,永远使用设备坐标。可以这样来考虑:由于映像方式是一种设备内容属性,所以,只有对需要设备内容句柄作参数的GDI函数,映像方式才会起作用。GetSystemMetrics不是GDI函数,所以它总是以设备单位(即图素)为量度来传回大小的。尽管GetDeviceCaps是GDI函数,需要一个设备内容句柄作为参数,但是Windows仍然对HORZRES和VERTRES以设备单位作为传回值,因为该函数的目的之一就是给程序提供以图素为单位的设备大小。
不过,从GetTextMetrics呼叫中传回的TEXTMETRIC结构的值是使用逻辑单位的。如果在进行此呼叫时映像方式为MM_LOENGLISH,则GetTextMetrics将以百分之一英寸为单位提供字符的宽度和高度。在呼叫GetTextMetrics以取得关于字符的宽度和高度信息时,映像方式必须设定成根据这些信息输出文字时所使用的映像方式,这样就可以简化工作。
设备坐标系
Windows将GDI函数中指定的逻辑坐标映像为设备坐标。在讨论以各种不同的映像方式使用逻辑坐标系之前,我们先来看一下Windows为视讯显示器区域定义的不同的设备坐标系。尽管我们大多数时间在窗口的显示区域内工作,但Windows在不同的时间使用另外两种设备坐标区域。所有设备坐标系都以图素为单位,水平轴(即x轴)上的值从左到右递增,垂直轴(即y轴)上的值从上到下递增。
当我们使用整个屏幕时,就根据「屏幕坐标」进行操作。屏幕的左上角为(0,0)点,屏幕坐标用在WM_MOVE消息(对于非子窗口)以及下列Windows函数中:CreateWindow和MoveWindow(都是对于非子窗口)、GetMessagePos、GetCursorPos、SetCursorPos、GetWindowRect以及WindowFromPoint(这不是全部函数的列表)。它们或者是与窗口无关的函数(如两个光标函数),或者是必须相对于某个屏幕点来移动(或者寻找)窗口的函数。如果以DISPLAY为参数呼叫CreateDC,以取得整个屏幕的设备内容,则内定情况下GDI呼叫中指定的逻辑坐标将被映像为屏幕坐标。
「全窗口坐标」以程序的整个窗口为基准,如标题列、菜单、滚动条和窗口框都包括在内。而对于普通窗口,点(0,0)是缩放边框的左上角。全窗口坐标在Windows中极少使用,但是如果用GetWindowDC取得设备内容,GDI函数中的逻辑坐标就会转换为显示区域坐标。
第三种坐标系是我们最常使用的「显示区域坐标系」。点(0,0)是显示区域的左上角。当使用GetDC或BeginPaint取得设备内容时,GDI函数中的逻辑坐标就会内定转换为显示区域坐标。
用函数ClientToScreen和ScreenToClient可以将显示区域坐标转换为屏幕坐标,或者反过来,将屏幕坐标转换为显示区域坐标。也可以使用GetWindowRect函数取得屏幕坐标下的整个窗口的位置和大小。这三个函数为一种设备坐标转换为另一种提供了足够的信息。
视端口和窗口
映像方式定义了Windows如何将GDI函数中指定的逻辑坐标映像为设备坐标,这里的设备坐标系取决于您用哪个函数来取得设备内容。要继续讨论映像方式,我们需要一些术语:映像方式用于定义从「窗口」(逻辑坐标)到「视端口」(设备坐标)的映像。
「窗口」和「视端口」这两个词用得并不恰当。在其它图形接口语言中,视端口通常包含有剪裁区域的意思,并且,我们已经用窗口来指程序在屏幕上占据的区域。在这里的讨论中,我们必须把关于这些词的先入之见丢到一边。
「视端口」是依据设备坐标(图素)的。通常,视端口和显示区域相同,但是,如果您已经用GetWindowDC或CreateDC取得了一个设备内容,则视端口也可以是指整窗口坐标或者屏幕坐标。点(0,0)是显示区域(或者整个窗口或屏幕)的左上角,x的值向右增加,y的值向下增加。
「窗口」是依据逻辑坐标的,逻辑坐标可以是图素、毫米、英寸或者您想要的任何其它单位。您在GDI绘图函数中指定逻辑窗口坐标。
但是在真正的意义上,视端口和窗口仅是数学上的概念。对于所有的映像方式,Windows都用下面两个公式来将窗口(逻辑)坐标转化为视埠(设备)坐标:
其中,(xWindow,yWindow)是待转换的逻辑点,(xViewport,yViewport)是转换后的设备坐标点,一般情形下差不多就是显示区域坐标了。
这两个公式使用了分别指定窗口和视端口「原点」的点:(xWinOrg,yWinOrg)是逻辑坐标的窗口原点;(xViewOrg,yViewOrg)是设备坐标的视端口原点。在内定的设备内容中,这两个点均被设定为(0,0),但是它们可以改变。此公式意味着,逻辑点(xWinOrg,yWinOrg)总被映像为设备点(xViewOrg,yViewOrg)。如果窗口和视端口的原点是默认值(0,0),则公式简化为:
此公式还使用了两点来指定「范围」:(xWinExt,yWinExt)是逻辑坐标的窗口范围;(xViewExt,yViewExt)是设备坐标的窗口范围。在多数映像方式中,范围是映像方式所隐含的,不能够改变。每个范围自身没有什么意义,但是视端口范围与窗口范围的比例是逻辑单位转换为设备单位的换算因子。
例如,当您设定MM_LOENGLISH映像方式时,Windows将xViewExt设定为某个图素数而将xWinExt设定为xViewExt图素占据的一英寸内有几百图素的长度。比值给出了一英寸内有几百个图素的数值。为了提高转换效能,换算因子表示为整数比而不是浮点数。
范围可以为负,也就是说,逻辑x轴上的值不一定非得在向右时增加;逻辑y轴上的值不一定非得在向下时增加。
Windows也能将视埠(设备)坐标转换为窗口(逻辑)坐标:
Windows提供了两个函数来让您将设备点转换为逻辑点以及将逻辑点转换为设备点。下面的函数将设备点转换为逻辑点:
DPtoLP (hdc, pPoints, iNumber) ;
其中,pPoints是一个指向POINT结构数组的指针,而iNumber是要转换的点的个数。您会发现这个函数对于将GetClientRect(它总是使用设备单位)取得的显示区域大小转换为逻辑坐标很有用:
GetClientRect (hwnd, &rect) ; DPtoLP (hdc, (PPOINT) &rect, 2) ;
下面的函数将逻辑点转换为设备点:
LPtoDP (hdc, pPoints, iNumber) ;
处理MM_TEXT
对于MM_TEXT映像方式,内定的原点和范围如下所示:
窗口原点:(0, 0) 可以改变
视埠原点:(0, 0) 可以改变
窗口范围:(1, 1) 不可改变
视埠范围:(1, 1) 不可改变
视端口范围与窗口范围的比例为1,所以不用在逻辑坐标与设备坐标之间进行缩放。上面所给出的公式可以简化为:
这种映像方式称为「文字」映像方式,不是因为它对于文字最适合,而是由于轴的方向。我们读文字是从左至右,从上至下的,而MM_TEXT以同样的方向定义轴上值的增长方向:
Windows提供了函数SetViewportOrgEx和SetWindowOrgEx,用来改变视端口和窗口的原点,这些函数都具有改变轴的效果,以致(0,0)不再指左上角。一般来说,您会使用SetViewportOrgEx或SetWindowOrgEx之一,但不会同时使用二者。
我们来看一看这些函数有何效果:如果将视埠原点改变为(xViewOrg,yViewOrg),则逻辑点(0.0)就会映像为设备点(xViewOrg,yViewOrg)。如果将窗口原点改变为(xWinOrg,yWinOrg),则逻辑点(xWinOrg,yWinOrg)将会映像为设备点(0,0),即左上角。不管对窗口和视端口原点作什么改变,设备点(0,0)始终是显示区域的左上角。
例如,假设显示区域为cxClient个图素宽和cyClient个图素高。如果想将逻辑点(0,0)定义为显示区域的中心,可进行如下呼叫:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;
SetViewportOrgEx的参数总是使用设备单位。现在,逻辑点(0,0)将映像为设备点(cxClient/2,cyClient/2),而显示区域的坐标系变成如下形状:
逻辑x轴的范围从-cxClient/2到+cxClient/2,逻辑y轴的范围从-cyClient/2到+cyClient/2,显示区域的右下角为逻辑点(cxClient/2,cyClient/2)。如果您想从显示区域的左上角开始显示文字。则需要使用负坐标:
TextOut (hdc, -cxClient / 2, -cyClient / 2, "Hello", 5) ;
用下面的SetWindowOrgEx叙述可以获得与上面使用SetViewportOrgEx同样的效果:
SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ;
SetWindowOrgEx的参数总是使用逻辑单位。在这个呼叫之后,逻辑点(-cxClient / 2,-cyClient / 2)映像为设备点(0,0),即显示区域的左上角。
您不会将这两个函数一起用,除非您知道这么做的结果:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ;
这意味着逻辑点(-cxClient/2,-cyClient/2)将映像为设备点(cxClient/2, cyClient/2),结果是如下所示的坐标系:
您可以使用下面两个函数取得目前视端口和窗口的原点:
GetViewportOrgEx (hdc, &pt) ; GetWindowOrgEx (hdc, &pt) ;
其中pt是POINT结构。由GetViewportOrgEx传回的值是设备坐标,而由GetWindowOrgEx传回的值是逻辑坐标。
您可能想改变视端口或者窗口的原点,以改变窗口显示区域内的显示输出-例如,响应使用者在滚动条内的输入。但是,改变视端口和窗口原点并不能立即改变显示输出,而必须在改变原点之后更新输出。例如,在第四章的SYSMETS2程序中,我们使用了iVscrollPos值(垂直滚动条的目前位置)来调整显示输出的y坐标:
case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) { y = cyChar * (i - iVscrollPos) ; // 显示文字 } EndPaint (hwnd, &ps) ; return 0 ;
我们可以使用SetWindowOrgEx获得同样的效果:
case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetWindowOrgEx (hdc, 0, cyChar * iVscrollPos) ; for (i = 0 ; i < NUMLINES ; i++) { y = cyChar * i ; // 显示文字 } EndPaint (hwnd, &ps) ; return 0 ;
现在,TextOut函数的y坐标的计算不需要iVscrollPos的值。这意味着您可以将文字输出函数放到一个例程中,不用将iVscrollPos值传给该例程,因为我们是通过改变窗口原点来调整文字显示的。
如果您有使用直角坐标系(即笛卡尔坐标系)的经验,那么将逻辑点(0,0)移到显示区域的中央(像我们上面所说的那样)的确值得考虑。但是,对于MM_TEXT映像方式来说,还存在着一个小小的问题:笛卡尔坐标系中,y值是随着上移而增加的,而MM_TEXT定义为下移时y值增加。从这一点来看,MM_TEXT有点古怪,而下面这五种映射方式都使用通常的增值方法。
「度量」映像方式
Windows包含五种以实际尺寸来表示逻辑坐标的映像方式。由于x轴和y轴的逻辑坐标映像为相同的实际单位,这些映像方式能使您画出不变形的圆和矩形。
这五种「度量」映像方式在表5-6中列出,按照从低精度到高精度的顺序排列。右边的两列分别给出了以英寸和毫米为单位时逻辑单位的大小,以便比较。
表5-6 |
映像方式 |
逻辑单位 |
英寸 |
毫米 |
MM_LOENGLISH |
0.01 in. |
0.01 |
0.254 |
MM_LOMETRIC |
0.1 mm. |
0.00394 |
0.1 |
MM_HIENGLISH |
0.001 in. |
0.001 |
0.0254 |
MM_TWIPS |
1/1400 in. |
0.000694 |
0.0176 |
MM_HIMETRIC |
0.01 mm. |
0.000394 |
0.01 |
内定窗口及视端口的原点和范围如下所示:
窗口原点:(0, 0) 可以改变
视埠原点:(0, 0) 可以改变
窗口范围:(1, 1) 不可改变
视埠范围:(1, 1) 不可改变
问号表示窗口和视端口的范围依赖于映像方式和设备的分辨率。前面已经提到过,这些范围本身并不重要,但是表示比例时就必须知道。下面是窗口坐标到视端口坐标的转换公式:
例如,对于MM_LOENGLISH,Windows计算的范围如下:
Windows使用这些来自GetDeviceCaps的有用信息设定范围。只是在Windows 98和Windows NT之间有一点差别。
首先,来看看Windows 98是如何做的:假设您使用「控制台」的「显示」程序选择了96 dpi的系统字体。GetDeviceCaps对于LOGPIXELSX和LOGPIXELSY索引都将传回值96。Windows为视埠范围使用这些值并以表5-7的方式设定视端口和窗口的范围。
表5-7 |
映像方式 |
视埠范围(x,y) |
窗口范围(x,y) |
MM_LOMETRIC |
(96, 96) |
(254, -254) |
MM_HIMETRIC |
(96, 96) |
(2540, -2540) |
MM_LOENGLISH |
(96, 96) |
(100, -100) |
MM_HIENGLISH |
(96, 96) |
(1000, -1000) |
MM_TWIPS |
(96, 96) |
(1440, -1440) |
这样,对MM_LOENGLISH来说,96除以100的比值是0.01英寸中的图素数。对MM_LOMETRIC来说,96除以254的比值是0.1毫米中的图素数。
Windows NT使用不同的方法设定视端口和窗口的范围(与早期16位版本的Windows一致的方法)。视端口范围依据屏幕的图素尺寸。可以使用HORZRES和VERTRES索引从GetDeviceCaps取得这种信息。窗口范围依据假定的显示大小,它是您使用HORZSIZE和VERTSIZE索引时由GetDeviceCaps传回的。我在前面提到过,这些值一般是320和240毫米。如果您将显示器的图素尺寸设定为1024×768,则表5-8就是Windows NT报告的视端口和窗口范围的值。
表5-8 |
映像方式 |
视埠范围(x,y) |
窗口范围(x,y) |
MM_LOMETRIC |
(1024, -768) |
(3,200, 2,400) |
MM_HIMETRIC |
(1024, -768) |
(32,000, 24,000) |
MM_LOENGLISH |
(1024, -768) |
(1,260, 945) |
MM_HIENGLISH |
(1024, -768) |
(12,598, 9,449) |
MM_TWIPS |
(1024, -768) |
(18,142, 13,606) |
这些窗口范围表示包含显示器全部宽度和高度的逻辑单位元数值。320毫米宽的屏幕也为1260 MM_LOENGLISH单位或12.6英寸(320除以25.4毫米/英寸)。
范围中,y前面的负号表示改变了轴的方向。对于这五种映像方式,y值随上升而增加,然而注意内定的窗口和视端口原点均为(0,0)。这个事实有一个有趣的结果。当一开始改变为五种映像方式之一时,坐标系如下:
要想在显示区域显示任何东西,必须使用负的y值。例如下面的程序代码:
SetMapMode (hdc, MM_LOENGLISH) ; TextOut (hdc, 100, -100, "Hello", 5) ;
将把文字显示在距离显示区域左边和上边各一英寸的地方。
为了使自己保持头脑清醒,您可能想避免这样做。一种解决办法是将逻辑的(0,0)点设为显示区域的左下角,您可以通过呼叫SetViewportOrgEx来完成(假设cyClient是以图素为单位的显示区域的高度):
SetViewportOrgEx (hdc, 0, cyClient, NULL) ;
此时的坐标系如下:
这是直角坐标系的右上象限。
另一种方法是将逻辑(0,0)点设为显示区域的中心:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;
此时的坐标系如下所示:
现在,我们有了一个真正的4象限笛卡尔坐标系,在x轴和y轴上有相等的按英寸、毫米或twip计算的逻辑单位。
您还可以使用SetWindowOrgEx函数来改变逻辑(0,0)点,但是这稍微困难一些,因为SetWindowOrgEx的参数必须使用逻辑单位,先要将(cxClient,cyClient)用DPtoLP函数转换为逻辑坐标。假设变量pt是型态为POINT的结构,下面的代码将逻辑(0,0)点改变到显示区域的中央:
pt.x = cxClient ; pt.y = cyClient ; DptoLP (hdc, &pt, 1) ; SetWindowOrgEx (hdc, -pt.x / 2, -pt.y / 2, NULL) ;
「自行决定」的映像方式
剩下的两种映像方式为MM_ISOTROPIC和MM_ANISOTROPIC。只有这两种映像方式可以让您改变视端口和窗口范围,也就是说可以改变Windows用来转换逻辑和设备坐标的换算因子。「isotropic」的意思是「同方向性」;「anisotropic」的意思是「异方向性」。与上面所讨论的度量映射方式相似,MM_ISOTROPIC使用相同的轴,x轴上的逻辑单位与y轴上的逻辑单位的实际尺寸相等。这对您建立纵横比与显示比无关的图像是有帮助的。
MM_ISOTROPIC与度量映像方式之间的区别是,使用MM_ISOTROPIC,您可以控制逻辑单位的实际尺寸。如果愿意,您可以根据显示区域的大小来调整逻辑单位的实际尺寸,从而使所画的图像总是包含在显示区域内,并相应地放大或缩小。例如, 第八章的两个时钟程序就是方向同性的例子。在您改变窗口大小时,时钟也相应地调整。
Windows程序完全可以通过调整窗口和视端口范围来处理图像大小的变化。因此,不管窗口尺寸怎样变,程序都可以在绘图函数中使用相同的逻辑单位。
有时候MM_TEXT和度量映像方式称为「完全局限性」映像方式,这就是说,您不能改变窗口和视端口的范围以及Windows将逻辑坐标换算为设备坐标的方法。MM_ISOTROPIC是一种「半局限性」的映像方式,Windows允许您改变窗口和视端口范围,但只是调整它们,以便x和y逻辑单位代表同样的实际尺寸。MM_ANISOTROPIC映像方式是「非局限性」的,您可以改变窗口和视端口范围,但是Windows不调整这些值。
MM_ISOTROPIC映像方式
如果想要在使用任意的轴时都保证两个轴上的逻辑单位相同,则MM_ISOTROPIC映像方式就是理想的映像方式。这时,具有相同逻辑宽度和高度的矩形显示为正方形,具有相同逻辑宽度和高度的椭圆显示为圆。
当您刚开始将映像方式设定为MM_ISOTROPIC时,Windows使用与MM_LOMETRIC同样的窗口和视端口范围(但是,不要对此有所依赖)。区别在于,您现在可以呼叫SetWindowExtEx和SetViewportExtEx来根据自己的偏好改变范围了,然后,Windows将调整范围的值,以便两条轴上的逻辑单位有相同的实际距离。
一般说来,您可以用所期望的逻辑窗口的逻辑尺寸作为SetWindowExtEx的参数,用显示区域的实际宽和高作为SetViewportExtEx的参数。Windows在调整这些范围时,必须让逻辑窗口适应实际窗口,这就有可能导致显示区域的一段落到了逻辑窗口的外面。必须在呼叫SetViewportExtEx之前呼叫SetWindowExtEx,以便最有效地使用显示区域中的空间。
例如,假设您想要一个「传统的」单象限虚拟坐标系,其中(0,0)在显示区域的左下角,宽度和高度的范围都是从0到32,767,并且希望x和y轴的单位具有同样的实际尺寸。以下就是所需的程序:
SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetViewportOrgEx (hdc, 0, cyClient, NULL) ;
如果其后用GetWindowExtEx和GetViewportExtEx函数获得了窗口和视端口的范围,可以发现,它们并不是先前指定的值。Windows将根据显示设备的纵横比来调整范围,以便两条轴上的逻辑单位表示相同的实际尺寸。
如果显示区域的宽度大于高度(以实际尺寸为准),Windows将调整x的范围,以便逻辑窗口比显示区域视端口窄。这样,逻辑窗口将放置在显示区域的左边:
Windows 98不允许在显示区域的右边超越x轴的范围之外显示任何东西,因为这需要一个大于16位所能表示的坐标。Windows NT使用全32位坐标,您可以在超出右边显示一些东西。
如果显示区域的高度大于宽度(以实际尺寸为准),那么Windows将调整y的范围。这样,逻辑窗口将放置在显示区域的下边:
Windows 98不允许在显示区域的顶部显示任何东西。
如果您希望逻辑窗口总是放在显示区域的左上部,那么将前面给出的程序代码改为:
SetMapMode (MM_ISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetWindowOrgEx (hdc, 0, 32767, NULL) ;
在呼叫SetWindowOrgEx中,我们要求将逻辑点(0, 32767)映像为设备点(0,0)。现在,如果显示区域的高大于宽,则坐标系将安排为:
对于时钟程序,您也许想要使用一个四象限的笛卡尔坐标系,四个方向的坐标尺度可以任意指定,(0,0) 必须居于显示区域的中央。如果您想要每条轴的范围从0到1000,则可以使用以下程序代码:
SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;
如果显示区域的宽度大于高度,则逻辑坐标系形如:
如果显示区域的高度大于宽度,那么逻辑坐标也会居中:
记住,窗口或者视端口范围并不意味着要进行剪裁。在呼叫GDI函数时,您仍然对以随便地使用小于-1000和大于1000的x和y值。根据显示区域的外形,这些点可能看得见,也可能看不见。
在MM_ISOTROPIC映像方式下,可以使逻辑单位大于图素。例如,假设您想要一种映像方式,使点(0,0)显示在屏幕的左上角,y的值向下增长(和MM_TEXT相似),但是逻辑坐标单位为1/16英寸。以下是一种方法:
SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 16, 16, NULL) ; SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX), GetDeviceCaps (hdc, LOGPIXELSY), NULL) ;
SetWindowExtEx函数的参数指出了每一英寸中逻辑单位数。SetViewportExtEx函数的参数指出了每一英寸中实际单位数(图素)。
然而,这种方法与Windows NT中的度量映像方式不一致。这些映射方式使用显示器的图素大小和公制大小。要与度量映像方式保持一致,可以这样做:
SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 160 * GetDeviceCaps (hdc, HORZSIZE) / 254, 160 * GetDeviceCaps (hdc, VERTSIZE) / 254, NULL) ; SetViewportExtEx (hdc, GetDeviceCaps (hdc, HORZRES), GetDeviceCaps (hdc, VERTRES), NULL) ;
在这个程序代码中,视埠范围设定为按图素计算的整个屏幕的大小,窗口范围则必须设定为以1/16英寸为单位的整个屏幕的大小。GetDeviceCaps以HORZRES和VERTRES为参数,传回以毫米为单位的设备尺寸。如果我们使用浮点数,将把毫米数除以25.4,转换为英寸,然后,再乘以16以转换为l/16英寸。但是,由于我们使用的是整数,所以先乘以160,再除以254。
当然,这种坐标系会使逻辑单位大于实际单位。在设备上输出的所有东西都将映像为按1/16英寸增量的坐标值。当然,这样就不能画两条间隔l/32英寸的水平直线,因为这样将需要小数逻辑坐标。
MM_ANISOTROPIC:根据需要放缩图像
在MM_ISOTROPIC映像方式下设定窗口和视端口范围时,Windows会调整范围,以便两条轴上的逻辑单位具有相同的实际尺度。在MM_ANISOTROPIC映射方式下,Windows不对您所设定的值进行调整,这就是说,MM_ANISOTROPIC不需要维持正确的纵横比。
使用MM_ANISOTROPIC的一种方法是对显示区域使用任意坐标,就像我们对MM_ISOTROPIC所做的一样。下面的程序代码将点(0,0)设定为显示区域的左下角,x轴和y轴都从0到32,767:
SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetViewportOrgEx (hdc, 0, cyClient, NULL) ;
在MM_ISOTROPIC方式下,相似的程序代码导致显示区域的一部分在轴的范围之外。但是对于MM_ANISOTROPIC,不论其尺度多大,显示区域的右上角总是(32767, 32767)。如果显示区域不是正方形的,则逻辑x和y的单位具有不同的实际尺度。
前一节在MM_ISOTROPIC映像方式下,我们讨论了在显示区域中画一个类似时钟的图像,x和y轴的范围都是从-1000到+1000。对于MM_ANISOTROPIC,也可以写出类似的程序:
SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;
与MM_ANISOTROPIC方式不同的是,这个时钟一般是椭圆形的,而不是圆形的。
另一种使用MM_ANISOTROPIC的方法是将x和y轴的单位固定,但其值不相等。例如,如果有一个只显示文字的程序,您可能想根据单个字符的高度和宽度设定一种粗刻度的坐标:
SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1, 1, NULL) ; SetViewportExtEx (hdc, cxChar, cyChar, NULL) ;
当然,这里假设cxChar和cyChar分别是那种字体的字符宽度和高度。现在,您可以按字符行和列指定坐标。下面的叙述在距离显示区域左边三个字符,上边二个字符处显示文字:
TextOut (hdc, 3, 2, TEXT ("Hello"), 5) ;
如果您使用固定大小的字体时会更加方便,就像下面的WHATSIZE程序所示的那样。
当您第一次设定MM_ANISOTROPIC映像方式时,它总是继承前面所设定的映像方式的范围,这会很方便。可以认为MM_ANISOTROPIC不「锁定」范围;也就是说,它允许您任意改变窗口范围。例如,假设您想用MM_LOENGLISH映像方式,因为希望逻辑单位为0.01英寸,但您不希望y轴的值向上增加,喜欢如MM_TEXT那样的方向,即y轴的值向下增加,可以使用如下的代码:
SIZE size ;
其它行程序
SetMapMode (hdc, MM_LOENGLISH) ; SetMapMode (hdc, MM_ANISOTROPIC) ; GetViewportExtEx (hdc, &size) ; SetViewportExtEx (hdc, size.cx, -size.cy, NULL) ;
我们首先将映像方式设定为MM_LOENGLISH,然后,通过将映像方式设定为MM_ANISOTROPIC让范围可以自由改变。GetViewportExtEx取得视埠范围并放到一个SIZE结构中,然后,我们使用范围来呼叫SetViewportExtEx,只是要将y范围取反。
WHATSIZE程序
Windows的小历史:第一篇如何写作Windows程序的介绍文章出现在《Microsoft Systems Journal》1986年12月号上。在那篇文章中,范例程序叫做WSZ(「what size:什么尺寸」),它以图素、英寸和毫米为单位显示了显示区域的大小。那个程序的更简易版本是WHATSIZE,如程序5-6所示。程序显示了以五种度量映像方式显示的窗口显示区域的大小。
程序5-6 WHATSIZE WHATSIZE.C /*------------------------------------------------------------ WHATSIZE.C -- What Size is the Window? (c) Charles Petzold, 1998 ----------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("WhatSize") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon= LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor= LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("What Size is the Window?"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void Show (HWND hwnd, HDC hdc, int xText, int yText, int iMapMode, TCHAR * szMapMode) { TCHAR szBuffer [60] ; RECT rect ; SaveDC (hdc) ; SetMapMode (hdc, iMapMode) ; GetClientRect (hwnd, &rect) ; DPtoLP (hdc, (PPOINT) &rect, 2) ; RestoreDC (hdc, -1) ; TextOut ( hdc, xText, yText, szBuffer, wsprintf (szBuffer, TEXT ("%-20s %7d %7d %7d %7d"), szMapMode, rect.left, rect.right, rect.top, rect.bottom)) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static TCHAR szHeading [] = TEXT ("Mapping Mode Left Right Top Bottom") ; static TCHAR szUndLine [] = TEXT ("------------ ---- ----- --- ------") ; static int cxChar, cyChar ; HDC hdc ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1, 1, NULL) ; SetViewportExtEx (hdc, cxChar, cyChar, NULL) ; TextOut (hdc, 1, 1, szHeading, lstrlen (szHeading)) ; TextOut (hdc, 1, 2, szUndLine, lstrlen (szUndLine)) ; Show (hwnd, hdc, 1, 3, MM_TEXT, TEXT ("TEXT (pixels)")) ; Show (hwnd, hdc, 1, 4, MM_LOMETRIC, TEXT ("LOMETRIC (.1 mm)")) ; Show (hwnd, hdc, 1, 5, MM_HIMETRIC, TEXT ("HIMETRIC (.01 mm)")) ; Show (hwnd, hdc, 1, 6, MM_LOENGLISH, TEXT ("LOENGLISH (.01 in)")) ; Show (hwnd, hdc, 1, 7, MM_HIENGLISH,TEXT ("HIENGLISH (.001 in)")) ; Show (hwnd, hdc, 1, 8, MM_TWIPS, EXT ("TWIPS (1/1440 in)")) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
为了便于用TextOut函数显示信息,WHATSIZE使用了一种固定间距的字体。下面一条简单的叙述就可以切换为固定间距的字体(在Windows 3.0中它是优先使用的):
SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
有两个同样的函数用于选取画笔和画刷。像前面提到的,WHATSIZE也使用MM_ANISTROPIC映像方式将逻辑单位设定为字符大小。
当WHATSIZE需要取得六种映像方式之一的显示区域的大小时,它保存目前的设备内容,设定一种新的映像方式,取得显示区域坐标,将它们转换为逻辑坐标,然后在显示信息之前,恢复原映像方式。底下这些程序代码在WHATSIZE的Show函数里:
SaveDC (hdc) ; SetMapMode (hdc, iMapMode) ; GetClientRect (hwnd, &rect) ; DptoLP (hdc, (PPOINT) &rect, 2) ; RestoreDC (hdc, -1) ;
图5-19显示了WHATSIZE的典型输出。
图5-19 典型的WHATSIZE显示 |
Windows包含了几种使用RECT(矩形)结构和「区域」的绘图函数。区域就是屏幕上的一块地方,它是矩形、多边形和椭圆的组合。
矩形函数
下面三个绘图函数需要一个指向矩形结构的指针:
FillRect (hdc, &rect, hBrush) ; FrameRect (hdc, &rect, hBrush) ; InvertRect (hdc, &rect) ;
在这些函数中,rect参数是一个RECT型态的结构,它包含有4个字段:left、top、right和bottom。这个结构中的坐标被当作逻辑坐标。
FillRect用指定画刷来填入矩形(直到但不包含right和bottom坐标),该函数不需要先将画刷选进设备内容。
FrameRect使用画刷画矩形框,但是不填入矩形。使用画刷画矩形看起来有点奇怪,因为对于我们所介绍过的函数(如Rectangle),其边线都是用目前画笔绘制的。FrameRect允许使用者画一个不一定为纯色的矩形框。该边界框为一个逻辑单位元宽。如果逻辑单位大于设备单位,则边界框将会为2个图素宽或者更宽。
InvertRect将矩形中所有图素翻转,1转换成0,0转换为1,该函数将白色区域转变成黑色,黑色区域转变为白色,绿色区域转变成洋红色。
Windows还提供了9个函数,使您可以更容易、更清楚地操作RECT结构。例如,要将RECT结构的四个字段设定为特定值,通常使用如下的程序段:
rect.left = xLeft ; rect.top = xTop ; rect.right = xRight ; rect.bottom = xBottom ;
但是,通过呼叫SetRect函数,只需要一道叙述就可以得到同样的结果:
SetRect (&rect, xLeft, yTop, xRight, yBottom) ;
在您想要做以下事情之一时,可以很方便地选用其它8个函数:
- 将矩形沿x轴和y轴移动几个单元:
OffsetRect (&rect, x, y) ;
- 增减矩形的尺寸:
InflateRect (&rect, x, y) ;
- 矩形各字段设定为0:
SetRectEmpty (&rect) ;
- 将矩形复制给另一个矩形:
CopyRect (&DestRect, &SrcRect) ;
- 取得两个矩形的交集:
IntersectRect (&DestRect, &SrcRect1, &SrcRect2) ;
- 取得两个矩形的联集:
UnionRect (&DestRect, &SrcRect1, &SrcRect2) ;
- 确定矩形是否为空:
bEmpty = IsRectEmpty (&rect) ;
- 确定点是否在矩形内:
bInRect = PtInRect (&rect, point) ;
大多数情况下,与这些函数相同作用的程序代码很简单。例如,您可以用下列叙述来替代CopyRect函数呼叫:
DestRect = SrcRect ;
随机矩形
在图形系统中,有这么一个「永远」有人执行的有趣程序,它简单地使用随机的大小和色彩绘制一系列矩形。您可以在Windows中建立一个这样的程序,但是它并不像乍看起来那样容易编写。我希望您能认识到,您不能简单地在WM_PAINT消息中使用一个while(TRUE)循环。当然,它能够执行,但是程序将停止对其他消息的处理,同时,这个程序不能中止或者最小化。
一种可以接受的方法是设定一个Windows定时器,给窗口程序发送WM_TIMER消息(我将在 第八章中讨论定时器)。对于每条WM_TIMER消息,您使用GetDC取得一个设备内容,画一个随机的矩形,然后用ReleaseDC释放设备内容。但是这样又降低了程序的趣昧性,因为程序不能尽可能快地画随机矩形,它必须等待WM_TIMER消息,而这又依赖于系统时钟的分辨率。
在Windows中一定有很多「闲置时间」,在这个时间内,所有消息队列为空,Windows只停在一个小循环中等待键盘或者鼠标输入。我们能否在闲置时间内获得控制,绘制矩形,并且只在有消息加入程序的消息队列之后才释放控制呢?这就是PeekMessage函数的目的之一。下面是PeekMessage呼叫的一个例子:
PeekMessage (&msg, NULL, 0, 0, PM_REMOVE) ;
前面的四个参数(一个指向MSG结构的指针、一个窗口句柄、两个值指示消息范围)与GetMessage的参数相同。将第二、三、四个参数设定为NULL或0时,表明我们想让PeekMessage传回程序中所有窗口的所有消息。如果要将消息从消息队列中删除,则将PeekMessage的最后一个参数设定为PM_REMOVE。如果您不希望删除消息,那么您可以将这个参数设定为PM_NOREMOVE。这就是为什么Peek_Message是「偷看」而不是「取得」的原因,它使得程序可以检查程序的队列中的下一个消息,而不实际删除它。
GetMessage不将控制传回给程序,直到从程序的消息队列中取得消息,但是PeekMessage总是立刻传回,而不论一个消息是否出现。当消息队列中有一个消息时,PeekMessage的传回值为TRUE(非0),并且将按通常方式处理消息。当队列中没有消息时,PeekMessage传回FALSE(0)。
这使得我们可以改写普通的消息循环。我们可以将如下所示的循环:
while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ;
替换为下面的循环:
while (TRUE) { if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; } else { // 完成某些工作的其它行程序 } } return msg.wParam ;
注意,WM_QUIT消息被另外挑出来检查。在普通的消息循环中您不必这么作,因为如果GetMessage接收到一个WM_QUIT消息,它将传回0,但是PeekMessage用它的传回值来指示是否得到一个消息,所以需要对WM_QUIT进行检查。
如果PeekMessage的传回值为TRUE,则消息按通常方式进行处理。如果传回值为FALSE,则在将控制传回给Windows之前,还可以作一点工作(如显示另一个随机矩形)。
(尽管Windows文件上说,您不能用PeekMessage从消息队列中删除WM_PAINT消息,但是这并不是什么大不了的问题。毕竟,GetMessage并不从消息队列中删除WM_PAINT消息。从队列中删除WM_PAINT消息的唯一方法是令窗口显示区域的失效区域变得有效,这可以用ValidateRect和ValidateRgn或者BeginPaint和EndPaint对来完成。如果您在使用PeekMessage从队列中取出WM_PAINT消息后,同平常一样处理它,那么就不会有问题了。所不能作的是使用如下所示的程序代码来清除消息队列中的所有消息:
while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) ;
这行叙述从消息队列中删除WM_PAINT之外的所有消息。如果队列中有一个WM_PAINT消息,程序就会永远地陷在while循环中。)
PeekMessage在Windows的早期版本中比在Windows 98中要重要得多。这是因为Windows的16位版本使用的是非优先权式的多任务(我将在第二十章中讨论这一点)。Windows的Terminal程序在从通讯端口接收输入后,使用一个PeekMessage循环。打印管理器程序使用这个技术来进行打印,其它的Windows打印应用程序通常都会使用一个PeekMessage循环。在Windows 98优先权式的多任务环境下,程序可以建立多个线程,我们将第二十章看到这一点。
不管怎样,有了PeekMessage函数,我们就可以编写一个不停地显示随机矩形的程序。这个RANDRECT如程序5-7中所示。
RANDRECT.C /*---------------------------------------------------------------------- RANDRECT.C -- Displays Random Rectangles (c) Charles Petzold, 1998 -----------------------------------------------------------------------*/ #include <windows.h> #include <stdlib.h>// for the rand function LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; void DrawRectangle (HWND) ; int cxClient, cyClient ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("RandRect") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Random Rectangles"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (TRUE) { if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; } else DrawRectangle (hwnd) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { switch (iMsg) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; } void DrawRectangle (HWND hwnd) { HBRUSHhBrush ; HDC hdc ; RECT rect ; if (cxClient == 0 || cyClient == 0) return ; SetRect (&rect, rand () % cxClient, rand () % cyClient, rand () % cxClient, rand () % cyClient) ; hBrush = CreateSolidBrush ( RGB (rand () % 256, rand () % 256, rand () % 256)) ; hdc = GetDC (hwnd) ; FillRect (hdc, &rect, hBrush) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; }
这个程序在现在的计算机上执行得非常快,看起来都不像是一系列随机矩形了。程序使用我在上面讨论过的SetRect和FillRect函数,根据由C的rand函数得到的随机数决定矩形坐标和实心画刷的色彩。我将在第二十章中提供这个程序的多线程版本。
建立和绘制剪裁区域
剪裁区域是对显示器上一个范围的描述,这个范围是矩形、多边形和椭圆的组合。剪裁区域可以用于绘制和剪裁,通过将剪裁区域选进设备内容,就可以用剪裁区域来进行剪裁(就是说,将可以绘图的范围限制为显示区域的一部分)。与画笔、画刷和位图一样,剪裁区域是GDI对象,您应该呼叫DeleteObject来删除您所建立的剪裁区域。
当您建立一个剪裁区域时,Windows传回一个该剪裁区域的句柄,型态为HRGN。最简单的剪裁区域是矩形,有两种建立矩形的方法:
hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom) ;
或者
hRgn = CreateRectRgnIndirect (&rect) ;
您也可以建立椭圆剪裁区域:
hRgn = CreateEllipticRgn (xLeft, yTop, xRight, yBottom) ;
或者
hRgn = CreateEllipticRgnIndirect (&rect) ;
CreateRoundRectRgn建立圆角的矩形剪裁区域。
建立多边形剪裁区域的函数类似于Polygon函数:
hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode) ;
point参数是一个POINT型态的结构数组,iCount是点的数目,iPolyFillMode是ALTERNATE或者WINDING。您还可以用CreatePolyPolygonRgn来建立多个多边形剪裁区域。
那么,您会问,剪裁区域究竟有什么特别之处?下面这个函数才真正显示出了剪裁区域的作用:
iRgnType = CombineRgn (hDestRgn, hSrcRgn1, hSrcRgn2, iCombine) ;
这一函数将两个剪裁区域(hSrcRgn1和hSrcRgn2)组合起来并用句柄hDestRgn指向组合成的剪裁区域。这三个剪裁区域句柄都必须是有效的,但是hDestRgn原来所指向的剪裁区域被破坏掉了(当您使用这个函数时,您可能要让hDestRgn在初始时指向一个小的矩形剪裁区域)。
iCombine参数说明hSrcRgn1和hSrcRgn2如何组合,见表5-9。
表5-9 |
iCombine值 |
新剪裁区域 |
RGN_AND |
两个剪裁区域的公共部分 |
RGN_OR |
两个剪裁区域的全部 |
RGN_XOR |
两个剪裁区域的全部除去公共部分 |
RGN_DIFF |
hSrcRgn1不在hSrcRgn2中的部分 |
RGN_COPY |
hSrcRgn1的全部(忽略hSrcRgn2) |
从CombineRgn传回的iRgnType值是下列之一:NULLREGION,表示得到一个空剪裁区域;SIMPLEREGION,表示得到一个简单的矩形、椭圆或者多边形;COMPLEXREGION,表示多个矩形、椭圆或多边形的组合;ERROR,表示出错了。
剪裁区域的句柄可以用于四个绘图函数:
FillRgn (hdc, hRgn, hBrush) ; FrameRgn (hdc, hRgn, hBrush, xFrame, yFrame) ; InvertRgn (hdc, hRgn) ; PaintRgn (hdc, hRgn) ;
FillRgn、FrameRgn和InvertRgn类似于FillRect、FrameRect和InvertRect。FrameRgn的xFrame和yFrame参数是画在区域周围的边框的宽度和高度。PaintRgn函数用设备内容中目前画刷填入所指定的区域。所有这些函数都假定区域是用逻辑坐标定义的。
在您用完一个区域后,可以像删除其它GDI对象那样删除它:
DeleteObject (hRgn) ;
矩形与区域的剪裁
区域也在剪裁中扮演了一个角色。InvalidateRect函数使显示的一个矩形区域失效,并产生一个WM_PAINT消息。例如,您可以使用InvalidateRect函数来清除显示区域并产生一个WM_PAINT消息:
InvalidateRect (hwnd, NULL, TRUE) ;
您可以通过呼叫GetUpdateRect来取得失效矩形的坐标,并且可以使用ValidateRect函数使显示区域的矩形有效。当您接收到一个WM_PAINT消息时,无效矩形的坐标可以从PAINTSTRUCT结构中得到,该结构是用BeginPaint函数填入的。这个无效矩形还定义了一个「剪裁区域」,您不能在剪裁区域外绘图。
Windows有两个作用于剪裁区域而不是矩形的函数,它们类似于InvalidateRect和ValidateRect:
InvalidateRgn (hwnd, hRgn, bErase) ;
和
ValidateRgn (hwnd, hRgn) ;
当您接收到一个由无效区域引起的WM_PAINT消息时,剪裁区域不一定是矩形。
您可以使用以下两个函数之一:
SelectObject (hdc, hRgn) ;
或
SelectClipRgn (hdc, hRgn) ;
通过将一个剪裁区域选进设备内容来建立自己的剪裁区域,这个剪裁区域使用设备坐标。
GDI为剪裁区域建立一份副本,所以在将它选进设备内容之后,使用者可以删除它。Windows还提供了几个对剪裁区域进行操作的函数,如ExcludeClipRect用于将一个矩形从剪裁区域里排除掉,IntersectClipRect用于建立一个新的剪裁区域,它是前一个剪裁区域与一个矩形的交,OffsetClipRgn用于将剪裁区域移动到显示区域的另一部分。
CLOVER程序
CLOVER程序用四个椭圆组成一个剪裁区域,将这个剪裁区域选进设备内容中,然后画出从窗口显示区域的中心出发的一系列直线,这些直线只出现在剪裁区域所限定的范围,结果显示如图5-20所示。
图5-20 CLOVER利用复杂的剪裁区域画出的图像 |
要用常规的方法画出这个图形,就必须根据椭圆的边线公式计算出每条直线的端点。利用复杂的剪裁区域,可以直接画出这些线条,而让Windows确定其端点。CLOVER如程序5-8所示。
程序5-8 CLOVER CLOVER.C /*-------------------------------------------------------------------------- CLOVER.C -- Clover Drawing Program Using Regions (c) Charles Petzold, 1998 ----------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #define TWO_PI (2.0 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Clover") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Draw a Clover"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { static HRGN hRgnClip ; static int cxClient, cyClient ; double fAngle, fRadius ; HCURSORhCursor ; HDC hdc ; HRGN hRgnTemp[6] ; int i ; PAINTSTRUCT ps ; switch (iMsg) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; hCursor= SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; if (hRgnClip) DeleteObject (hRgnClip) ; hRgnTemp[0] = CreateEllipticRgn (0, cyClient / 3, cxClient / 2, 2 * cyClient / 3) ; hRgnTemp[1] = CreateEllipticRgn (cxClient / 2, cyClient / 3, cxClient, 2 * cyClient / 3) ; hRgnTemp[2] = CreateEllipticRgn (cxClient / 3, 0, 2 * cxClient / 3, cyClient / 2) ; hRgnTemp[3] = CreateEllipticRgn (cxClient / 3, cyClient / 2, 2 * cxClient / 3, cyClient) ; hRgnTemp[4] = CreateRectRgn (0, 0, 1, 1) ; hRgnTemp[5] = CreateRectRgn (0, 0, 1, 1) ; hRgnClip = CreateRectRgn (0, 0, 1, 1) ; CombineRgn (hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR) ; CombineRgn (hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR) ; CombineRgn (hRgnClip, hRgnTemp[4], hRgnTemp[5], RGN_XOR) ; for (i = 0 ; i < 6 ; i++) DeleteObject (hRgnTemp[i]) ; SetCursor (hCursor) ; ShowCursor (FALSE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SelectClipRgn (hdc, hRgnClip) ; fRadius = _hypot (cxClient / 2.0, cyClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWO_PI ; fAngle += TWO_PI / 360) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), (int) (-fRadius * sin (fAngle) + 0.5)) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteObject (hRgnClip) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; }
由于剪裁区域总是使用设备坐标,CLOVER程序必须在每次接收到WM_SIZE消息时重新建立剪裁区域。几年前,这可能需要几秒钟。现在的快速机器在一瞬间就可以画出来。
CLOVER从建立四个椭圆剪裁区域开始,这四个椭圆存放在hRgnTemp数组的头四个元素中,然后建立三个「空」剪裁区域:
hRgnTemp [4]= CreateRectRgn (0, 0, 1, 1) ; hRgnTemp [5]= CreateRectRgn (0, 0, 1, 1) ; hRgnClip = CreateRectRgn (0, 0, 1, 1) ;
显示区域左右的两个椭圆区域组合起来:
CombineRgn (hRgnTemp [4], hRgnTemp [0], hRgnTemp [1], RGN_OR) ;
同样,显示区域上下两个椭圆区域组合起来:
CombineRgn (hRgnTemp [5], hRgnTemp [2], hRgnTemp [3], RGN_OR) ;
最后,两个组合后的区域再组合到hRgnClip中:
CombineRgn (hRgnClip, hRgnTemp [4], hRgnTemp [5], RGN_XOR) ;
RGN_XOR标识符用于从结果区域中排除重迭部分。最后,删除6个临时区域:
for (i = 0 ; i < 6 ; i++) DeleteObject (hRgnTemp [i]) ;
与画出的图形比起来,WM_PAINT的处理很简单。视端口原点设定为显示区域的中心(使画直线更容易一些),在WM_SIZE消息处理期间建立的区域选择为设备内容的剪裁区域:
SetViewportOrg (hdc, xClient / 2, yClient / 2) ; SelectClipRgn (hdc, hRgnClip) ;
现在,剩下的就是画直线了,共360条,每隔一度画一条。每条线的长度为变量fRadius,这是从中心到显示区域的角落的距离:
fRadius = hypot (xClient / 2.0, yClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWO_PI ; fAngle += TWO_PI / 360) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), (int) (-fRadius * sin (fAngle) + 0.5)) ; }
在处理WM_DESTROY消息时,删除该剪裁区域:
DeleteObject (hRgnClip) ;
这不是本书关于图形程序设计的最后内容。第十三章讨论打印,第十四章和十五章讨论位图,第十七章讨论文字和字体,第十八章讨论MetaFile。