65.9K
CodeProject 正在变化。 阅读更多。
Home

保存绘图上下文

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2001年3月28日

6分钟阅读

viewsIcon

126493

已选择到设备上下文 (DC) 中的 GDI 对象在调用 DeleteObject 时也无法删除。此类中的这个便捷类使这些潜在的内存泄漏成为过去。

引言

在图形绘制资源(如画笔、画刷等)的维护方面存在一些问题。当您将一个对象选择到设备上下文中时,该对象就变得无法删除。您可能会退出分配它的上下文,并且它的析构函数会被调用,最终会对它调用 DeleteObject,但令人惊讶的是,它实际上并未消失。因为它被选择到了一个设备上下文中,所以有一个标志指示它正在被使用,因此您的 DeleteObject 调用将不起作用。我不知道这在内部是如何工作的具体细节,但我知道它的外部表现。画笔、画刷等只是保留在“GDI 堆”中。

如果您正在使用那些糟糕的 16 位 Windows 上的 MS-DOS 系统的“临时解决方案”,如 Windows 3.1、Windows 3.11、Win98、Win98 或 WinME(如果您认为 Win9x/ME 是 32 位操作系统,那么您要么(a)没有仔细阅读 API 文档,要么(b)相信了微软的消费级文档并将其视为技术文档),那么有一个非常有限的系统范围的池,所有的画笔和画刷都从这个池中分配,最终甚至您的桌面也无法分配它绘制所需的资源。在一个真正的 32 位操作系统(如 NT/2000/XP)上,GDI 堆要大得多,并且是每个进程的,所以最终是 *您的* 程序将无法绘制,但系统的其余部分将正常工作,因为您只能“搬起石头砸自己的脚”。

这是一个经典案例

void CMyView::OnPaint()
   {
    CPaintDC dc(this);
    CFont f;
    f.CreateFont(...); // parameters not shown
    dc.SelectObject(&f);
    dc.TextOut(...); // whatever...
   } // destructors called here...

看起来不错,对吧?错了。看看会发生什么。当上下文退出时,析构函数会被调用。这意味着 DC 将被释放(在 CPaintDC 的情况下,这意味着将调用 ::EndPaint),并且 CFont 的析构函数将被调用,这意味着将调用 ::DeleteObject。(这还有其他含义,例如,如果您执行 CWnd::SetFont 调用,字体必须具有比变量生命周期更长的生命周期;请参阅我关于此主题的文章)。

严格来说,析构函数被调用的顺序并没有明确规定。我查阅了 C++ 标准,虽然所有类型的析构函数执行顺序问题都有规定,但普通堆栈变量(即 `auto` 变量)的析构函数调用顺序似乎是不确定的。实际上,它似乎是按声明的逆序调用的,也就是说,声明在 DC 之后的字体将首先被销毁,然后 DC 才会被销毁。这就有问题了,因为当字体被销毁时,它仍然被选择到字体中。(您可能会怀疑通过在 CPaintDC 之前声明您的字体来解决这个问题。我认为这是一种严重的编程失误。一方面,我不确定先删除 DC 是否能正确设置字体中的值,使其知道它不再处于 DC 中(它可以被选择到多个 DC 中)。我绝不会尝试这样做。

正确的方法是在销毁 DC 之前恢复其状态。通常,这通过在调用 SelectObject 时保存 DC 的内容,然后恢复它们来实现,例如

void CMyView::OnPaint()
   {
    CPaintDC dc(this);
    CFont f;
    f.CreateFont(...); // parameters not shown
    CFont * oldfont = dc.SelectObject(&f);
    dc.TextOut(...); // whatever...
    dc.SelectObject(oldfont);
   } // destructors called here...

现在这就能正常工作了。当字体的析构函数被调用时,它不再处于 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(...); // parameters not shown
    int save = dc.SaveDC();
    dc.SelectObject(&f);
    dc.TextOut(...); // whatever...
    dc.RestoreDC(save);
   } // destructors called here...

实际上,您在 DC 中做了多少更改并不重要;当您调用 RestoreDC 时,DC 将被恢复到 `SaveDC` 之前的任何状态。这意味着在 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(...); // parameters not shown
    { /* save context */
     CSaveDC sdc(dc);
     dc.SelectObject(&f);
     dc.TextOut(...); // whatever...
    } /* save context */
   } // destructors called here...

请注意,您通过使用 CSaveDC 类创建的保存上下文必须位于比选择到 DC 中的对象更小的作用域内。因此,/* 保存上下文 */块确保 CSaveDC 的析构函数在字体的析构函数之前被调用。您所要做的就是将您的字体、画笔、画刷和区域声明在 /* 保存上下文 */ 块之外,您可以保证它们的析构函数将在它们未被选择到活动 DC 的上下文中被调用。

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

void CMyView::OnPaint()
   {
    CPaintDC dc(this);
    CFont f;
    f.CreateFont(12,...); // most parameters not shown
    { /* save context */
     CSaveDC sdc(dc);
     dc.SelectObject(&f);
     drawboxes(dc);
     dc.TextOut(...); // whatever...
   } // destructors called here

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, ...); // most parameters not shown
    { /* save context */
     dc.SelectObject(&RedPen);
     dc.SelectObject(&GreenBrush);
     dc.SetBkMode(TRANSPARENT);
     dc.SetTextColor(::GetSysColor(COLOR_GRAYTEXT);
     ...
    } /* save context */
   }

请注意,`drawboxes` 中的保存上下文包含许多更改;当您离开保存上下文时,这些更改将被撤消,因此当 `drawboxes` 返回到 `OnPaint` 时,DC 将具有正确的字体(12 像素)、背景模式等。


这些文章中表达的观点是作者的观点,不代表,也不被微软认可。

版权所有 © 1999 <!--webbot bot="Substitution" s-variable="CompanyLongName" startspan -->CompanyLongName <!--webbot bot="Substitution" endspan i-checksum="22147" --> 版权所有。
www.flounder.com/mvp_tips.htm

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.