GDI编程
一、GDI相关概念
1、GDI(Graphics Device Interface):图形设备接口,是一个应用程序与输出设备之间的中介。它提供了一套函数库,这些函数在不同的输出设备上输出图形和文字。一方面,GDI向应用程序提供一个与设备无关的编程环境,另一方面,它又以设备相关的格式和具体的设备打交道。
2、DC (Device Context):设备描述表(设备上下文),是一种Windows数据结构,包括了如线的宽度和颜色、刷子的样式和颜色、字体、剪裁区域等信息。用于表达显示器、打印机等设备。
DC的主要作用是进行绘图和输出文字,如绘制线条、形状和文本等,具体如dc.MoveTo(),dc.LineTo(),dc.Ellipse(),dc.FillRect(),dc.FillSolidRect(),dc.TextOut()等。
Win32下与HDC相关的函数有:GetDC(), BeginPaint()/EndPaint(),GetWindowDC()等
对应的MFC版本的类有:CDC, CPaintDC, CClientDC, CWindowDC等
3、GDI对象:DC定义了一组GDI对象,包括画笔,画刷,字体,位图,调色板,剪裁区域,路径层(Path)。他们有Win32和MFC两套实现版本,其对应关系如下:
Win32对象 |
MFC类 |
HPEN |
CPen |
HBRUSH |
CBrush |
HFONT |
CFont |
HBITMAP |
CBitmap |
HPALETTE |
CPalette |
HRGN |
CRgn |
4、DC与GDI对象之间的关系:GDI对象是通过DC发生作用的,要使用这些GDI对象,可以使用Win32函数SelectObject来将其选入DC中,如::SelectObject(hdc, hPen);
5、利用DC和GDI对象绘图的完整步骤为:
(1). 获取或者创建一个DC
(2). 获取或者创建一个GDI对象(Pen, Brush等)
(3). 使用dc.SelectObject函数把GDI对象选入DC
(4). 使用DC进行绘图或文字输出
(5). 恢复DC原来的GDI对象并删除刚新创建的GDI对象,如pen.DeleteObject()
(6). 释放或删除设备描述表DC
其中,(1)和(6),(2)和(4)是成对出现的。
二、设备描述表DC
Win32下获取DC的API函数有:
HDC BeginPaint(HWND hwnd, LPPAINTSTRUCT lpPaint):特定用于WM_PAINT消息
HDC GetDC(HWND hWnd):用于获得hWnd参数所指定窗口的客户区域的HDC。
HDC GetWindowDC(HWND hWnd):返回hWnd参数所指定的窗口的HDC,包括非客户区,如标题栏、菜单、滚动条,以及边框等。hWnd为NULL时,获取整个屏幕的HDC。
MFC对上述HDC对象和Win32函数进行了封装,基类为CDC类。CDC类包含了各种Win32 HDC的全部功能。在MFC下,使用CDC的成员函数进行图形绘制和文字输出。
CDC类有两个成员变量:m_hDC,m_hAttribDC,它们都是Windows设备描述表句柄。CDC的成员函数作输出操作时,使用m_Hdc;要获取设备描述表的属性时,使用m_hAttribDC。在创建一个CDC类实例时,缺省的m_hDC等于m_hAttribDC。
CDC在封装Win32函数SelectObject(HDC hdc,HGDIOBJECT hgdiobject)时,采用了重载技术,即它针对不同的GDI对象,提供了如下名同而参数不同的成员函数:
SelectObject(CPen *pen) //用于选入笔
SelectObject(CBitmap* pBitmap) //用于选入位图
SelectObject(CRgn *pRgn) //用于选入剪裁区域
SelectObject(CBrush *pBrush) //用于选入刷子
SelectObject(CFont *pFont) //用于选入字体
SelectPalette(CPalette *pPalette,BOOL bForceBackground ) //选入调色板到DC
RealizePalletter() //实现逻辑调色板到物理调色板的映射
直接使用CDC的例子是内存设备上下文,例如:
CDC dcMem.CreateCompatibleDC(&dc); //创建设备描述表
CDC pbmOld = dcMem.SelectObject(&m_bmBall); //更改设备描述表属性
//作一些绘制操作
dcMem.SelectObject(pbmOld); //恢复设备描述表的属性
dcMem.DeleteDC(); //可以不调用,而让析构函数去删除设备描述表
从CDC 派生出四个功能更具体的DC类。继承层次如下图所示:
下面分别讨论这四种设备描述表。
l CCientDC:代表窗口客户区的设备描述表。其构造函数CClientDC(CWnd *pWin)通过::GetDC获取指定窗口的客户区的设备描述表HDC,并且使用成员函数Attach把它和CClientDC对象捆绑在一起;其析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::ReleaseDC释放设备描述表HDC。
l CPaintDC:仅仅用于响应WM_PAINT消息时绘制窗口,因为它的构造函数调用了::BeginPaint获取设备描述表HDC,并且使用成员函数Attach把它和CPaintDC对象捆绑在一起;析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::EndPaint释放设备描述表HDC,而::BeginPaint和::EndPaint仅仅在响应WM_PAINT时使用。
例如,MFC中CView对WM_PAINT消息的实现方法如下:
void CView::OnPaint()
{
CPaintDC dc(this);
OnPrepareDC(&dc);
OnDraw(&dc);
}
l CMetaFileDC:用于生成元文件。
l CWindowDC:代表整个窗口区(包括非客户区)的设备描述表。其构造函数CWindowDC(CWnd *pWin)通过::GetWindowDC获取指定窗口的客户区的设备描述表HDC,并使用Attach把它和CWindowDC对象捆绑在一起;其析构函数使用Detach把设备描述表HDC分离出来,调用::ReleaseDC释放设备描述表HDC。
三、使用DC进行绘图的基本过程
l 获取或者创建设备描述表.DC;
l 必要的话,改变设备描述表的属性(见第四节:GDI对象的介绍);
l 使用设备描述表完成绘制操作;
l 释放或删除设备描述表DC。
第一种绘图方式是对WM_PAINT消息的处理
void CAaView::OnPaint()
{
CPaintDC dc(this); // 得到绘图DC
dc.TextOut(100,100,"Hello World");
}
或者
void CAaView::OnDraw(CDC *pDC)
{
pDC->TextOut(100,100,"Hello World");
}
上面的程序可以在窗口的100,100位置处,打印Hello World字符串。
那么什么时候会产生WM_PAINT消息呢?由于Windows是一个多任务环境,某个应用程序的窗口上面可能被对话框或窗口覆盖,当撤消这些对话框或窗口时,这个应用程序窗口中就有一个"空洞",这个"空洞"就是一块无效的用户区域。为重新显示无效用户区域,Windows发送WM_PAINT消息实现。要求Windows发送WM_PAINT的情况有:改变窗口大小,覆盖用户区的菜单或对话框关闭,使用UpdateWindow和ScrollWindow函数等。
Windows发送WM_PAINT消息时,把它放到应用程序队列的最后,使得其它的输入能够先于WM_PAINT消息被处理。GetMessage函数也得到队列中WM_PAINT消息之后的其它消息,即只有有没有其它消息的情况下,才从队列中取出WM_PAINT消息进行处理。这样做是为了让应用程序首先完成影响窗口显示结果的其它操作,不致因为频繁地执行输出操作而引起显示器的闪烁。Windows把WM_PAINT消息放在队列最后就是这个原因。
Windows并非WM_PAINT消息的唯一来源,使用InvalidateRect或InvalidateRgn函数也可以产生绘图窗口的WM_PAINT消息。这两个函数把用户区全部或部分标记成无效用户区而要求重新显示。下面的函数调用是把整个用户区标记成无效:
InvalidateRect(hWnd, NULL, TRUE);
上面代码把hWnd句柄参数指定的窗口用户区标记成无效。作为矩形结构的NULL参数指定整个用户区,TRUE参数表示擦除背景。
第二种绘图的方式是在非OnDraw / OnPaint中绘图
void CAaView::OnLButtonDown(UINT nFlags, CPoint point)
{
CClientDC dc(this);
dc.Ellipse(point.x-50, point.y-50, point.x+50, point.y+50);
}
这段程序实现了:以鼠标的当前位置为圆心,画一个半径为50的圆。
基本的画线函数有以下几种
CDC::MoveTo( int x, int y ); 改变当前点的位置
CDC::LineTo( int x, int y ); 画一条由当前点到参数指定点的线
CDC::BOOL Arc( LPCRECT lpRect, POINT ptStart, POINT ptEnd ); 画弧线
CDC::BOOL Polyline( LPPOINT lpPoints, int nCount ); 将多条线依次序连接
基本的作图函数有以下几种:
CDC::BOOL Rectangle( LPCRECT lpRect ); 矩形
CDC::RoundRect( LPCRECT lpRect, POINT point ); 圆角矩形
CDC::Draw3dRect( int x, int y, int cx, int cy,
COLORREF clrTopLeft, COLORREF clrBottomRight ); 3D边框
CDC::Chord( LPCRECT lpRect, POINT ptStart, POINT ptEnd ); 扇形
CDC::Ellipse( LPCRECT lpRect ); 椭圆形
CDC::Pie( LPCRECT lpRect, POINT ptStart, POINT ptEnd );
CDC::Polygon( LPPOINT lpPoints, int nCount ); 多边形
对于矩形,圆形或类似的封闭曲线,系统会使用画笔绘制边缘,使用刷子填充内部。如果你不希望填充或是画出边缘,你可以选入空(NULL_PEN)笔或空(NULL_BRUSH)刷子。
多边形和剪贴区域
dc.CreateRectRgn 由矩形创建一个多边形
dc.CreateEllipticRgn 由椭圆创建一个多边形
dc.CreatePolygonRgn 创建一个有多个点围成的多边形
dc.PtInRegion 某点是否在内部
dc.CombineRgn 两个多边形相并
dc.EqualRgn 两个多边形是否相等
基本的绘图函数
CDC类中提供各种各样的输出操作,从画线到写字应有尽有。为了画线、矩形、圆、扇形和写字,可相应地调用一些函数。这些函数使用已选择的画笔和画刷,来画出边框,并填写图形内部区域,以及使用已选择的字体写字。
l 画点函数SetPixel
COLORREF CDC::SetPixel(int x,int y,COLORREF cclrref);
该函数把x和y指定的点置为clrref指定的颜色。
l 画线函数LineTo与移动函数MoveTo
LineTo函数用来画线,并且通常与MoveTo函数配合使用,如画一条从点(10,70)到点(250,100)的线:
dc.MoveTo(10,70);
dc.LineTo(250,100);
l 画矩形函数Rectangle
Rectangle函数用来画矩形。它使用已选择的画笔画出边框,使用已选择的刷子填满矩形内部。下面的例子画一个左上角位于点(10,20),右下角位于点(40,100)的矩形:
dc.Rectangle(10,20,40,100);
l 画圆或椭圆函数Ellipse
Ellipse函数用来画圆或椭圆。它使用已选择的笔画框,使用已选择的刷填满圆或椭圆的内部。下面的例子画一个用点(10,20)和点(40,100)构成矩形框中的椭圆:
dc.Ellipse(10,20,40,100);
l 画圆弧函数Arc
Arc函数用来画一段弧,这段弧由包围它的矩形和弧的开始点和结束点共同定义。下面的例子在点(10,90)和点(360,120)所指定的矩形中画一段弧,它的起点和终点分别是点(15,90)和点(360,90):
dc.Arc(10,90,360,120,15,90,360,110);
弧的起点坐标和终点坐标精确地位于弧上。
l 画扇形函数Pie
Pie函数用来画扇形。扇形由一段弧和两条从弧焦点到弧端点的半径组成。Pie函数使用已选择的笔画框,使已选择的刷填满扇形内部。下面的例子画一个用点(310,30)和点(360,80)构成的矩形围成的扇形。其起点和终点分别为点(360,30)和点(360,80):
dc.Pie(310,30,360,80,310,30,360,80);
弧的起点和终点不必精确地位于弧线上。
四、GDI对象
前面的程序只能画基本的图形,我们不能改变线条的颜色,线条的大小,不能填充颜色,也不能改变字体,显示一张位图等。要实现这些功能,我们就要使用GDI对象。不过,GDI对象是要通过DC才能发生作用的。要使用这些GDI对象,必须使用SelectObject函数将其选入DC中,如::SelectObject(hdc, hPen);
当然,使用之前,这些GDI对象必须存在,可以通过如下Win32函数来创建这些对象:
或者,通过Win32函数HGDIOBJ GetStockObject(int fnObject)来获取系统预先定义好的如下备用对象:
fnObject参数 含义