📄 5. 图形基础.txt
字号:
图形基础
智能中国——游戏组 整理编译
--------------------------------------------------------------------------------
图形设备接口(GDI:Graphics Device Interface)是Windows的子系统,它负责在视讯显示器和打印机上显示图形。正如您所认为的那样,GDI是Windows非常重要的部分。不只您为Windows编写的应用系统在显示视觉信息时使用GDI,就连Windows本身也使用GDI来显示使用者接口对象,诸如菜单、滚动条、图标和鼠标光标。
不幸的是,如果要对GDI进行全面的讲述,将需要一整本书-当然不是这本书。在本章中,我只是想向您提供画线和填入区域的基本知识,这对于理解下面几章的GDI已经足够了。在后面几章中会讲述GDI支持的位图、metafile以及格式化文字。
GDI 的结构
从程序写作者的观点来看,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将在第十三章给出,用于取得打印机信息)。
程序5-1 DEVCAPS1
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 ;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -