特殊说明:版权归个人所有,请勿转载,谢谢合作。
Windows图形设备接口GDI(Graphics Device Interface),它的主要任务是负责系统与绘图程序之间的信息交换,处理所有Windows程序的图形输出。本章节讨论设备环境和基本的GDI函数。了解这些知识,对于Windows编程来说是非常重要的。
5.1 图形设备接口
GDI是Windows系统的重要组成部分,负责系统与用户之间以更直观的方式进行交互――图形方式交互。控制在输出设备上显示图形或文字。GDI的出现使程序员无需要关心硬件设备及设备驱动,就可以将应用程序的输出转化为硬件设备上的输出,实现了程序开发者与硬件设备的隔离,大大方便了开发工作。GDI具有如下特点:
(1)不需要程序直接访问物理显示硬件,通过称为“设备环境”的抽象接口,间接访问显示硬件;
(2)程序需要与显示硬件(显示器、打印机等)进行通讯时,必须首先获得与特定窗口相关联的设备环境;
(3)用户无需关心具体的物理设备类型;
(4)Windows参考设备环境的数据结构完成数据的输出。
5.1.1 设备描述表
设备描述表是它与显示设备具有一定的对应关系,在Windows GDI界面下,它总是与某个窗口或这窗口上的某个显示区域相关。通常意义上窗口的设备描述表,一般指的是窗口的客户区,不包括标题栏、菜单栏所占用的区域,而对于整个窗口来说,其设备描述表严格意义上来讲应该称为窗口设备描述表,它包含窗口的全部显示区域。二者的操作方法完全一致,所不同的仅仅是可操作的范围不同而已。目前设备描述表有四种类型,它们分别是:
显示类型,主要支持画图操作以及视频显示等;
打印类型,支持打印机、绘图仪等输入出设备的绘图操作;
存储类型,主要支持绘制位图操作;
消息类型,主要支持设备数据的恢复。
设备描述表所描述图形对象及其属性如表5.1所示。
- 表5.1 设备描述对象
图形对象 | 描述 |
画刷 | 颜色、样式等 |
画笔 | 颜色、样式等 |
位图 | 位图的颜色、像素、缩放模式等 |
字体 | 字体内容、大小、磅数、字符集等 |
区域 | 位置、尺寸等 |
调色板 | 颜色等 |
设备环境代表屏幕上的一块区域,要想某个区域绘制图形或输出文字,就必须先获得此区域的设备环境句柄(HDC)。HDC是Windows提供的描述设备环境句柄的数据类型,它代表了程序当前的显示设备。在绘图时,必须要指定一个设备环境(DC),用来将某个窗口或设备与设备环境类的句柄指针关联起来,所有的绘图操作都与该句柄有关。HDC的获得要在WM_PATIN消息中,通过BeginPaint函数来获得,通过EndPaint函数来释放。因为所有的绘图、文字输出等,都需要这个HDC,所以今后这类的代码,通常会写在BeginPaint函数与EndPaint函数之间。
5.1.2 系统刷新请求
在Windows系统中,WM_PAINT消息非常重要,当窗口的部分区域或全部,变为无效,需要更新窗口时,系统将执行此消息。窗口之所以无效,是因为在最初创建窗口时,整个窗口区域都是处于无效状态,当在创建窗口时,使用的UpdateWindow函数时(创建窗口的第四步),它会执行第一个WM_PAINT消息,执行结果后,它需要将显示的内容在窗口区域显示,此时不允许再进行更改,所以窗口区域无效。如果想再次更改窗口区域显示的内容(即重新响应WM_PAINT消息),只有如下几种请求能办到:
(1)窗口发生变化,窗口的尺寸发生变化、客户区域移动/显示或程序通过滚动条滚动窗口等;
(2)窗口覆盖,窗口被其他窗口覆盖、窗口切换焦点或有菜单操作等;
(3)使用系统API,使用系统屏幕刷新函数,如,InvalidateRect和InvalidateRgn等。
在Windows窗口刷新时,内存中存放着一个显示输出的副本,当需要重新绘制窗口时,将内存中的副本,复制到相应的窗口中去。在应用程序中,通常将图形绘制(包括文本输出)处理,放在WM_PAINT消息响应模块中去(即在过程处理函数中的,消息响应的位置),当程序接收到刷新请求后,即可重新绘制图形。
5.1.3 获取设备环境
在WM_PAINT消息处理过程中,通常是从BeginPaint函数调用开始,而以一个EndPaint函数调用结束,【例5-1】是文本绘制的示例,说明了这一点。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { char szBuff[64] = { "文本绘制" }; // 显示的文本内容 PAINTSTRUCT ps; // 记录一些绘制信息 HDC hdc; // 设备环境句柄 // 消息处理 switch (message) { // 绘制消息处理 case WM_PAINT: hdc = BeginPaint(hWnd, &ps); RECT rt; GetClientRect(hWnd, &rt); DrawText(hdc, szBuff, strlen(szBuff), &rt, DT_CENTER); EndPaint(hWnd, &ps); break; // 窗口销毁消息,关闭窗口时响应。 case WM_DESTROY: PostQuitMessage(0); break; default: // 调用系统默认消息处理,即交给系统处理。 return DefWindowProc(hWnd, message, wParam, lParam); }//end switch return 0; }
程序运行结果如图5.1所示。
图5.1 文本绘制
BeginPaint与EndPaint函数,第一个参数都是程序的窗口句柄,第二个参数是指向PAINTSTRUCT的结构指针。PAINTSTRUCT结构中包含一些窗口消息处理程序,可以用来更新显示区域的内容,结构体原型如下:
typedef struct tagPAINTSTRUCT { // ps HDC hdc; BOOL fErase; RECT rcPaint; BOOL fRestore; BOOL fIncUpdate; BYTE rgbReserved[32]; } PAINTSTRUCT;
PAINTSTRUCT 结构体包含了用于绘制窗口客户区域的信息。例如,要更新的客户区的矩形区域的大小等等,其各参数说明如下:
参数hdc,设备环境句柄;
参数fErase,一般取值为真,表示擦除无效矩形区域的背景,否则不擦除;
参数rcPaint,无效矩形区域标识。通过制定左上角和右下角的坐标确定一个要绘制的矩形范围,该矩形单位相对于客户区左上角;
参数fRestore,系统预留,一般用不到;
参数fIncUpdate,系统预留,一般用不到;
参数rgbReserved,系统预留,一般用不到。
GetClientRect 函数的功能是获取窗口客户区的坐标。客户区坐标指定客户区的左上角和右下角。由于客户区坐标是相对窗口客户区的左上角而言的,因此左上角坐标为(0,0)。第一个参数是程序窗口的句柄。第二个参数是一个指针,指向一个RECT类型。
RECT是个特别的数据结构,它的作用就是定义一个矩形区域对象,用来存储一个矩形框的左上角坐标、宽度和高度。其函数原型如下:
typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT;
参数left,指定矩形框左上角的x坐标。
参数top,指定矩形框左上角的y坐标。
参数right,指定矩形框右下角的x坐标。
参数bottom,指定矩形框右下角的y坐标。
如果根据left、top、right及bottom四个值根据坐标位置难记忆的话,可以根据如图5.2所示进行记忆,这样更直接一些。
图5.2 RECT参数记忆
DrawText函数,在指定的矩形里写入格式化的正文,根据指定的方法对正文格式化(扩展的制表符,字符对齐、换行等)。其函数原型如下:
int DrawText( HDC hDC, // handle to device context LPCTSTR lpString, // pointer to string to draw int nCount, // string length, in characters LPRECT lpRect, // pointer to struct with formatting dimensions UINT uFormat // text-drawing flags );
参数hDC,设备环境句柄。
参数lpString,指向将被写入的字符串的指针,如果参数nCount是-1,则字符串必须是以“\0”结束的。 如果uFormat包含DT_MODIFYSTRING,则函数可为此字符串增加4个字符,存放字符串的缓冲区必须足够大,能容纳附加的字符。
参数nCount,指向字符串中的字符数。如果nCount为-1,则lpString指向的字符串被认为是以“\0”结束的,DrawText会自动计算字符数。
参数lpRect,指向结构RECT的指针,其中包含文本将被置于其中的矩形的信息,所有绘制操作,包括图形,都是以矩形区域作为绘画的基准。
参数uFormat,指定格式化文本的方法。它可以下列值的任意组合,各值描述如表5.2所示。
- 表5.2 设备描述对象
值 | 描述 |
DT_BOTTOM | 将正文调整到矩形底部。此值必须和DT_SINGLELINE组合 |
DT_CALCRECT | 决定矩形的宽和高 |
DT_CENTER | 使正文在矩形中水平居中 |
DT_EDITCONTROL | 复制多行编辑控制的正文显示特性 |
DT_END_ELLIPSIS或DT_PATH_ELLIPSIS | 对于显示的文本,如果结束的字符串的范围不在矩形内,它会被截断并以省略号标识。 如果一个字母不是在字符串的末尾处超出了矩形范围,它不会被截断并以省略号标识。 字符串不会被修改,除非指定了DT_MODIFYSTRING标志 |
DT_EXPANDTABS | 扩展制表符,每个制表符的缺省字符数是8 |
DT_EXTERNALLEADING | 在行的高度里包含字体的外部标头,通常,外部标头不被包含在正文行的高度里 |
DT_INTERNAL | 用系统字体来计算正文度量 |
DT_LEFT | 正文左对齐 |
DT_MODIFYSTRING | 修改给定的字符串来匹配显示的正文。
此标志必须和DT_END_ELLIPSIS 或 DT_PATH_ELLIPSIS同时使用 |
DT_NOCLIP | 无裁剪绘制。当DT_NOCLIP使用时DrawText的使用会有所加快 |
DT_RIGHT | 正文右对齐 |
DT_RTLREADING | 当选择进设备环境的字体是希伯来文或阿拉伯文字体时,为双向正文安排从右到左的阅读顺序都是从左到右的 |
DT_SINGLELINE | 单行显示 |
DT_TABSTOP | 设置制表,Tab停止 |
DT_TOP | 正文顶端对齐 |
DT_VCENTER | 使正文在矩形中垂直居中 |
DT_WORDBREAK | 当一行中的字符将会延伸到由lpRect指定的矩形的边框时,此行自动地在字之间断开 |
DT_WORD_ELLIPSIS | 截短不符合矩形的正文,并增加省略号 |
最后,再重新整理一下程序的执行过程。首先,捕获WM_PAINT消息,在WM_PAINT消息中,使用BeginPaint函数来获得HDC(设备环境句柄);其次,通过GetClientRect函数获得当前客户区域的尺寸;再次,使用DrawText函数将文字绘制到屏幕上;最后,使用EndPaint函数来结束绘制过程。
5.1.4 其他绘制方法
在前几节已经提及到,通常图形绘制、文字输出,都属于WM_PAINT消息处理范围之内的。而在实际应用中,如果不是在WM_PAINT消息,可以使用GetDC函数来获取窗口客户区域的设备环境句柄,从而实现图形、文字的绘制操作。操作方法如下:
hdc = GetDC(hWnd); // 图形或文字绘制 // ... ReleaseDC(hWnd, hdc);
首先要通过GetDC函数来获得客户区的设备环境句柄,句柄得到后就可以随意绘制,绘制完成后,需要使用ReleaseDC函数,将GetDC获得到的句柄释放。虽然图形、文字的绘制,可以不必居于WM_PAINT消息,更随意一些,但是它有一个问题,它不会永久地保留在屏幕上。例如,绘制代码写在鼠标左键接下消息处(WM_LBUTTONDOWN),代码如下所示:
// 鼠标消息的处理 case WM_LBUTTONDOWN: hdc = GetDC(hWnd); // 图形或文字绘制 // ... ReleaseDC(hWnd, hdc);
程序执行后,客户区不会有任何内容,当点击鼠标左键时,绘制的内容将打印到屏幕上,但此时,如果窗口被其他窗口覆盖、窗口的尺寸发生变化或任何一项(如5.1.2所描述的刷新请求),刚刚绘制的内容将会消失。只有再次点击鼠标左键时,绘制的内容才能再次打印到屏幕上。
5.1.5 窗口刷新函数的应用
窗口刷新中,屏幕刷新函数应用更为广泛,因为它使用较为灵活。屏幕刷新函数有两个:InvalidateRect函数与InvalidateRgn函数。
InvalidateRect函数向指定的窗体添加一个矩形,然后窗口客户区域的这一部分将被重新绘制,其函数原型如下:
BOOL InvalidateRect( HWND hWnd, // handle of window with changed update region CONST RECT *lpRect, // address of rectangle coordinates BOOL bErase // erase-background flag );
参数hWnd,要更新的客户区所在的窗体的句柄。如果为NULL,则系统将在函数返回前重新绘制所有的窗口,然后发送 WM_ERASEBKGND 和 WM_NCPAINT 给窗口过程处理函数。
参数lpRect,无效区域的矩形代表,它是一个结构体指针,存放着矩形的大小。如果为NULL,全部的窗口客户区域将被增加到更新区域中。
参数bErase,指出无效矩形被标记为有效后,是否重绘该区域,重绘时用预先定义好的画刷。当指定TRUE时需要重绘。
【例5-2】将5.1.3节示例的功能进行了更改,当鼠标左键按下时,屏幕显示文字,鼠标右键按下时,清空屏幕上的文字,代码如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { char szBuff[64] = { "文本绘制" }; // 显示的文本内容 PAINTSTRUCT ps; // 记录一些绘制信息 HDC hdc; // 设备环境句柄 static BOOL bIsDraw = FALSE; // 静态变量,存放是否绘制的布尔值变量 // 获得客户区域范围 RECT rt; GetClientRect(hWnd, &rt); // 消息处理 switch (message) { // 鼠标消息的处理 case WM_LBUTTONDOWN: bIsDraw = TRUE; // 刷新整个客户区 InvalidateRect(hWnd, &rt, TRUE); break; case WM_RBUTTONDOWN: bIsDraw = FALSE; // 刷新整个客户区 InvalidateRect(hWnd, &rt, TRUE); break; // 绘制消息处理 case WM_PAINT: hdc = BeginPaint(hWnd, &ps); if(bIsDraw) { // 绘制文字到客户区,文字顶端居中 DrawText(hdc, szBuff, strlen(szBuff), &rt, DT_CENTER); }//end if EndPaint(hWnd, &ps); break; // 窗口销毁消息,关闭窗口时响应。 case WM_DESTROY: PostQuitMessage(0); break; default: // 调用系统默认消息处理,即交给系统处理。 return DefWindowProc(hWnd, message, wParam, lParam); }//end switch return 0; }
首先,定义布尔型静态变量bIsDraw,用来识别是否绘制,默认不做绘制操作;
其次,通过GetClientRect函数获得客户区的尺寸;
再次,就是消息的捕获过程。在WM_LBUTTONDOWN消息处,bIsDraw赋值为TRUE,目的是鼠标左键按下则执行绘制操作。虽然变量已经设置,但如果不执行InvalidateRect函数,它不会做刷新操作(即不会执行WM_PAINT消息)。在WM_RBUTTONDOWN消息处,bIsDraw赋值为FALSE,目的是鼠标右键按下后,不再执行文字的绘制操作。同样需要执行InvalidateRect函数,刷新一下窗口;
最后,在WM_PAINT消息处,判断bIsDraw的值,如果为真执行绘制文字操作,否则不做任何操作。