1 设备描述表(Device Context , DC)的概念
Windows应用程序通过为指定硬件设备(屏幕,打印机等)创建一个设备描述表(DC),并在DC表示的逻辑意义的“画布”上进行图形的绘制。简单来说,DC就是为了方便软件开发人员进行绘图操作而创造出来的,可以认为是硬件设备的虚拟化,这样我们只需要通过DC对硬件设备进行操作,而不需要直接对硬件进行编程,大大提高了开发效率。实际上,DC是一种包含设备信息的数据结构,它包含了物理设备所需的各种状态信息。Win32程序在绘制图形之前需要获取DC的句柄HDC,并且需要在不继续使用时释放掉。
2 常用设备描述表类型的基本概念及相互关系
在Win32和MFC 编程中常会见到HDC,CDC,CClientDC,CPaintDC,CWindowDC这样的设备描述表类型,如果不搞清楚它们之间区别和使用方法,就会出现各种各样的问题。下面详细描述一下它们的基本概念及相互之间的关系。
2.1 HDC类型
HDC是DC的句柄,Win32 API中的一个类似指针的数据类型。
2.2 CDC类型
CDC是MFC的DC的一个类。CDC类及其派生类都含有一个类的成员变量:m_nHdc,即HDC类型的句柄。继承于CDC的派生类有CClientDC,CPaintDC,CWindowDC 和CMetaFileDC,除CMetaFileDC以外的三个派生类都用于图形绘制。
CDC类定义了一个设备描述表相关的类,其对象提供成员函数操作设备描述表进行工作,如显示器,打印机,以及显示器描述表相关的窗口客户区域。
通过CDC的成员函数可进行一切绘图操作。CDC提供成员函数进行设备描述表的基本操作,使用绘图工具,选择类型安全的图形设备结构(GDI或GDI+),以及色彩,调色板。除此之外还提供成员函数获取和设置绘图属性,映射,控制视口,窗体范围,转换坐标,区域操作,裁减,划线以及绘制简单图形(椭圆,多边形等)。同时也提供了用于绘制文本,设置字体,打印机换码,滚动,处理元文件的成员函数。
2.3 CPaintDC类型
(1)CPaintDC类封装了BeginPaint和EndPaint两个API的调用;
(2)用于响应窗口重绘消息(WM_PAINT)的绘图输出;
(3)CPaintDC在构造函数中调用BeginPaint()取得设备上下文,在析构函数中调用EndPaint()释放设备上下文。EndPaint()除了释放设备上下文外,还负责从消息队列中清除WM_PAINT消息。因此,在处理窗口重画时,必须使用CPaintDC,否则WM_PAINT消息无法从消息队列中清除,将引起不断的窗口重画;
(4)CPaintDC也只能用在WM_PAINT消息处理之中。
2.4 CClientDC类型(客户区设备描述表)
处理显示器描述表的相关的窗体客户区域。用于客户区的输出,与特定窗口关联,可以让开发者访问目标窗口中客户区,其构造函数中包含了GetDC,析构函数中包含了ReleaseDC。
2.5 CWindowDC类型(窗体区设备描述表)
(1)处理显示器描述表相关的整个窗体区域,包括框架和控件(子窗体);
(2)可在非客户区绘制图形,而CClientDC,CPaintDC只能在客户区绘制图形;
(3)坐标原点是在屏幕的左上角,CClientDC,CPaintDC下坐标原点是在客户区的左上角;
(4)关联一特定窗口,允许开发者在目标窗口的任何一部分进行绘图,包含边界与标题,这种DC同WM_NCPAINT消息一起发送。
2.6 设备描述表之间的关系
CClientDC和CWindowDC 区别不大,可以说 CWindowDC包含了CClientDC,例如在单文档应用程序中:
(1)客户区指的是白色的可以进行图形绘画的区域,也就是CClientDC类的可操作区域;
(2)窗体区除了上面说的白色绘图区域,还包括菜单栏和工具栏等,也就是CWindowDC类的可操作区域;
CClientDC、CWindowDC 与 CPaintDC 的区别较大:
(1)在DC的获取方面CClientDC和CWindowDC 使用的是并只能是 GetDC 和 ReleaseDC,而CPaintDC 使用的是并只能是 BeginPaint 和 EndPaint;
(2)CPaintDC 只能用于响应 WM_PAINT事件,而CClientDC和CWindowDC 只能用于响应非WM_PAINT事件。
3 常用设备描述表类型的使用方法
3.1 HDC
HDC hdc=::GetDC(m_hWnd); //m_hWnd == this->m_hWnd 即当前窗口句柄
MoveToEx(hdc,m_ptOrigin.x,m_ptOrigin.y,NULL);
LineTo(hdc,point.x,point.y);
::ReleaseDC(m_hWnd,hdc); //必须和GetDC配对
HDC使用过程中,每次画线等操作都比MFC封装的CDC类多了个HDC类型的参数,指定在哪个设备描述表执行操作。可以看到HDC的使用较麻烦,而且如果::GetDC和::ReleaseDC不配对的话,会造成错误。
3.2 CDC类
CDC *pDC=GetDC();
pDC->MoveTo(m_ptOrigin);
pDC->LineTo(point);
ReleaseDC(pDC);
如果需要从CDC类型的设备对象中获取设备句柄HDC,可以使用下面的方法:
CDC *pDC =GetDlgItem(IDC_PICTUREBOX)->GetDC();
HDC hdc = pDC->GetSafeHdc();
这里pDC和hdc 是否都需要手动释放?否,ReleaseDC(pDC)即可,在Release(pDC)的过程中,会自动调用DeleteObject()释放掉与pDC相关联的HDC。
这里顺便解释下GetDC()与GetSafeHdc()的区别:GetSafeHdc检查CDC对象指针是否为空,如果不为空则返回m_hDC成员,如果为空就直接返回NULL,以免产生异常。GetSafeHandle也是同样做法。GetDC是返回窗口客户区DC,与GetSafeHdc是不同功能的函数。
3.3 CClientDC类
CClientDCdc(this);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
3.4 CWindowDC类
CWindowDC dc(this);
CWindowDCdc2(GetDesktopWindow()); // 获得整个桌面的句柄
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
3.5 CPaintDC类
CPaintDC dc(this);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
可以看到 MFC 类的使用相对于Win32API中的句柄(HDC)更加方便,因为它们在构造函数和析构函数中自动调用了相应的函数进行DC的获取和释放。
4 关于 WM_PAINT 事件
系统会在多个不同的时机发送WM_PAINT消息:当第一次创建一个窗口时,当改变窗口的大小时,当把窗口从另一个窗口背后移出时,当最大化或最小化窗口时,等等,这些动作都是由操作系统进行管理的,应用程序只是被动地接收该消息,并在消息处理函数中进行绘制操作;大多数的时候应用也需要能够主动引发窗口中的绘制操作,比如当窗口显示的数据改变的时候,这一般是通过InvalidateRect和InvalidateRgn函数来完成的。InvalidateRect和InvalidateRgn把指定的区域加到窗口的Update Region中,当应用的消息队列没有其他消息时,并且窗口的Update Region不为空时,系统就会自动产生WM_PAINT消息。
系统为什么不在调用Invalidate时发送WM_PAINT消息呢?又为什么非要等应用消息队列为空时才发送WM_PAINT消息呢?这是因为系统把在窗口中的绘制操作当作一种低优先级的操作,于是尽可能地推后做。这样也有利于提高绘制的效率:两个WM_PAINT消息之间通过InvalidateRect和InvaliateRgn使之失效的区域就会被累加起来,然后在一个WM_PAINT消息中一次得到更新,不仅能避免多次重复地更新同一区域,也优化了应用的更新操作。像这种通过InvalidateRect和InvalidateRgn来使窗口区域无效,依赖于系统在合适的时机发送WM_PAINT消息的机制实际上是一种异步工作方式,也就是说,在无效化窗口区域和发送WM_PAINT消息之间是有延迟的;有时候这种延迟并不是我们希望的,这时我们当然可以在无效化窗口区域后利用SendMessage 发送一条WM_PAINT消息来强制立即重画,但不如使用Windows GDI为我们提供的更方便和强大的函数:UpdateWindow和RedrawWindow。UpdateWindow会检查窗口的Update Region,当其不为空时才发送WM_PAINT消息;RedrawWindow则给我们更多的控制:是否重画非客户区和背景,是否总是发送WM_PAINT消息而不管Update Region是否为空等。