一.关于GDI的基本概念
什么是GDI?
Windows绘图的实质就是利用Windows提供的图形设备接口GDI(Graphics Device Interface)将图形绘制在显示器上。
在Windows操作系统中,动态链接库C:/WINDOWS/system32/gdi32.dll(GDI Client DLL)中定义了GDI函数,实现与设备无关的包括屏幕上输出像素、在打印机上输出硬拷贝以及绘制Windows用户界面功能。在Visual C++6.0中的头文件C:/Program Files/Microsoft Visual Studio/VC98/Include/wingdi.h和Visual Studio 2005中的头文件C:/Program Files/Microsoft Visual Studio 8/VC/PlatformSDK/Include/WinGDI.h是访问gdi32.dll库文件的钥匙。下面我们大致浏览一下wingdi.h(included in Windows.h)头文件:
/* Bitmap Header Definition */定义了BITMAP位图结构
/* Mapping Modes */定义了DC中的坐标映射方式,包括以下常用函数:
SetMapMode、SetViewportExtEx、SetViewportOrgEx、 SetWindowExtEx 、SetWindowOrgEx。
/* Stock Logical Objects */系统预定义的堆(STOCK)对象,包括BRUSH、PEN和FONT对象
/* Brush Styles */定义了画刷格式,包括SOLID、HOLLOW、HATCHED等格式
/* Hatch Styles */定义了画刷阴影格式,包括:
HS_VERTICAL /* ||||| */
HS_FDIAGONAL /* ///// */
HS_BDIAGONAL /* ///// */
HS_CROSS /* +++++ */
HS_DIAGCROSS /* xxxxx */
/* Pen Styles */定义了画笔格式,包括SOLID、DASH、DOT等格式
什么是DC?
设备环境DC(Device Context),也称为设备描述表或设备上下文。
设备环境保存了绘图操作中一些共同需要设置的信息,如当前的画笔、画刷、字体和位图等图形对象及属性,以及坐标映射、颜色和背景等影响图形输出的绘图模式。形象的说,一个设备环境提供了一张画布和一些绘画的工具,我们可以使用不同格式、颜色的绘画工具在上面涂鸦。这里,设备环境中的“设备”是指任何类型的显示器或打印机等输出设备,绘图时,我们不必关心所使用设备的编程的原理和方法,所有的绘制操作必须通过设备环境进行间接的处理,Windows会自动将设备环境所描述的结构映射到相应的物理设备上。
从根本上来说,DC它是Windows内部使用的数据结构,它存储着向设备输出时说需要的信息,应用程序利用它定义图形对象及其属性,并实现应用程序、设备驱动程序和输出设备之间绘图命令的转换。要想调用GDI函数向某个区域输出文字或绘制图形,必须先取得或建立设备环境句柄,应用程序每一次绘图操作均按照设备环境中的设置的绘图属性进行。
设备环境不像其他Windows结构,在程序中不能直接存取设备环境结构,只能通过系统提供的一系列函数或使用设备环境的句柄HDC来间接地获取或设置设备环境结构中的各项属性,这些属性包括显示器高度和宽度、支持的颜色数和分辨率等。
MFC中与GDI有关的类
为了支持GDI绘图,MFC提供了两种重要的类:设备环境DC(Device Context)类,用于设置绘图属性和绘制图形;绘图对象类,封装了各种GDI绘图对象,包括画笔、刷子、字体、位图、调色板和区域。
在MFC中,CDC是设备环境类的基类,除了一般的窗口显示外,还用于基于桌面的全屏幕绘制和非屏幕显示的打印机输出。CDC类封装了所有图形输出函数,包括矢量、光栅和文本输出。CDC的派生类包括CClientDC、CPaintDC、WindowDC、CMetaFileDC。
(1)CPaintDC类是一个来自CDC的设备环境类。它在构造期间执行CWnd::BeginPaint,在析构期间执行CWnd::EndPaint,EndPaint()除了释放设备环境外,还负责从消息队列中清除WM_PAINT消息。一个CPaintDC对象只在响应一个窗口重绘消息(WM_PAINT)的时候被使用,通常是在你的OnPaint消息处理成员函数中。因此,在处理窗口重画时,必须使用CPaintDC,否则WM_PAINT消息无法从消息队列中清除,将引起不断的窗口重画。
CPaintDC类成员:
数据成员
m_ps:包含了用于画客户区的PAINTSTRUCT
m_hWnd: CPaintDC对象所附着的HWND
构造函数CPaintDC:构造一个连接到指定的CWnd上的CPaintDC对象
(2)CClientDC(窗口客户区设备环境)类用于管理窗口用户区对应的显示上下文,它在构造时调用了Windows函数GetDC,在析构时调用了ReleaseDC。这意味着和CClientDC对象相关的设备上下文是窗口的客户区。一般在响应非窗口重画消息(如键盘输入时绘制文本、鼠标绘图)绘图时要用到它。
CClientDC类的成员:
构造函数CClientDC:构造一个连接到CWnd上的CClientDC对象数据成员
数据成员m_hWnd:所在的有效窗口的HWND
(3)CWindowDC(窗口设备环境)类用于管理与整个窗口对应的显示上下文,包括它的结构和控件。它在构造的时候调用Windows函数GetWindowDC,在销毁的时候调用ReleaseDC。这意味着CWindowDC对象可以访问CWnd的全部屏幕区域(包括客户区和非客户区)。它用于窗口(包括窗口边框、标题栏、控制按钮等)的绘制,除非要自己绘制窗口边框和按钮(如一些CD播放程序等),否则一般不用它。
CWindowDC类成员:
Construction CWindowDC:构造一个CWindowDC对象
Data Members m_hWnd:与这个CWindowDC相关联的HWND句柄
(4)CMetaFileDC专门用于图元文件的绘制。图元文件记录一组GDI命令,可以通过这一组GDI命令重建图形输出。使用CMetaFileDC时,所有的图形输出命令会自动记录到一个与CMetaFileDC相关的图元文件中。
(5)此外我们还可以利用Windows内存DC进行绘图,此时涉及到屏幕DC和内存DC。把所要绘制的一切先在内存DC中进行绘制,之后全部搬到屏幕DC中,从而把所有繁琐的绘制过程都在内存DC中完成了,我们在屏幕上看到的是一幅完整的图画,所以不可能出现闪烁的情况。
二.MFC中GDI绘图
GDI绘图包括以下步骤:获取设备环境,设置坐标映射,创建绘图工具,调用DC绘图函数绘图。
1、获取设备环境
(1)在SDK编程中,获取设备环境的方法有两种:
<1>通过API函数BeginPaint。应用程序响应WM_PAINT消息进行图形刷新时主要通过BeginPaint函数获取设备环境,在消息处理函数返回前调用API函数EndPaint释放设备环境。
函数原型为:WINUSERAPI HDC WINAPI BeginPaint( HWND hWnd,LPPAINTSTRUCT lpPaint);
//以下为Win API示例::BeginPaint(HWND hWnd, LPPAINTSTRUCT lpPaint);
case WM_PAINT://窗口客户区需要重绘
{
char szText[]="Hello World";
PAINTSTRUCT ps;
HDC hdc=::BeginPaint(hWnd,&ps);
::TextOut(hdc,10,10,szText,strlen(szText));
::EndPaint(hWnd,&ps);
return 0;
}
MFC对BeginPaint进行了封装:
CWnd::BeginPaint,CDC* BeginPaint( LPPAINTSTRUCT lpPaint ); 等价于::BeginPaint(CWnd::m_hWnd, LPPAINTSTRUCT lpPaint);
<2>通过API函数GetDC。在非WM_PAINT消息处理函数中,需要调用GetDC来获取设备环境,调用API函数ReleaseDC来释放设备环境。
函数原型为:WINUSERAPI HDC WINAPI GetDC( HWND hWnd);
(2)在MFC中,MFC提供了不同类型的DC类,每一个类都封装了DC句柄,并且它们的构造函数自动调用获取DC的API函数,析构函数自动调用释放DC的API函数。因此,在程序中通过声明一个MFC设备环境类的对象就自动获取了一个DC,而当该对象被销毁时就自动释放了获取的DC。MFC AppWizard应用程序向导创建的OnDraw()函数自动支持所获取的DC。
<1> CPaintDC构造函数:CPaintDC(CWnd* pWnd); 构造一个CPaintDC对象(pWnd指向一个CPaintDC对象所属的CWnd对象),准备用于绘画的应用程序窗口。
// BeginPaint
void CView::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: Add your message handler code here
OnPrepareDC(&dc);
OnDraw(&dc)
}
当我们改变了窗口尺寸、移动窗口或恢复了先前被覆盖的部分,应用程序窗口就会收到一个Windows系统发送来的WM_PAINT消息,然后调用基类Cview的OnPaint函数或我们自己添加的消息处理函数OnPaint。我们可以在OnPaint函数中重绘窗口中重新可见的部分(),但简单的处理办法是重绘整个窗口。上面的代码中,由于基类Cview的OnPaint函数调用了OnDraw函数,因此应用程序经常在OnDraw函数中绘制视图。
<2>CClientDC构造函数:CClientDC(CWnd* pWnd); 构造一个CClientDC对象,它将存取pWnd指向的CWnd的客户区。
// 鼠标左键事件处理
void CExView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);//定义客户区设备环境
dc.LineTo(point);//绘制线段
}
CClientDC代表了窗口客户区对应的显示上下文,它在构造时调用了API函数GetDC,并将当前窗口的句柄m_hWnd作为函数参数;在析构时调用了API函数ReleaseDC。当在客户去绘图时,需要利用CClientDC类定义一个客户区设备环境句柄。
有时候需要访问与一个客户设备环境相关联的窗口对象,可以通过Attach函数把这个CClientDC的成员m_hWnd句柄传递给一个窗口对象,该窗口就是与客户区设备环境相关联的窗口。
CWnd::Attach,BOOL Attach( HWND hWndNew );
说明:将一个Windows窗口与CWnd对象相连接。
返回值:如果成功,则返回非零值;否则返回0。
参数:hWndNew指定了Windows窗口的句柄
<3>CWindowDC构造函数:CWindowDC( CWnd* pWnd );构造一个CWindowDC对象,它可以访问pWnd指向的CWnd对象的整个屏幕区域(包括客户区和非客户区)。比如我们在做屏幕保护程序时,一般以整个屏幕区域作为绘制区域。
2、设置坐标映射
(1)Windows坐标系统
Windows坐标系分为逻辑坐标系和设备坐标系两种,GDI支持这两种坐标系。一般而言,GDI的文本和图形输出函数使用逻辑坐标,而在客户区移动或按下鼠标的鼠标位置是采用设备坐标。
<1>逻辑坐标系是面向DC的坐标系,这种坐标不考虑具体的设备类型,在绘图时,Windows会根据当前设置的映射模式将逻辑坐标转换为设备坐标。
<2>设备坐标系是面向物理设备的坐标系,这种坐标以像素或设备所能表示的最小长度单位为单位,X轴方向向右,Y轴方向向下。设备坐标系的原点位置(0, 0)不限定在设备显示区域的左上角。
设备坐标系分为屏幕坐标系、窗口坐标系和客户区坐标系三种相互独立的坐标系。
屏幕坐标系以屏幕左上角为原点,一些与整个屏幕有关的函数均采用屏幕坐标,如GetCursorPos()、SetCursorPos()、CreateWindow()、MoveWindow()。弹出式菜单使用的也是屏幕坐标。
窗口坐标系以窗口左上角为坐标原点,它包括窗口标题栏、菜单栏和工具栏等范围。
客户区坐标系以窗口客户区左上角为原点,主要用于客户区的绘图输出和窗口消息的处理。鼠标消息的坐标参数使用客户区坐标,CDC类绘图成员函数使用与客户区坐标对应的逻辑坐标。
(2)坐标之间的相互转换
编程时,有时需要根据当前的具体情况进行三种设备坐标之间或与逻辑坐标的相互转换。
MFC提供了两个函数CDC::DPtoLP()和CDC::LPtoDP()用于设备坐标与逻辑坐标之间的相互转换。
MFC提供了两个函数CWnd::ScreenToClient()和CWnd::ClientToScreen()用于屏幕坐标与客户区坐标的相互转换。
(3)映射模式
映射模式确定了在绘制图形时所依据的坐标系,它定义了逻辑单位的实际大小、坐标增长方向,所有映射模式的坐标原点均在设备输出区域(如客户区或打印区)的左上角。此外,对于某些映射模式,用户还可以自定义窗口的长度和宽度,设置视图区的物理范围。
Windows定义了8种映射模式,见下表。
映射模式使得程序员可不必考虑输出设备的具体设备坐标系,而在一个统一的逻辑坐标系中进行图形的绘制。
映射方法(Mapping Mode) |
逻辑单位 |
坐标轴方向 |
MM_TEXT(默认方式) |
1 pixel |
X轴正方向朝右,Y轴正方向朝下 |
MM_LOMETRIC |
0.1 mm |
X轴正方向朝右,Y轴正方向朝上 |
MM_HIMETRIC |
0.01 mm |
X轴正方向朝右,Y轴正方向朝上 |
MM_LOENGLISH |
0.01 inch |
X轴正方向朝右,Y轴正方向朝上 |
MM_HIENGLISH |
0.001 inch |
X轴正方向朝右,Y轴正方向朝上 |
MM_TWIPS |
1/1440 inch |
X轴正方向朝右,Y轴正方向朝上 |
MM_ISOTROPIC |
自定义(X=Y) |
自定义 |
MM_ANISOTROPIC |
自定义(X!=Y) |
自定义 |
当绘制的图形需要随着窗口的大小改变而自动改变的时候,一般选择MM_ISOTROPIC和MM_ANISOTROPIC映射方式。它们的唯一区别就是前者的X轴和Y轴的逻辑单位的大小是相同的,单词“isotropic”就是各个方向相等的意思,此映射方式适合绘制圆或正方形。而实际应用中,常常给X轴和Y轴取不同的比例,这时候选择MM_ANISOTROPIC映射方式。单词“anisotropic”就是各个方向相异的意思。
(4)自定义映射模式
“窗口”和“视口”的概念:
窗口(Window):对应逻辑坐标系上程序员设定的区域
视口(Viewport):对应实际输出设备上程序员设定的区域
窗口原点是指逻辑窗口坐标系的原点在视口(设备)坐标系中的位置,视口原点是指设备实际输出区域的原点。
除了映射模式,窗口和视口也是决定一个点的逻辑坐标如何转换为设备坐标的一个因素。一个点的逻辑坐标按照如下式子转换为设备坐标:
设备(视口)坐标 = 逻辑坐标 –窗口原点坐标 + 视口原点坐标
//定义坐标映射方式
WINGDIAPI int WINAPI SetMapMode(HDC, int);
此API函数在MFC中封装为CDC::virtual int SetMapMode(int nMapMode);
//定义逻辑窗口区域,单位为逻辑单位(Logical)
WINGDIAPI BOOL WINAPI SetWindowExtEx (HDC, int, int, LPSIZE);
此API函数在MFC中封装为CDC::virtual CSize SetWindowExt(int cx, int cy);
//设置逻辑窗口的原点坐标,缺省原点为(0,0)。
WINGDIAPI BOOL WINAPI SetWindowOrgEx(HDC, int, int, LPPOINT);
此API函数在MFC中封装为CDC::CPoint SetWindowOrg(int x, int y);
注意:SetWindowOrg(Ex) 只有在映射模式为MM_ANISOTROPIC或MM_ISOTROPIC时才有意义。
//定义视口的坐标轴方向及区域、定义域和值域,单位为像素(Pixel)
WINGDIAPI BOOL WINAPI SetViewportExtEx(HDC, int, int, LPSIZE);
此API函数在MFC中封装为CDC::virtual CSize SetViewportExt(int cx, int cy);
注意:SetViewportExt(Ex) 只有在映射模式为MM_ANISOTROPIC或MM_ISOTROPIC时才有意义。
//设置视口的原点坐标,缺省原点为(0,0)。
WINGDIAPI BOOL WINAPI SetViewportOrgEx(HDC, int, int, LPPOINT);
此API函数在MFC中封装为CDC:: virtual CPoint SetViewportOrg(int x, int y);
参考:《GDI中的坐标映射问题》http://dev.csdn.net/article/12/12013.shtm
3、创建绘图工具并选入DC
有了画布,要绘图我们必须有画笔画刷。在Windows中有HPEN、HBRUSH等GDI对象,MFC对GDI对象进行了很好的封装,提供了封装GDI对象的类,如CPen、CBrush、CFont、CBitmap和CPalette等,这些类都是GDI对象类CGdiObject的派生类。
一般先创建画笔(刷),然后调用CDC::SelectObject函数将画笔(刷)选入设备环境最为当前绘图工具,绘图完毕恢复设备环境以前的画笔(刷)对象,最后调用CGdiObject::DeleteObject函数删除画笔(刷)对象。
这里需要注意的是,CGdiObject::DeleteObject函数彻底删除底层GDI对象(CPen和CBrush类的基类)。在MFC中,当对象销毁时会调用对象的析构函数自动删除对象,一般不必调用CGdiObject::DeleteObject删除GDI对象,因为如果设备环境还在使用一个GDI对象时,将引起应用程序崩溃或出现难以理解的运行错误。
(1)创建画笔
BOOL CPen::CreatePen( int nPenStyle, int nWidth, COLORREF cfColor );
nPenStyle 指定画笔的风格。其可能取值的列表,请参见CPen构造函数中的nPenStyle参数。
nWidth 指定画笔的宽度。如果这个值为0,则不管是什么映射模式,以设备单位表示的宽度总是一个像素。
crColor 包含画笔的一个RGB颜色,为COLORREF结构。
此外,可通过CDC::SelectStockObject函数来调用系统预定义的库存笔对应的CGdiObject对象。
pOldPen = (Cpen*)pDC->SelectStockObject(BLACK_PEN);
(2)创建画刷
BOOL CBrush::CreateSolidBrush ( COLORREF crColor );
BOOL CBrush::CreateHatchBrush( int nIndex, COLORREF crColor );
参数: nIndex 指定画刷的阴影线风格。可取的值如下:
HS_HORIZONTAL /* ==== */
HS_VERTICAL /* ||||| */
HS_FDIAGONAL /* ///// */
HS_BDIAGONAL /* ///// */
HS_CROSS /* +++++ */
HS_DIAGCROSS /* xxxxx */
返回值:调用成功时返回非零值,否则为0。
此外,可通过CDC::SelectStockObject函数来调用系统预定义的库存画刷对应的CGdiObject对象。
pOldBrush = (CBrush*)pDC->SelectStockObject(BLACK_BRUSH);
(3)将画笔(刷)选入设备环境。
以下为MFC中默认映射方式下的GDI绘图的模块:
//先获取设备环境pDC
CPen *pOldPen,newPen;
CBrush *pOldBrush,newBrush1,newBrush2;
//创建宽度为pixel的白色实线画笔
newPen.CreatePen(PS_SOLID,1,RGB(0,0,0));
//创建红色实线画刷
newBrush1.CreateSolidBrush(RGB(255,0,0));
//创建红色实线度的向下(从右到左)影线的阴影画刷
newBrush2.CreateHatchBrush(HS_BDIAGONAL,RGB(255,0,0));
//将newPen画笔和newBrush1画刷对象选入设备环境
pOldPen = pDC->SelectObject(&newPen);
pOldBrush = pDC->SelectObject(&newBrush1);
//调用DC绘图函数绘图
//……
//绘图完毕,恢复原来画笔、画刷
pDC->SelectObject(pOldPen);
pDC->SelectObject(pOldBrush);
//删除创建的画笔、画刷
// newPen.DeleteObject();
// newBrush1.DeleteObject();
// newBrush2.DeleteObject();
(4)当绘制文本Text时,一般可以通过调用CDC::SetBkColor函数来设置背景颜色,调用CDC::SetTextColor函数来设置文字颜色,调用CDC::SetTextAlign函数设置文本对齐标记。
4、调用DC绘图函数绘图
GDI为提供了绘制基本图形的成员函数,在MFC中这些函数封装在CDC类中。
注意:绘图函数使用的坐标都是逻辑坐标。
常用CDC绘图函数 |
|
函数 |
功能 |
线输出函数 |
|
GetCurrentPosition |
获取笔的当前位置(以逻辑坐标表示) |
MoveTo |
移动当前位置 |
LineTo |
从当前位置到一点画直线,但不包括那个点 |
Arc |
画一段椭圆弧 |
ArcTo |
画一段椭圆弧。除了更新当前位置以外,这个函数与Arc类似 |
PolyPolyline |
画多组相连线段。这个函数不使用也不更新当前位置 |
PolylineTo |
画一条或多条直线,并把当前位置移到最后一条直线的终点 |
PolyBezier |
画一条或多条Bezier样条。不使用也不更新当前位置 |
PolyBezierTo |
画一条或多条Bezier样条,并把当前位置移到最后一条Bezier样条的终点 |
椭圆和多边形函数 |
|
Chord |
绘制椭圆弧(椭圆和一条线段相交围成的闭合图形) |
DrawFocusRect |
绘制用于表示焦点的风格的矩形 |
Ellipse |
绘制椭圆 |
Pie |
绘制饼形图 |
Polygon |
绘制多边形,包含由线段连接的一个或多个点(顶点) |
PolyPolygon |
创建使用当前多边形填充模式的两个或多个多边形,多边形可以相互分开或叠加 |
Polyline |
绘制多边形,包含连接指定点的一组线段 |
Rectangle |
使用当前笔绘制矩形,用当前画刷填充 |
RoundRect |
使用当前笔绘制圆角矩形,用当前画刷填充 |
位图函数 |
|
BitBlt |
从指定设备上下文拷贝位图 |
StretchBlt |
把位图由源矩形和设备移动到目标矩形,必要时拉伸或压缩位图以适合目标矩形的维数 |
GetPixel |
获取指定点像素的RGB颜色值 |
SetPixel |
设置指定点像素为最接近指定色的近似值 |
文本函数 |
|
TextOut |
用当前选取字体在指定位置写字符串 |
ExtTextOut |
用当前选取字体在矩形区域写字符串 |
TabbedTextOut |
在指定位置写字符串,制表符扩展为制表符停止位置数组中指定值 |
DrawText |
在指定矩形内绘制格式化文本 |
------------------------------详情参考MSDN、MFC类库详解--------------------------- |