29 GDI基础
图形设备接口GDI是Windows的子系统,它负责在视频显示器和打印机上显示图形。
Windows NT中的图形主要由GDI32.DLL动态链接库输出的函数来处理。
GDI的主要目的之一是支持与设备无关的图形。
图形输出设备分为两大类:光栅设备和矢量设备。
大多数PC机显示器、打印机都是光栅设备。
绘图仪是矢量设备。
组成GDI的函数可以分为这样几类:
① 获取(或重建)和释放(或清除)设备描述表的函数;
② 获取有关设备描述表信息的函数;
③ 绘图函数;
④ 设置和获取设备描述表参数的函数;
⑤ 使用GDI对象的函数。
30 GDI图元
在屏幕或打印机上显示的图形类型本身可以被分为几类,通常被称为“图元”。
① 直线和曲线
② 填充区域
③ 位图:位图是位的矩形数组,位对应于显示设备上的像素,它们是光栅图形的基本工具。GDI支持两种类型的位图——老的“设备有关”位图,新的“设备无关”位图。
④ 文本
31 GDI其他方面
① 映射模式和变化;
② 元文件:元文件是以二进制形式存储的GDI命令的集合。元文件主要用于通过剪贴板传输矢量图形表示。
③ 区域:区域是形状任意的复杂区;
④ 路径:路径是GDI内部存储的直线和曲线的集合;
⑤ 剪裁:绘图可以限制在客户区的某一部分中,这就是剪裁。剪裁通常是通过区域或者路径定义的。
⑥ 调色板:定制调色板通常限于显示256色的显示器。Windows仅保留这些色彩之中的20种供系统使用,但可以改变其他236种色彩。
⑦ 打印
32 进一步探讨设备描述表
想在一个图形输出设备上绘图时,首先必须获得一个设备描述表的句柄。将句柄返回给程序时,Windows就给了用户使用设备的权限。然后在GDI函数中将该句柄作为一个参数,向Windows标识想在其上进行绘图的设备。
(1)获取设备描述表句柄
如果在处理一条消息时获取了设备描述表句柄,应该在退出窗口函数之前释放它。
获取设备描述表句柄的几种方法:
① 在处理WM_PAINT消息时,使用BeginPaint和EndPaint调用
hdc = BeginPaint(hwnd, &ps);
GDI操作
EndPaint(hwnd, &ps);
注:变量ps是类型为PAINTSTRUCT的结构,该结构的hdc字段是BeginPaint返回的设备描述表句柄。PAINTSTRUCT结构包含了一个名为rcRect的RECT结构,该结构定义了包围窗口无效范围的矩形。使用从BeginPaint获得的设备描述表句柄,只能在这个区域内绘图。BeginPaint调用使这个区域有效。
② 可以在处理非WM_PAINT消息时获取设备描述表句柄
hdc = GetDC(hwnd);
GDI操作
ReleaseDC(hwnd, hdc);
注:这个设备描述表适用于窗口句柄为hwnd的客户区。这个调用可以在整个客户区上绘图。
③ Windows程序还可以获取适用于整个窗口的设备描述表句柄
hdc = GeWindowstDC(hwnd);
GDI操作
ReleaseDC(hwnd, hdc);
注:这个设备描述表除了客户区之外,还包括窗口的标题栏、菜单、滚动条和框架。如果要使用该函数,必须捕获WM_NCPAINT消息。
④ 获取整个屏幕的设备描述表句柄
hdc = CreateDC(TEXT(“DISPLAY”), NULL, NULL, NULL);
原型是:hdc = CreateDC(pszDriver, pszDevice, pszOutput, pData);
GDI操作
DeleteDC(hdc);
⑤ 如果只需要获取关于某设备描述表的一些信息,而并不进行任何绘画,在这种情况下,可以使用CreateIC来获取一个“信息描述表”的句柄,其参数和CreateDC一样。
⑥ 一个设备描述表通常是指一个物理显示设备。通常,需要获取有关该设备的信息,其中包括显示器的显示尺寸和色彩范围。可以通过GetDeviceCaps函数来获取这些信息。
iValue = GetDeviceCaps(hdc, iIndex);
参数iIndex的取值为WINGDI.H头文件中定义的29个标识符之一。
33 用TextOut输出整型的方法
设一开始有整型:int i = 100;
要用TextOut函数将i输出,需要用到三个函数:
① wsprintf
② TEXT宏
③ TextOut
首先得先说明下wsprintf的原型:
int wsprintf(LPTSTR lpOut, LPCTSTR lpFmt,...);
第一个参数:缓冲区,是一个字符数组,一般定义为TCHAR型。
第二个参数:格式字符串,因为第一个参数是TCHAR类型,一定要和TEXT宏联合使用,这样才能在不同的编译环境下都可以顺利编译。
后续参数:要输出的的整型变量。
针对第一个参数:要定义一个TCHAR的字符数组作为缓冲区。
TCHAR szBuffer[10]; //足够大就行了
针对第二个参数,需要使用到TEXT宏。
TEXT宏的原型:
TEXT(LPTSTR string //ANSI or Unicode string);
用来处理要转换的整型,具体用法是:
TEXT(“%d”);
那么上述的两个调用应该写成:
int iLength = 0; //用来保存字符串中的字符个数;
iLength = wsprintf(szBuffer, TEXT(“%d”), i);
上述语句的作用是:将i存进szBuffer中,返回szBuffer存有的字符个数到iLength中。
TextOut的原型是:
TextOut(hdc, x, y, psText, iLength);
第一个参数是设备描述表句柄;
第二个参数是输出的文本的x坐标;
第三个参数是输出的文本的y坐标;
第四个参数是是指向要输出的字符串的指针;
第五个参数是字符串中的字符个数;
那么TextOut函数应该写成:
TextOut(hdc, x, y, szBuffer, iLength);
34 设备的大小
使用GetDeviceCaps函数能获取有关输出设备物理大小的信息。
对于打印机,用“每英寸的点数dpi”表示分辨率。
对于显示器,用水平和垂直的总的像素数来表示分辨率。
用“像素大小”或“像素尺寸”表示设备水平或垂直显示的总像素数。
用“度量大小”或“度量尺寸”表示以每英寸或毫米为单位的显示区域的大小。
像素大小 / 度量大小 = 分辨率
使用SM_CXSCREEN和SM_CYSCREEN参数从GetDeviceCaps得到像素大小;
使用HORZSIZE和VERTSIZE参数从GetDeviceCaps得到度量大小;
两者相除就可以得到水平分辨率和垂直分辨率。
如果设备的水平分辨率和垂直分辨率相等,就称该设备具有“正方形像素”。
因为整个屏幕的度量大小是固定的,所以可以根据分辨率调整水平或垂直显示的像素数。如果分辨率小,那么“像素大小”也就小,也就是说,总像素数少了,那么每个像素的尺寸也就变得大些。
35 字体的大小
现在讨论字体的大小问题,这里不是说字号,而是说字体显示的dpi值。Windows系统默认是每英寸96点,所另外一种选择,就是每英寸120点。
我们在调整分辨率的时候,从小分辨率变化到大分辨率时,会觉得图标的文字变小,那是因为在大分辨率下,每个像素的面积变小,假设一个字需要100个像素来显示,那么从小分辨率变化到大分辨率时,字的总面积就变小了,所以字的大小也就发生变化,而这一变化是字体的大小变化,而不是该字的字号发生变化。
在传统的排版中,字体的字母大小由“磅”表示。1磅≈1/72英寸,在计算机排版中1磅正好为1/72英寸。
理论上,字体的磅值是从字体中最高的字符顶部到字符下部的字符底部的距离,其中不包括重音号。根据TEXTMETRIC结构,字体的磅值等于tmHeight – tmInternalLeading。
36 关于色彩
“全色”视频显示器的分辨率是每个像素24位:8位红色、8位绿色、8位蓝色。
“高彩色”显示分辨率是每个像素16为:5位红色、6位绿色、5位蓝色。
显示256种颜色的视频适配器每个像素需要8位。然而这些8位的值一般由定义实际颜色的调色板表组织。
使用GetDeviceCaps可以使程序员确定视频适配器的存储组织,以及能够表示的色彩数目。
这个调用返回色彩平面的数目:iPlanes = GetDeviceCaps(hdc, PLANES);
这个调用返回每个像素的色彩位数:iBitsPixel = GetDeviceCaps(hdc, BITSPIXEL);
大多数彩色图形显示设备要么使用多个色彩平面,要么每像素有多个色彩位,但是不能同时二者兼用;即这两个调用必须有一个返回1.(一般都是第一个返回1)。
在大多数GDI函数调用中,使用COLORREF值(32位)来表示一种色彩。
31 |
… |
24 |
23 |
… |
16 |
15 |
… |
8 |
7 |
… |
0 |
0 |
蓝 |
绿 |
红 |
理论上,COLORREF可以指定2的24次方或1600万种色彩。
这个无符号长整数常常称为一个“RGB色彩”。在使用RGB(r, g, b);宏时注意参数的顺序是红、绿、蓝。而在无符号长整数中,由高位到低位是0、蓝、绿、红。
当三个参数都是0时,表示黑色,当三个参数都是255时,表示白色。
黑色 = RGB(0,0,0) = 0x00000000
白色 = RGB(255, 255, 255) = 0x00FFFFFF
37 保存设备描述表
通常,在调用GetDC或BeginPaint时,Windows会用默认值创建一个新的设备描述表,对设备描述表其属性所做的一切修改在调用ReleaseDC或EndPaint被释放掉。
如果需要使用非默认的设备描述表属性,则必须在每次获取设备描述表句柄时初始化设备描述表。
如果需要在释放设备描述表之后,仍然保存程序中对设备描述表所做的改变,以便在下一次调用GetDC和BeginPaint时它们仍起作用。则应该在窗口类那将CS_OWNDC标志包含进窗口类风格中。
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
现在,基于这个窗口类所创建的每个窗口都将拥有自己的设备描述表,它一直存在,直到窗口被删除。
如果使用了CS_OWNDC风格,就只需初始化设备描述表一次,可以在处理WM_CREATE消息期间完成这一操作。
CS_OWNDC风格只影响GetDC和BeginPaint获得的设备描述表,不影响其他函数获得的设备描述表,如GetWindowDC获得的设备描述表。
在某些情况下,可以需要改变某些设备描述表,用改变后的属性进行绘图,然后又要恢复回改变前的属性。这时,可以通过如下调用来保存设备描述表的状态。
保存:int idSaved = 0; idSaved = SaveDC(hdc);
恢复:RestoreDC(hdc, idSaved);
也可以不保存SaveDC的返回值,这时候如果要恢复,就只能恢复到最近保存的状态,RestoreDC(hdc, -1);
38 写像素
写像素SetPixel(hdc, x, y, crColor);其中:
hdc是设备描述表句柄;
x, y是像素点的坐标;
crColor是要设置的颜色,一般可以用RGB(r, g, b)设置。
39 线条
几种画线函数:
① LineTo:画直线
② Polyline和PolylineTo:画一系列相连的直线
③ PolyPolyline:画多组相连的线
④ Arc和ArcTo和AngleArc:画椭圆线
⑤ PolyBezier和PolyBezierTo:画贝塞尔线条
⑥ PolyDraw:画一系列相连的线以及贝塞尔线条
几种填充函数:
① Rectangle:画矩形
② Ellipse:画椭圆
③ RoundRect:画带圆角的矩形
④ Pie:画椭圆的一部分,使其看起来像一个扇形
⑤ Chord:画椭圆的一部分,使其看起来像弓形
⑥ Polygon:画多边形
⑦ PolyPolygon:画多个多边形
设备描述表的5个属性影响着用这些函数所画线条的外观:
① 当前画笔的位置;
② 画笔;
③ 背景方式;
④ 背景色;
⑤ 绘图模式。
画一条直线,必须调用2个函数,第一个函数指定了线的开始点坐标,第二个函数指出了线的终点坐标:
MoveToEx(hdc, x1, y1, NULL);
LineTo(hdc, x2, y2);
MoveToEx不会画线,只是设置了设备描述表的“当前位置”属性。然后LineTo函数从当前的位置到它所指定的点画一直线。在默认的设备描述表中,当前位置最初是在点(0,0)。
MoveToEx最后一个参数是指向POINT结构的指针。从该函数返回后,POINT结构的x和y字段指出了之前的“当前位置”,如果不需要这个信息,直接填NULL。
如果需要获取当前位置,先定义一个POINT的结构变量pt,然后通过下面的调用:
GetCurrentPositionEx(hdc, &pt);
几个函数的原型:
Rectangle(hdc, xLeft, yTop, xRight, yBottom);
Ellipse(hdc, xLeft, yTop, xRight, yBottom);
RoundRect(hdc, xLeft, yTop, xRight, yBottom, xCorner, yCorner);
Chord(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);
Pie(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);
Arc(hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd);
一个二维的贝塞尔线条由4个点定义——两个端点和两个控制点。曲线的控制点固定,将曲线从两个端点间的直线处拉伸构造曲线。
40 使用画笔
调用任何画笔函数时,Windows使用设备描述表中当前选中的“画笔”来画线。画笔决定线的色彩、宽度、线型。线型可以是实线、点划线、虚线,默认设备描述表中画笔是BLACK_PEN,一个像素宽,实线。
Windows提供三种现有画笔,分别是:BLACK_PEN, WHITE_PEN和NULL_PEN。
Windows使用句柄来引用画笔。用HPEN的类型定义,即画笔的句柄。
HPEN hPen;
调用GetStockObject,可以获得现有画笔的句柄。
hPen = GetStockObject(WHITE_PEN);
调用SelectObject将画笔选进设备描述表。
SelectObject(hdc,hPen);
SelectObject的返回值是选进前设备描述表的画笔句柄。
使用CreatePen或CreatePenIndirect创建一个“逻辑画笔”,这仅仅是对画笔的描述。这些函数返回逻辑画笔的句柄,然后调用SelectObject将画笔选进设备描述表,之后才可以使用新的画笔来画线。
在任何时候,只能有一种画笔选进设备描述表。
在释放设备描述表或在选择了另一种画笔到设备描述表中之后,就可以调用DeleteObject来删除所创建的逻辑画笔。
逻辑画笔是一种“GDI对象”,GDI对象有六种:画笔、刷子、位图、区域、字体、调色板。
CreatePen的原型是:
HPEN CreatePen(iPenStyle, iWidth, crColor);
iPenStyle参数确定画笔是实线、虚线还是点线。
iWidth参数确定线宽,如果iPenStyle不是实线,且iWith大于1,那么画笔将变成实线。
crColor是RGB颜色。
获取当前画笔句柄:
hPen = GetCurrentObject(hdc, OBJ_PEN);
还可以建立一个逻辑画笔LOGPEN结构,调用CreatePenIndirect来创建画笔。
LOGPEN logpen;
此结构有三个成员:UINT lopnStyle 是画笔线型;POINT lopnWidth是按逻辑单位度量的画笔宽度,只用其中的x值;COLORREF lopnColor是画笔颜色。
41 填充空隙
点式画笔和虚线画笔的空隙的着色取决于设备描述表的两个属性——背景模式和背景颜色。默认的背景模式是OPAQUE,在这种方式下,Windows使用背景色填充空隙,默认的背景色为白色。
下述调用用来改变和获取Windows用来填充空隙的背景色:
改变:SetBkColor(hdc, crColor);
获取:GetBkColor(hdc);
下述调用用来改变和获取背景模式:
改变:SetBkMode(hdc, 模式);
模式:TRANSPARENT,忽略背景色,并且不填充空隙。
OPAQUE默认。
获取:GetBkMode(hdc);
42 绘图方式
设备描述表中定义的绘图方式也影响显示器上所画线的外观。
当Windows使用画笔来画线时,实际上执行画笔像素与目标位置处原来像素之间的某种按位布尔运算。像素间的按位布尔运算叫做“光栅运算”,简称为“ROP”。由于画一条直线只涉及两种像素(画笔和目标),因此这种布尔运算又称为“二元光栅运算”,简称为“ROP2”。
在默认设备描述表中,绘图方式定义为R2_COPYPEN,这意味着Windows只是将画笔像素复制到目标像素代替之。
Windows定义了16种不同的ROP2码,用来设置不同的绘图方式。
设置绘图方式:SetROP2(hdc, iDrawMode);
获取绘图方式:iDrawMode = GetROP2(hdc);
43 绘制填充区域
Windows中有7个用来画带边缘的填充图形的函数:
① Rectangle:画矩形
② Ellipse:画椭圆
③ RoundRect:画带圆角的矩形
④ Pie:画椭圆的一部分,使其看起来像一个扇形
⑤ Chord:画椭圆的一部分,使其看起来像弓形
⑥ Polygon:画多边形
⑦ PolyPolygon:画多个多边形
Windows用设备描述表中选择的当前画笔来画图形的边界框,边界框还使用当前背景方式、背景色彩和绘图方式,跟画线时一样。
图形以当前设备描述表中选择的刷子来填充。默认情况下,使用现有对象,这意味着图形内部将画成白色。
Windows定义6种现有刷子:WHITE_BRUSH、LTGRAY_BRUSH、GRAY_BRUSH、DKGRAY_BRUSH、BLACK_BRUSH和NULL_BRUSH。
也可以自己定义刷子 HBRUSH hBrush;
通过GetStockObject来获取现有刷子:
hBrush = GetStockObject(WHITE_BRUSH);
通过SeletctObject将刷子选进设备描述表:
SelectObject(hdc, bBrush);
如果要画一个没有边界框的图形,可以将NULL_PEN选进设备描述表。
SelectObject(hdc, GetStockObject(NULL_PEN));
如果要画一个没有填充内部的图像,可以将NULL_BRUSH选进设备描述表。
SelectObject(hdc, GetStockObject(NULL_BRUSH));
画多边形函数的原型:
Polygon(hdc, apt, iCount);
apt参数是POINT结构的一个数组,iCount是点的数目。如果该数组中的最后一个点和第一个点不同,则Windows将会再加一条线,将最后一个点与第一个点连起来。
画多个多边形函数的原型:
PolyPolygon(hdc, apt, aiCounts, iPolyCount);
apt数组具有全部多边形的所有点。
aiCounts数组给出了多边形的端点数。
iPolyCount给出了所画的多边形的个数。
44 用画刷填充内部
Rectangle、RoundRect、Ellipse、Chord、Pie、Polygon和PolyPolygon图形的内部是用选进设备描述表的当前画刷来填充的。画刷是一个8×8的位图,它水平和垂直地重复使用来填充内部区域。
Windows有5个函数,可以自己创建逻辑画刷,然后用SelectObject将画刷选进设备描述表。
① hBrush = CreateSolidBrush(crColor); 纯颜色刷子
② hBrush = CreateHatchBrush(iHatchStyle, crColor); 带影射线的刷子
crColor是影线的颜色,影线的间隙用设备描述表定义的背景方式和背景色来着色。
③ CreatePatternBrush()
④ CreateDIBPatternBrushPt() 基于位图的刷子
⑤ hBrush = CreateBrushIndirect(&logbrush);
该函数包含其他4个函数。
变量logbrush是一个类型为LOGBRUSH的结构,该结构有三个字段
UINT lbStyle;
COLORREF lbColor;
LONG lbHatch;
45 矩形函数
Windows包含了几种使用RECT结构和“区域”的绘图函数。区域就是屏幕上的一块地方,是矩形,多边形和椭圆的组合。
FillRect(hdc, &rect, hBrush);
用指定画刷来填充矩形。该函数不需要事先将画刷选进设备描述表。
FrameRect(hdc, &rect, hBrush);
使用画刷画矩形框,但不填充矩形。
InvertRect(hdc, &rect);
将矩形中所有像素反转。
常用矩形函数:
① SetRect(&rect, xLeft, yTop, xRight, yBottom); 设置矩形的4个字段值。
② OffsetRect(&rect, x, y); 将矩形沿x轴和y轴移动几个单元。
③ InflateRect(&rect, x, y); 增减矩形尺寸
④ SetRectEmpty(&rect); 将矩形各字段设为0
⑤ CopyRect(&DestRect, &SrcRect); 将矩形复制给另一个矩形。
⑥ IntersectRect(&DestRect, &SrcRect1,&ScrRect2);获取两个矩形的交集
⑦ UnionRect(&DestRect, &SrcRect1,&ScrRect2); 获取两个矩形的并集
⑧ bEmpty = IsRectEmpty(&rect); 确定矩形是否为空
⑨ binRect = PtinRect(&rect, point);确定点是否在矩形内
46 创建和绘制区域
区域是对显示器上一个范围的描述,这个范围是矩形、多边形和椭圆的组合。
区域可以用于绘制和剪裁,通过将区域选进设备描述表,就可以用区域来进行剪裁。
当创建一个区域时,Windows返回一个该区域的句柄,类型为HRGN。
HRGN hRgn;
① 创建矩形区域:
hRgn = CreateRectRgn(xLeft, yTop, xRight, yBottom);
或
hRgn = CreateRectRgnIndirect(&rect);
② 创建椭圆区域:
hRgn = CreateEllipticRgn(xLeft, yTop, xRight, yBottom);
或
hRgn = CreateEllipticRgnIndirect(&rect);
③ 创建多边形区域:
hRgn = CreatePolygonRgn(&point, iCount, iPolyFillMode);
point参数是个POINT类型的结构数组;
iCount是点的数目;
iPolyFillMode是ALTERNATE或者WINDING
④ 区域的融合
iRgnType = CombineRgn(hDestRgn, hSrcRgn1, hSrcRgn2, iCombine);
这一函数将两个源区域组合起来并用句柄hDestRgn指向组合成的目标区域。
iCombine参数说明了hSrcRgn1和hSrcRgn2是怎么组合的。
RGN_AND 公共部分
RGN_OR 全部
RGN_XOR 全部除去公共部分
RGN_DIFF hSrcRgn1不在hSrcRgn2的部分
RGN_COPY hSrcRgn1的全部,忽略hSrcRgn2
区域的句柄可以用到4个绘图函数:
FillRgn(hdc, hRgn, hBrush);
FrameRgn(hdc, hRgn, xFrame, yFrame);
xFrame, yFrame是画在区域周围边框的宽度和高度。
InvertRgn(hdc, hRgn);
PaintRgn(hdc, hRgn);
47 矩形与区域的剪裁
区域也在剪裁中扮演了一个角色。
InvalidateRect函数使显示的一个矩形区域失效,并产生一个WM_PAINT消息。
InvalidateRect(hwnd, NULL, TRUE); 清除客户区;
可以通过调用GetUpdateRect来获取失效矩形的坐标。
使用ValidateRect函数使客户区的矩形有效。
当接收到一个WM_PAINT消息时,无效矩形的坐标可以从PAINTSTRUCT结构中得到,该结构是用BeginPaint函数填充的。
Windows中有两个作用于区域而不是矩形的函数:
InvalidateRgn(hwnd, hRgn, bErase);
和
ValidateRgn(hwnd, hRgn);
所以当接收到一个WM_PAINT消息时,可能由无效区域引起的。剪裁区域不一定是矩形。
SelectObject(hdc, hRgn);
或
SelectClipObject(hdc, hRgn);
通过将一个区域选进设备描述表来创建自己的剪裁区域。