深入理解Windows GDI对象与设备上下文管理

Windows编程中,正确管理GDI对象和设备上下文(DC)是至关重要的。GDI对象,如笔(pen)、刷子(brush)等,一旦被选入DC,就会变得无法删除。即使退出了分配它的上下文,其析构函数被调用,最终调用了DeleteObject函数,但对象实际上并没有被销毁。因为被选入了DC,存在一个标志表示它正在被使用,因此DeleteObject无法工作。

如果正在使用Windows 3.1、Windows 3.11、Win98、WinME等16位Windows系统,这些系统有一个非常有限的系统范围内的池,所有的笔和刷子都是从中分配的。最终,甚至桌面都无法分配到需要的资源来绘制。而在真正的32位操作系统如NT/2000/XP中,GDI堆更大,并且是每个进程独有的,所以最终程序可能无法绘制,但系统的其余部分会正常工作,因为只能伤害自己。

下面是一个经典案例:

void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); dc.SelectObject(&f); dc.TextOut(...); }

看起来不错,对吧?错了。看看发生了什么。当上下文退出时,析构函数被调用。这意味着DC将被释放(对于CPaintDC,这意味着::EndPaint将被调用),CFont的析构函数将被调用,这意味着一个::DeleteObject将被调用。

严格来说,析构函数的调用顺序并没有明确规定。查阅了C++标准,尽管在析构函数的执行顺序上有很多问题被规定,但它们对于自动变量(即普通栈变量)的调用顺序似乎是未指定的。在实践中,它似乎是以声明的相反顺序,即在DC之后声明的字体将首先被销毁,然后DC将被销毁。这会失败,因为当字体被销毁时,它仍然被选入字体中。

正确的做法是在销毁之前恢复DC的状态。通常,这是通过保存DC的内容,然后在需要的时候恢复它们来完成的,例如:

void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); CFont *oldfont = dc.SelectObject(&f); dc.TextOut(...); dc.SelectObject(oldfont); }

现在它将正常工作。当字体的析构函数被调用时,它不再在DC中,它将被删除。

事实上,徒步旅行者遵循的原则“像发现的那样留下大门”,在DC管理中也很重要。当编写一个函数,它接受一个传入的DC,必须确保所做的每一个改变都被恢复。注意,每个修改DC的操作都会返回一个对象(或值),可以用来恢复DC的状态(MFC调用包装某些值,如HFONT值,在一个相应的“包装类”中,如CFont,并返回一个指向该MFC对象的指针)。然而,DC有很多参数,这意味着最终,在一个严肃的绘图程序中,有很多奇怪的变量需要跟踪。

然而,这比这更简单。有一个严重被低估的函数,::SaveDC,它的伙伴,::RestoreDC,作为方法CDC::SaveDC和CDC::RestoreDC可用。使用这些,现在可以改变代码,避免使用任何不必要的变量:

void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); int save = dc.SaveDC(); dc.SelectObject(&f); dc.TextOut(...); dc.RestoreDC(save); }

实际上,无论在DC中做了多少改变;当做RestoreDC调用时,DC被恢复到SaveDC之前的状态。这意味着所有选入它之后的对象,所有的文本颜色、背景颜色、样式等的改变都被抹去了。

使用整数SaveDC有有趣的创造性方式,但不会深入讨论,主要是因为不这样做。相反,将展示一个类,使这一切变得非常容易。这段代码没有下载;它是如此简单,可以复制并粘贴到自己的文件中。

SaveDC.h class CSaveDC { public: CSaveDC(CDC &dc) { sdc = &dc saved = dc.SaveDC(); } CSaveDC(CDC *dc) { sdc = dc; saved = dc->SaveDC(); } virtual ~CSaveDC() { sdc->RestoreDC(saved); } protected: CDC *sdc; int saved; };

就是这样!注意它有两个构造函数,一个如果有一个CDC *,一个如果有一个CDC或CDC &。所要做的就是声明一个虚拟变量。但这里有一个重要的技巧,如下所示。

void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(...); { /* save context */ CSaveDC sdc(dc); dc.SelectObject(&f); dc.TextOut(...); } }

注意,通过使用CSaveDC类创建的保存上下文必须在选入DC的对象之外的作用域内。因此,/* save context */块保证了CSaveDC析构函数在字体的析构函数之前被调用。所要做的就是声明字体、笔、刷子和区域在/* save context */块之外,可以保证它们的析构函数在它们不被选入活动的DC的上下文中被调用。

注意CSaveDC可以嵌套(因为::SaveDC可以嵌套)。嵌套可以是静态的或动态的。例如:

void CMyView::OnPaint() { CPaintDC dc(this); CFont f; f.CreateFont(12,...); { /* save context */ CSaveDC sdc(dc); dc.SelectObject(&f); drawboxes(dc); dc.TextOut(...); } } void CMyView::drawboxes(CDC &dc) { CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0)); CBrush GreenBrush(RGB(0, 255, 0)); CFont f; f.CreateFont(6, ...); { /* save context */ dc.SelectObject(&RedPen); dc.SelectObject(&GreenBrush); dc.SetBkMode(TRANSPARENT); dc.SetTextColor(::GetSysColor(COLOR_GRAYTEXT)); ... } /* save context */ }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485