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

绘图技术

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.44/5 (9投票s)

2000年5月17日

viewsIcon

112425

了解如何有效地绘制对话框

引言

最近,关于绘图技巧的提问数量有所增加。几乎所有这些问题都使用了比实际需要更复杂的代码。本文将介绍我在Windows中进行绘图的方法。由于我比较懒惰,我希望用最少的精力来完成它。多年来,我已经开发了一些有效的技术。

工具或工具*?

我见过的一个最常见的误解是,您必须分配一个绘图对象才能使用它。因此,我看到了如下形式的代码:

CPen * myPen = new CPen;
CPen->Create(...);
CPen * OldPen = dc->SelectObject(myPen);
...
delete myPen;

这是不必要的复杂代码。至少部分混淆在于,SelectObject 方法需要一个 CPen * (或者更普遍地说,一个 "Tool *"),这使得程序员认为他们必须提供一个 CPen * 变量。调用规范中没有任何内容要求对象必须在堆上分配;只要求提供一个指向对象的指针。这可以通过以下方式实现:

CPen myPen(...);
CPen * OldPen = dc->SelectObject(&myPen);
...
dc->SelectObject(OldPen);

这是更简单的代码;它不调用分配器。参数 &myPen 满足了 CPen * 的要求。由于画笔和其他 GDI 工具通常是“即时”创建然后丢弃的,因此没有必要在堆上分配它们。当您离开作用域时调用析构函数,CPen 底层的 HPEN 通常会被销毁——但请参阅下面的情况!

何时需要分配

您唯一需要保留对象的时间是当您需要将它们返回到您自己的上下文之外时。例如,以下代码将 *无法 *正常工作:

HBRUSH MyWnd::OnCtlColor(...)
    {
     CBrush MyBackground(RGB(255, 0, 0));
     return (HBRUSH)MyBackground;
    }

这是无效的,因为在画笔创建的上下文退出时,HBRUSH 被销毁了,所以当调用者最终使用该句柄时,它代表一个无效的 GDI 对象,并且被 GDI 忽略。但以下代码也是错误的:

HBRUSH MyWnd::OnCtlColor(...)
    {
     CBrush * MyBackground = new CBrush(RGB(255, 0, 0));
     return (HBRUSH)*MyBackground; 
     // or return (HBRUSH)MyBackground->m_hObject;
    }

这是错误的,因为每次调用该例程时都会分配一个画笔,并且 *从不删除 *。您不仅会用大量未回收的 CBrush 对象来充斥您的应用程序空间,还会用大量未回收的 HBRUSH 对象来充斥 GDI 空间。这最终会使 Win9x 崩溃,而在 NT 上最终会使您的应用程序崩溃(只是在 NT 上需要更长时间,因为您有更多空间可以填满)。

在这些情况下,您必须为您的类添加一个成员变量,即背景画笔,例如:

CBrush * MyBackground;

在构造函数中初始化它,并在析构函数中删除它:

MyWnd::MyWnd()
   {
    ...
    MyBackground = new CBrush(RGB(255,0,0));
   }

MyWnd::~MyWnd()
   {
    ...
    delete MyBackground;
   }

何时删除无效

当对象被选入活动的 DC 后离开作用域时,析构函数中的隐式删除将不起作用。::DeleteObject 被调用,但由于对象在 DC 中,此操作失败。因此,以下代码会泄漏 GDI 对象,最终所有 GDI 空间都会被填满:

void OnDraw(CDC * pDC)
   {
     CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0));
     ...
     pDC->SelectObject(&RedPen);
     ...
    }

当我们离开作用域时,与 RedPen 关联的 HPEN 仍然被选入 DC。调用了析构函数,但被忽略。HPEN 未被删除。

正确的解决方案是确保在调用析构函数时,没有任何 GDI 对象被选入 DC。通常的方法是保存旧对象。这意味着您必须记住保存它,并记住恢复它,而且您不需要保存任何除了原始对象之外的其他对象,例如:

{
 CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0));
 CPen GreenPen(PS_SOLID, 0, RGB(0, 255, 0));
 CPen BluePen(PS_SOLID, 0, RGB(0, 0, 255)):
 
 CPen * OldPen = dc->SelectObject(&RedPen);
 ...
 dc->SelectObject(&GreenPen);
 ...
 dc->SelectObject(&BluePen);
 ...
 dc->SelectObject(OldPen);
}

请注意,只需要恢复原始画笔。但如果有一个循环怎么办?您需要在第一次时存储原始画笔,或者在循环结束时始终恢复它,以便下一次迭代正确,依此类推。如果您需要更改画笔、画刷、ROP、填充模式等等怎么办?非常繁琐。如果您决定在代码的更早处更改画笔怎么办?我称之为“维护下的健壮性”的妥协会带来相当大的风险。最简单的方法是使用 SaveDC/RestoreDC

{
 CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0));
 CPen GreenPen(PS_SOLID, 0, RGB(0, 255, 0));
 CPen BluePen(PS_SOLID, 0, RGB(0, 0, 255)):
 
 int saved = dc->SaveDC();

 dc->SelectObject(&RedPen);
 ...
 dc->SelectObject(&GreenPen);
 ...
 dc->SelectObject(&BluePen);
 ...
 dc->RestoreDC(saved);
}

请注意,没有理由维护一堆其唯一目的是将 DC 恢复到原始状态的变量,并记住哪些变量,以及如何管理它们,以及许多其他不必要的复杂性。只需执行 RestoreDC。所有 DC 状态都将恢复到执行 SaveDC 时的状态,这意味着 DC 中所有选入的 GDI 对象都将被取消选入。现在,当它们的析构函数被调用时,底层的 GDI 对象将被销毁,因为它们不在任何活动的 DC 中。

SaveDCRestoreDC 会“嵌套”,这意味着您可以调用一个执行某些绘图的函数,它可以进行自己的保存/恢复,或者您可以在函数内联执行此操作,例如:

{
 CPen RedPen(PS_SOLID, 0, RGB(255, 0, 0));
 CPen GreenPen(PS_SOLID, 0, RGB(0, 255, 0));
 CPen BluePen(PS_SOLID, 0, RGB(0, 0, 255)):
 
 int saved = dc->SaveDC();

 dc->SelectObject(&RedPen);
 ...
 dc->SelectObject(&GreenPen);
 int saved2 = dc->SaveDC();
 for(int i = 0; i < something; i++)
    {
     dc->SelectObject(...);
     ...
     dc->SelectObject(...);
    }
 dc->RestoreDC(saved2);
 ...
 dc->SelectObject(&BluePen);
 ...
 dc->RestoreDC(saved);
}

唯一的要求是,您创建的任何 GDI 对象 *必须与 SaveDC 处于相同或更外层的作用域,并且其析构函数不能在 RestoreDC 之后调用。* 我通常通过要求 GDI 对象和保存/恢复变量处于完全相同的作用域来解决此问题。例如,以下代码将 *无法 *正确工作:

 int saved2 = dc->SaveDC();
 for(int i = 0; i < something; i++)
    {
     CBrush br(RGB(i, i, i));
     dc->SelectObject(&br);
     ...
     dc->SelectObject(&GreenPen);
    }
 dc->RestoreDC(saved2);

因为当调用 br 的析构函数时,它仍然被选入 DC。我不能将 br 的创建移到循环之外,因为它依赖于循环变量 i。有两种解决方案:传统的保存旧画刷并显式恢复它,或者在循环内部执行 SaveDC/RestoreDC

 int saved2 = dc->SaveDC();
 for(int i = 0; i < something; i++)
    {
     CBrush br(RGB(i, i, i));
     CBrush * oldBrush = dc->SelectObject(&br);
     ...
     dc->SelectObject(&GreenPen);
     dc->SelectObject(oldBrush);
    }
 dc->RestoreDC(saved2);
 
 int saved2 = dc->SaveDC();
 
 for(int i = 0; i < something; i++)
    {
     int save = dc->SaveDC();
     CBrush br(RGB(i, i, i));
     dc->SelectObject(&br);
     ...
     dc->SelectObject(&GreenPen);
     dc->RestoreDC(save);
    }
 dc->RestoreDC(saved2);

我没有进行任何性能测量,所以我不知道在紧密循环中使用 SaveDC 是否真的重要;证据表明,对于大多数计算机上的大多数绘图例程,这种开销是难以察觉的,但在高性能的内部循环绘图例程中可能会更严重地显现出来。当保存旧值和恢复新值之间只有几行代码时,在内部循环中,我会恢复到旧机制,但如果代码范围超过大约六行,我会使用 SaveDC/RestoreDC,因为它不容易出错。

我在我的 Hook DLL 的 GUI 中包含了一些有趣的代码;它绘制了一个可爱的猫咪图片。您也可以通过阅读这段代码找到一些有趣的想法。


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

请在下方留下您的提问或对本文的评论。
版权所有 © 1999 The Joseph M. Newcomer Co. 保留所有权利。
www.flounder.com/mvp_tips.htm

许可证

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

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

© . All rights reserved.