图形设备接口(GDI)是一个可执行程序,它接受Windows应用程序的绘图请求(表现为GDI函数调用),并将它们传给相应的设备驱动程序,完成特定于硬件的输出,象打印机输出和屏幕输出。GDI负责Windows的所有图形输出,包括屏幕上输出像素、在打印机上输出硬拷贝以及绘制Windows用户界面。也就是Windows的图形编程。
一、GDI体系结构
1、 GDI32.DLL导出的函数
GDI提供几百个Windows程序中可以调用的函数。这些函数大多数是从Win32的子系统DLL GDI32.DLL中导出的。窗口管理模块UER32.DLL是使用GDI函数的大用户,它用GDI函数来绘制菜单、图标、滚动条、标题栏和每个窗口的框架等细节内容。有一些绘图函数从USER32.DLL导出,提供给应用程序。仅Windows2000 GDI32.DLL就导出了543个入口点。与DevStudio一起发行的dumpbin工具是一个简单的工具,能列出模块导出的函数。下面就是由dumpbin gdi32.dll /export 产生的部分内容:
AbortDoc
AbortPath
AddFontMemResourceEx
AddFontResourceA
AddFontResourceExA
AddFontResourceExW
AddFontResourceTracking
AddFontResourceW
AngleArc
AnimatePalette
AnyLinkedFonts
Arc
ArcTo
具体查看:http://www.cnblogs.com/del/archive/2008/03/11/1101291.html
2、 GDI函数分类
GDI的功能太多了,所以我们需要一种办法对Win32 GDI API的函数分类,以便理解GDI的结构,MSDN库将GDI API分成17个领域,清楚地描述了GDI的功能。
(1)位图:处理创建、绘制设备相关位图(DDB)、设备无关位图(DIB)、DIB段、像素和区域填充的函数。
(2)画刷:处理创建、修改GDI画刷对象的函数。
(3)剪裁:处理设备上下文可绘制区域的函数。
(4)颜色:调色板管理。
(5)坐标和变换:处理映射模式、设备坐标映射逻辑和通用变换矩阵的函数。
(6)设备上下文:创建设备上下文,查询、设置其属性,及选择GDI对象的函数。
(7)填充形状:绘制闭合区域及其周线的函数。
(8)字体和文本:在系统中安装和枚举字体,并用它们绘制文本字符串的函数。
(9)直线和曲线:绘制直线、椭圆曲线和贝赛尔曲线的函数。
(10)元文件:处理Windows格式的元文件或增强型元文件的生成和回放的函数。
(11)多显示监视器:允许在一个系统中使用多个显示监视器的函数。这些函数实际上是从uer32.dll导出的。
(12)画图和绘图:负责绘图消息管理和窗口已绘图区域的函数。其中一些函数实际上是从uer32.dll导出的。
(13)路径:负责将一系列直线和曲线组成名为路径的GDI对象,并用它来绘制的函数。
(14)画笔:处理直线绘制属性的函数。
(15)打印和打印池:负责将GDI绘图命令发送到硬拷贝设备(如行式打印机和绘图仪)并平滑地管理这些任务的。打印池函数是由Win32打印池提供的,包括几个系统提供的DLL和销售自定义的模块。
(16)矩形:user32.dll提供的处理RECT结构的函数。
(17)区域:负责用区域GDI对象描述一个点集的函数,并对该点集进行操作。
还有一些没有文档记载的函数。有一些GDI函数在DDK中说明,还一些没有文档记载但系统DLL使用的函数,另处还有一些函数则没有用过。下面是这些函数的粗略分类:
用户模式打印机驱动程序。
OPENGL。
EUDC。
其他系统DLL支持。
其他没有文档记载的函数。
3、 GDI对象句柄
32位的GDI句柄由8位未知的高位、1位堆对象标记、7位对象类型信息和高4位是0的16位索引组成。借助7位对象类型信息,可以确定设备上下文、区域、位图、调色板、字体、画刷、加强型图元文件、画笔和扩展画笔。
4、GDI对象类型
GDI对象表
typedef struct
{
void * pKernel;
unsigned short nProcess;
unsigned short nCount;
unsigned short nUpper;
unsigned short nType;
void * pUser;
}GdiTableCell;
(1) pKernel指向页面池:对每个有效GDI对象,pKernel从不为空,并且值总是唯一的。因此看起来对每个GDI对象有一个相应的数据结构,这个数据结构只能从内核模式代码存取,甚至不能从GDI32.DLL直接存取。对于不同进程的对象,从pKernel的值中看不出明显区分区域来。pKernel指向的对象起始地址是0xE1000000,根据《Inside Windows NT》,起始地址是0xE1000000的区域一般是被称为“页面池”的可分页系统的内存堆。
(2)nCount 是一个部分选择计数器:在Windows 2000下,nCount总是零,就是说未使用。但在Windows NT 4.0中,某些GDI对象用了它。为了理解nCount的意义,我们试着将对象句柄选入和取消一个或多个设备上下文中,观察选入和取消是否能根据nCount值的变化而成功。
(3)nProcess使得GDI句柄绑定到进程:如果程序想使用另一个进程的GDI对象句柄,Win32 API调用一般会失败。GdiTableCell中的nProcess字段就是这种现象背后的原因。对于库存(stock)对象,如GetStockObject(BLACK_PEN),nProcess被置为零。对于用户进程创建的其他 GDI对象,nProcess是创建进程的进程标识符。
有了这个字段,GDI就会很容易地检查当前进程标识符是否和GDI对象的nProcess字段一致,目的是强制对象句柄不能在另一个进程中访问的规则。
根据微软的文档,进程终止时,由该进程创建的所有GDI对象会被释放。如果你想知道这是怎样实现的,我们现在有一点线索了。GDI只需搜索GDI对象表并删除有指定进程标识符的对象。
(4)nUpper:再次检查句柄:我们发现GDI对象表入口的nUpper字段是4字节GDI对象句柄的高两个字节的完全拷贝——对GDI对象句柄进行错误检查是低成本的冗余校验。
(5)nType:内部对象类型:nType的低字节通常和HGDIOBJ中的是7位类型信息相同,高位字节通常是零。
(6)pUser指向用户模式数据结构。
GDI对象类型列表:
5、GDI函数大致分类
设备上下文函数(如GetDC、CreateDC、DeleteDC)、 画线函数(如LineTo、Polyline、Arc)、填充画图函数(如Ellipse、FillRect、Pie)、画图属性函数(如SetBkColor、SetBkMode、SetTextColor)、文本、字体函数(如TextOut、GetFontData)、位图函数(如SetPixel、BitBlt、StretchBlt)、坐标函数(如DPtoLP、LPtoDP、ScreenToClient、ClientToScreen)、映射函数(如SetMapMode、SetWindowExtEx、SetViewportExtEx)、元文件函数(如PlayMetaFile、SetWinMetaFileBits)、区域函数(如FillRgn、FrameRgn、InvertRgn)、路径函数(如BeginPath、EndPath、StrokeAndFillPath)、裁剪函数(如SelectClipRgn、SelectClipPath)等。
二、三种图形输出类型
应用程序可以使用GDI创建三种类型的图形输出:矢量输出、位图图形输出和文本输出。
矢量图形输出
矢量图形输出指的是创建线条和填充图形,包括点、直线、曲线、多边形、扇形和矩形的绘制。
位图输出
位图图形的输出是指位图图形函数对以位图形式存储的数据进行操作,它包括各种位图和图标的输出。
在屏幕上表现为对若干行和列的像素的操作,在打印机上则是若干行和列的点阵的输出。
位图图形输出的优点是速度很快,它是直接从内存到显存的拷贝操作。缺点是需要额外的内存空间。
文本输出
与DOS字符方式下的输出不同,Windows是按图形方式输出的。
这样,在输出文本时,必须以逻辑坐标为单位计算文本的输出位置,而不是象DOS下以文本行为单位输出文本。这比DOS下的文本输出要难一些。
按图形方式输出文本给文本输出带来很大的灵活性。用户可以通过调用各种GDI函数,制造出各种文本输出效果,包括加粗、斜体、设置颜色等。
Windows还提供了一种TrueType(写真字体)。TrueType字体用一组直线和曲线命令及一些参数来描述字体的轮廓。Windows可以通过参数来调整直线的长度和曲线的形状,从而实现对字体的自由缩放。
三、设备描述表(DC)
在Windows环境中,各程序的输出必须限制在自己的窗口中。
GDI使用一种简单的机制保证在窗口中画图的各程序遵循这个规则。这种机制即为设备描述表(DC);当Windows程序在屏幕、打印机或其它设备上画图时,它并不是将像素直接输出到设备上,而是将图绘制到由设备描述表表示的逻辑意义上的"显示平面"上去。设备描述表是深寓于Windows中的一种数据结构,它包含GDI需要的所有关于显示平面情况的描述字段,包括相连的物理设备和各种各样的状态信息。
获取与窗口关联的设备上下文,应用程序不能使用CreateDC创建与一个窗口相关的设备上下文,但是WIN32 API提供了几个获取与窗口相关的设备上下文的函数,包括:
HDC GetWindowDC(HWND hWnd)
HDC GetDC(HWND hWnd)
HDC GetDCEx(HWND hWnd,HRGN hrgnClip,DWORD flags)
HDC BeginPaint(HWND hWnd,LPPAINTSTRUCT lpPaint)
使用GetDCEx可以代替其他的函数。例如,GetDCEx(hWnd,NULL,DCX_WIND|DCX_NORESETATTRS)可以轻松地替代GetWindowDC(hWnd),而GetDCEx(hWnd,NULL,DCX_NORESETATTRS)可以替代GetDC(hWnd)。
设备上下文类包括CDC和它的派生类CClientDC、CPaintDC、CWindowDC、CMetaFileDC。
CDC、CclientDC、CpaintDC、CwindowDC、CmetaFileDC含义与区别
CDC是设备上下文类的基类,除了一般的窗口显示外,还用于基于桌面的全屏幕绘制和非屏幕显示的打印机输出。CDC类封装了所有图形输出函数,包括矢量、位图和文本输出。
CClientDC(客户区设备上下文)用于客户区的输出,它在构造函数中封装了GetDC(),在析构函数中封装了ReleaseDC()函数。一般在响应非窗口重画消息(如键盘输入时绘制文本、鼠标绘图)绘图时要用到它。
CClientDC dc(this);//this一般指向本窗口或当前活动视图
dc.TextOut(10,10,str,str.GetLength());
CPaintDC用于响应窗口重绘消息(WM_PAINT)时的绘图输出。CPaintDC在构造函数中调用BeginPaint()取得设备上下文,在析构函数中调用EndPaint()释放设备上下文。EndPaint()除了释放设备上下文外,还负责从消息队列中清除WM_PAINT消息。因此,在处理窗口重画时,必须使用CPaintDC,否则WM_PAINT消息无法从消息队列中清除,将引起不断的窗口重画。CPaintDC也只能用在WM_PAINT消息处理之中。
CWindowDC用于窗口客户区和非客户区(包括窗口边框、标题栏、控制按钮等)的绘制。除非要自己绘制窗口边框和按钮(如一些CD播放程序等),否则一般不用它。
CMetaFileDC专门用于图元文件的绘制。图元文件记录一组GDI命令,可以通过这一组GDI命令重建图形输出。使用CMetaFileDC时,所有的图形输出命令会自动记录到一个与CMetaFileDC相关的图元文件中。
BeginPaint和GetDC区别
为什么WINDOWS要提出无效区域的概念呢?这是为了加速。因为BeginPaint和EndPaint用到的设备描述符只会在当前的无效区域内绘画,在有效区域内的绘画会自动被过滤,大家都知道,WIN GDI的绘画速度是比较慢的,所以能节省一个象素就节省一个,不用吝啬,这样可以有效加快绘画速度。
可见BeginPaint、EndPaint是比较“被动”的,只在窗口新建时和被摧残时才重画。
而GetDC用于主动绘制,只要你指到哪,它就打到哪。它不加判断就都画上去,无效区域跟它没关系。对话框没被覆盖没被摧残,它很健康,系统没要求它重画,但开发者有些情况下需要它主动重画:比如一个定时换外观的窗口,这时候就要在WM_TIMER处理代码用GetDC。这时候再用BeginPaint、EndPaint的话,会因为无效区域为空,所有绘画操作都将被过滤掉。
由于WM_PAINT消息的优先级很低,这样,由于窗口对象不能及时收到WM_PAINT消息而影响用户对屏幕对象的视觉感觉。为弥补这个缺陷,程序员可以考虑使用函数UpdateWindows(),它在应用程序的消息队列中存在WM_PAINT消息的情况下,强使Windows立即向窗口对象发送WM_PAINT消息.
//由于WM_PAINT的优先级别很低(甚至在虚拟按键消息之后,见《windows核心编程》窗口消息一章),它只是改变了消息结构体中的QS_PAINT标志。所以呢,如果是使用函数UpdateWindows(),会直接send一个WM_PAINT消息,那样会直接调用窗口处理函数,比普通的WM_PAINT消息处理的快很多。
case WM_LBUTTONDOWN: PAINTSTRUCT ps;这个结构就是为了填充无效区域的坐标等等, 这样BeginPaint就可以只画无效区域了,提高了效率。 |
BeginPaint可以使无效区域变有效,GetDC不改变区域属性,无效的还是无效,有效的依然是有效。
BeginPaint在WM_PAINT消息里使用,GetDC则可以在所有的消息中使用,一般是非WM_PAINT消息。
四、图形对象
图形对象类包括CGdiObject、画笔、刷子、字体、位图、调色板、区域等。
CGdiObject是图形对象类的基类,但该类不能直接为应用程序所使用。
要使用GDI对象,必须使用它的派生类:画笔、刷子、字体、位图、区域等等。
使用图形对象要注意两点:
a.同其他MFC对象一样,GDI对象的创建也要分为两步:第一步,是定义一个GDI绘图对象类的实例;第二步调用该对象的创建方法真正创建对象。
b.创建对象:使用该对象,首先要调用CDC::SelectObject()将它选入到设备上下文中,同时保存原来的设置到一个GDI对象指针比如说pOldObject中。在使用完后,再用SelectObject(pOldObject)恢复原来的设置。
1、GDI画笔和CPen类
Windows用当前选入设备描述表的画笔绘制直线和曲线,并给用Rectangle,Ellipse以及其他图形生成函数画出的图形镶画边框。默认画笔画出的是一个像素点宽的黑色实线。
如果要改变画线方式,则需创建一个GDI画笔,并由CDC::SelectOjbect将它选设备描述表,MFC用类CPen表示GDI画笔。
创建画笔的方法
创建画笔的最简单的方法是构造一个CPen对象并把定义画笔所用的参数都传送给该对象
CPen pen(PS_SOLID,1,RGB(255,0,0));
创建画笔的第二种方法是构造 一个没有初始化的CPen对象并调用CPen::CreatePen:
CPen pen;
pen.CreatePen(PS_SOLID,1,RGB(255,0,0));
创建画笔的第三种方法是构造一个没有初始化的CPen对象,向LOGPEN结构中填充描述画笔特性的参数,然后调用CPen::CreatePenIndirect生成画笔。
CPen pen;
<p cl