Win32 内存 DC 指南






4.98/5 (82投票s)
关于在 Win32 中创建和使用内存设备上下文 (DC) 的指南。
引言
在遥远的计算机时代,我写了一篇名为《Win32 绘图(面向中级开发者)指南》的文章。那篇文章描述了 WM_PAINT
消息的基础知识,以及如何使用 大多数 Win32 设备上下文 (DC) 类型。本文将介绍 内存 DC。本文仍将忽略图元文件 DC。
我以前的文章都是用 WTL 编写的。我仍然倾向于使用 WTL 进行 Win32 开发。然而,提供的演示源代码将是纯 C/C++ Win32 调用和窗口管理。这是为了简化您正在查看的代码,并消除代码依赖。
背景
九年前,我开始了一系列关于 Win32 绘图的教程,并在第一篇文章中指出这是五篇系列文章中的第一篇。直到我重读第一篇文章,我才完全想起这个计划。我想重新讨论这个话题并完成这个系列,但我很快就打消了这个念头,认为时间已经过去太久了,人们都在转向 .NET、DirectX 和 OpenGL 等其他技术。
我仍然惊讶于这些基础教程的访问量和下载量如此之高。然后,我发现自己也在查找其他开发主题的基础信息。我最近用 Java 做了一些带有自定义控件的 GUI 工作。我惊讶地发现这个过程与 Win32 绘图如此相似。(在校对时,我发现自己经常感到惊讶。)
我意识到,在过去的 9 年里,并没有发生太多变化。现在有新的库可以比基本的 Win32 API 更轻松地与多媒体进行交互,但是概念是一样的。此外,像 DirectX 这样的技术可能需要很高的学习曲线才能投入使用并获得框架带来的额外优势,而这些优势对于开发者可能需要的简单 UI 绘图来说可能并不重要,因为开发者可能希望让他们的应用程序在众多相似的程序中脱颖而出。
我决定继续记录 Win32 API 调用(主要是 GDI)的用法、功能和我的经验,但其中蕴藏着巨大的潜力,却缺乏足够的上下文来帮助人们将其应用于他们的程序以创造令人惊叹的事物。
Windows 上存在许多其他用于生成图形的技术,可以创建二维矢量图形和栅格图形:GDI+、DirectDraw 2D、DirectWrite。我希望将来能探讨其中一些技术。现在,让我们继续关注经典的 GDI API。
内存设备上下文
内存 DC 是 Win32 开发中的一个重要概念和工具。大多数其他 DC 类型都直接与系统上的窗口或打印机等设备相关。内存 DC 是一个仅存在于内存中的对象。内存 DC 的一些常见用途包括:存储用于显示组合的中间图像;从文件中加载位图和其他图像类型;创建用于双缓冲和三缓冲绘图的后备缓冲区,以减少动画闪烁。
获取内存设备上下文
只有一种方法可以创建内存设备上下文
HDC CreateCompatibleDC(
__in HDC hdc // Handle to an existing DC
);
创建的内存 DC 将与 `hdc` 参数表示的设备兼容。这意味着可以使用 `BitBlt` 和 `StretchBlt` 等光栅操作将内存 DC 中的图像传输到 `hdc` 所表示的同一设备上。
当您将内存 DC 用于显示器以外的设备时,请注意,该设备必须支持光栅操作才能使其有用。创建调用可能会成功,但尝试使用生成的内存 DC 将会失败。“难道它们不支持光栅操作吗?” 不一定,想想基于笔的绘图仪。图元文件是另一种不适用于内存 DC 的设备。图元文件用于记录 GDI 操作,并且其播放不是栅格化的。无论设备如何,您都可以通过调用 GetDeviceCaps
并请求 RASTERCAPS
的支持标志来确定您正在使用的设备是否支持光栅操作。
如果 `hdc` 为 NULL
,则将创建一个与应用程序当前显示屏幕兼容的内存 DC。创建此类型的内存 DC 时需要注意的一个注意事项是,内存 DC 已连接到创建它的线程。当线程被销毁时,内存 DC 也会被销毁。
当您不再需要内存 DC 时,请使用此函数释放资源
BOOL DeleteDC(
__in HDC hdc // Handle to the DC, in this case, memory DC
);
使内存设备上下文可用
如果您在调用 CreateCompatibleDC
后立即开始使用新的内存 DC,您很可能会失望。默认情况下,内存 DC 初始化为一个单色的 1x1 像素位图。如果您只需要一个可以为黑色或白色的像素,那么您就得到了您想要的。对于其他人来说,下一步是为内存 DC 分配一个缓冲区以供写入。
这是创建内存 DC 缓冲区最简单的方法,也是本文中我将介绍的唯一方法
HBITMAP CreateCompatibleBitmap(
__in HDC hdc, // Handle to the DC
__in int nWidth, // Desired width of the bitmap in pixels
__in int nHeight // Desired height of the bitmap in pixels
);
这将创建一个称为设备相关位图 (DDB) 的对象。DDB 是仅与当前设备(显示器、打印机等)兼容的位图,因此只能选择到与该设备兼容的 DC 中。使用新创建的位图,您可以通过调用 SelectObject
将其与内存 DC 关联起来。
另一种位图是设备无关位图 (DIB)。DIB 提供对位图资源的更高级访问,并且设置和初始化起来要复杂得多。它们是通用的位图对象,与任何设备所需的位格式无关。当您想要直接像素访问位图时,DIB 是最佳选择。由于我希望专注于内存 DC,因此我将把 DIB 主题留到另一篇文章中介绍。
以下是创建带有写入缓冲区的内存 DC 的命令序列
// Create a memory DC and Bitmap.
HDC hDC ... // existing DC from a window or call to BeginPaint
int width = 400;
int height= 300;
HDC hMemDC = ::CreateCompatibleDC(hDC);
HBITMAP hBmp = ::CreateCompatibleBitmap(hDC, width, height);
::SelectObject(hMemDC, hBmp);
避免常见错误
在我们离代码太远(我之前展示了您开始运行所需的内容)之前,我想确保您安全地握住这把新剪刀。请勿在调用 CreateCompatibleBitmap
时使用内存 DC。
...
// You may be tempted to do this; DON'T:
HDC hMemDC = ::CreateCompatibleDC(hDC);
// DON'T DO THIS
// |
// V
HBITMAP hBmp = ::CreateCompatibleBitmap(hMemDC, width, height);
...
// TIP: Try to use the same DC to create
// the Bitmap which you used to create the memory DC.
还记得“默认情况下,内存 DC 初始化为一个单色的 1x1 像素位图”的部分吗?您应该记住,我上面已将其加粗。如果您创建一个与内存 DC 兼容的位图,您将获得一个您请求大小的单色位图。通常,当您在屏幕上看到结果时,很容易识别出此错误。问题是,它可能不仅仅是黑白显示。它可能会使用系统前景色和系统背景色。这使得问题更难识别,尤其是当您在将所有内容 BitBlt
到屏幕之前组合多个位图时。
HBITMAP:每个 DC 只能有一个...
任何类型的 DC 一次只能选择一种 GDI 对象。内存 DC 是独特的,因为它是唯一可以调用 ::SelectObject
来使用 HBITMAP
的 DC 类型。与其他 GDI 对象类型不同,HBITMAP
一次只能选择到一个 DC 中。因此,如果您将同一位图用于多个内存 DC,请务必在将位图选择到 DC 时保存从内存 DC 中推出的原始 HGDIOBJ
。否则,您尝试将位图选择到第二个内存 DC 中将会失败。
// Standard setup for creating memory DC and compatible bitmap buffer.
HDC hMemDC = ::CreateCompatibleDC(hDC);
HBITMAP hBmp = ::CreateCompatibleBitmap(hDC, width, height);
::SelectObject(hMemDC, hBmp);
...
// Another memory DC is required later for something spectacular.
// (Note: The same errors will occur with the following code
// even if you are only attempting something medicore at best.)
HDC hMemDC2 = ::CreateCompatibleDC(hDC);
// Attempt to select the original bitmap that is currently held by hMemDC.
::SelectObject(hMemDC2, hBmp); // <- No Bueno: Call fails, returns NULL.
以下代码展示了如何正确处理多个内存 DC 的单个位图。
// Standard setup, getting pretty good with this part.
HDC hMemDC = ::CreateCompatibleDC(hDC);
HDC hMemDC2= ::CreateCompatibleDC(hDC);
HBITMAP hBmp = ::CreateCompatibleBitmap(hDC, width, height);
// Save off the object that is pushed out by selecting the new bitmap.
HGDIOBJ hOldBmp= ::SelectObject(hMemDC, hBmp);
...
// Select the original bitmap into this DC.
// 1) Free the original memory DCs hold on the bitmap.
::SelectObject(hMemDC, hOldBmp); // <- returns same handle as hBmp.
// 2) The bitmap is available to be selected into any compatible memory DC.
::SelectObject(hMemDC2, hBmp);
有一个可能的快捷方式,可用于释放内存 DC 对位图的控制。此快捷方式仅在您不再需要原始内存 DC 但希望保留位图对象时有效。如果您删除内存 DC,它将释放对位图的控制,无需进一步操作。您不再需要将原始位图恢复到内存 DC 中来释放位图句柄。
// Standard memDC/bitmap setup
...
::DeleteDC(hMemDC);
// hBmp is now available to be selected into a different memory DC if desired.
::SelectObject(hMemDC2, hBmp);
之前的快捷方式实际上是一种应该始终使用的良好习惯。HBITMAP
在被选择到内存 DC 中时无法删除。因此,如果您尝试调用 ::DeleteObject
来删除您的位图,而没有先将原始位图恢复到内存 DC 中,或者删除内存 DC,您将面临 GDI 资源泄露。
// Standard memDC/bitmap setup
...
::DeleteObject(hBmp); // <- Call fails, hBmp is still held by hMemDC.
// A resource leak will occur.
// ( Pfft, Who cares? isn't that why you splurged
// and bought 24 GB of RAM for your PC anyway?)
::DeleteDC(hMemDC);
为了完全避免这种情况,一个好的做法是先释放内存 DC。如果您此时也希望销毁位图,那么在此时释放它们没有任何问题。
// Standard memDC/bitmap setup
...
// Simply swap the order of clean up from the example above.
// Always free the memory DC first, then the bitmap to avoid common resource leaks.
::DeleteDC(hMemDC);
::DeleteObject(hBmp); // No resource leak.
// ( Looks like you will have to find some
// other way to frivolously waste resources.)
有一种情况,您必须接受事实,并按照特定的顺序编写所有代码才能使所有 API 正确工作;您想保留内存 DC 并释放位图。要做到这一点,需要将原始位图选择到内存 DC 中,然后就可以销毁所需的位图。
// Standard memDC/bitmap setup
...
// Kill the bitmap, keep the memory DC
// Make sure you save the original bitmap from the memory DC:
HGDIOBJ hOldBmp= ::SelectObject(hMemDC, hBmp);
// Select the original bitmap back into the memory DC.
// This frees the BITMAP object to be destroyed.
// Note: It does not have to be the original bitmap that gets selected back into hMemDC.
// It only has to be a compatible bitmap different than hBmp.
::SelectObject(hMemDC, hOldBmp);
::DeleteObject(hBmp);
// hMemDC is still valid at this point, and can accept another comaptible bitmap.
// If no other bitmap is selected into the memory DC, you are back to the
// mono-chromatic 1x1 pixel bitmap.
一些有用的函数
在我回顾了之前的文章后,我注意到了一些我尚未提及的非常实用的函数。现在是介绍它们的好时机。这些函数可以与任何类型的 DC 一起使用,它们并非仅限于内存 DC。MSDN 关于 SelectObject
方法的说明是
“应用程序在使用新对象绘制完成后,应始终用原始的默认对象替换新对象”.
大多数时候,您可以忽略这一点而不用担心。但是,如果您正在进行子类化或创建自定义控件,并且正在处理控件的绘制消息(WM_ERASEBKGND
、WM_CTLCOLOR_XXX
、WM_PRINT
等),那么您很可能需要遵循这个建议。遵循这个建议在您需要在多个画笔、钢笔、区域、字体和模式之间进行切换时会非常痛苦。您将面临一个非常复杂的绘制场景。
但是,您的代码看起来会像这样
// Setup paint for first layer.
HGDIOBJ hOldBrush = ::SelectObject(hDC, hBrush);
HGDIOBJ hOldPen = ::SelectObject(hDC, hPen);
HGDIOBJ hOldFont = ::SelectObject(hDC, hFont);
HGDIOBJ hOldMan = ::SelectObject(hDC, hBmp);
// ... Paint a motley display
::SelectObject(hDC, hOldBrush);
::SelectObject(hDC, hOldPen);
::SelectObject(hDC, hOldFont);
::SelectObject(hDC, hOldMan);
将之前的代码段与用于组合的其它 DC、可能无法按您预期方式工作的函数调用以及几百行绘制代码混合在一起,就很难将 DC 恢复到接收时的状态。更不用说每次调用 SelectObject
以使用新 DC 时,您都需要为需要恢复的旧 GDI 对象想一个新的名称。
一对函数,以减轻痛苦(绘制)
// Take a snap-shot of the current state of the DC
int SaveDC(
__in HDC hdc, // Handle to the DC
);
// Restore the DC to a previously saved state
int RestoreDC(
__in HDC hdc, // Handle to the DC
__in int nSavedDC // Saved state claim ticket
);
这对函数类似于堆栈的 Push(SaveDC
)和 Pop(RestoreDC
)行为。实现本身就是一个堆栈。每次调用 SaveDC
时,DC 配置的另一个快照就会保存在堆栈上。SaveDC
将返回一个 int
,该 int
指示该 SaveDC
调用。返回值是一个cookie或上下文 ID。
当您调用 RestoreDC
以返回到之前的配置时,传入相应的上下文 ID,DC 将恢复到您进行原始快照时的状态。您可以多次调用 SaveDC
。您不必调用相同次数的 RestoreDC
。所有保存的快照都将从堆栈中弹出,以达到您传递给 RestoreDC
的指定上下文 ID。
SaveDC 和 RestoreDC 有许多用途
// Parents are leaving you home alone for the weekend
// Take a snap-shot of the house
int houseBeforeMotherLeft = ::SaveDC(hDC);
// Start texting, get the word out "Party at my place"
// Get your cash and friend's older brother to buy "refreshments" for the party
::SelectObject(hDC, hBrush);
::SelectObject(hDC, hPen);
::SelectObject(hDC, hFont);
::SelectObject(hDC, hBmp);
// Get the party started,
...
// And oh yeah, the word spread a little too far, But what a party
...
// Cleanup is a snap, Mother will never know.
::RestoreDC(hDC, houseBeforeMomLeft);
// Parents return
return;
// Note: The previous scenario would be very creepy if you are 40 years old
// and live in your mom's basement, rather than are a highschool teenager.
通过调用 RestoreDC
,只会恢复 DC 的状态。您仍然需要负责清理创建的任何 GDI 对象,否则您的应用程序将出现资源泄露。如果您多次调用 SaveDC
而从未调用 RestoreDC
,则保存的状态将一直保留,直到指定的 DC 被释放。
演示
我创建了一个小型演示应用程序,展示了内存 DC 的多种用法。内存 DC 用于缓存位图,以消除每次调用绘制例程时都会产生的重复绘制代码。(注意:应用程序并未缓存所有可能的图像,以便仍然能够演示后备缓冲区的价值。)该应用程序使用内存 DC 来演示双缓冲绘制过程的实现。我还演示了组合多个位图以创建单个最终图像的过程。
当我开始编写演示应用程序时,我只想展示双缓冲绘制方案的实现。这是内存 DC 的常见用途。但是,完成那个阶段后,我想让闪烁效果更加明显。我可以通过减小屏幕上的窗口大小来消除大部分闪烁。因此,我在绘制过程中添加了一些随机矩形,以及渐变填充和 Alpha 混合的使用。
它做什么?
该应用程序将显示两组独立的随机形状。一边是矩形,另一边是椭圆。一个“擦除器”会从左向右滑动穿过屏幕。当它向右滑动时,右侧区域将减小,可见的随机形状也会减少。左侧会放大,露出更多的随机形状。当擦除器一直滑动到最右边时,只剩下原始的左侧形状组。然后擦除器将再次从左侧开始,左侧擦除器将显示一组新的随机形状。
交互
有一些交互式命令可以更改演示的行为。
- 按下鼠标左键:暂停动画。
- 按住鼠标左键拖动:左右移动擦除器。
- 右键单击:在双缓冲和直接缓冲绘制之间切换。
- 调整大小:将重新生成缓存的资源以匹配屏幕。
裁剪区域
为了绘制显示器的两半,我创建了两个 Win32 区域,一个用于左侧,一个用于右侧。一次,我将每个区域设置为绘制到的 DC 的裁剪区域,然后绘制该区域的所有形状。绘制完两个形状组并设置了两个裁剪区域后,图像看起来会像这样
擦除器
讽刺的是,擦除器是演示的第一个概念。我曾有过在控件上产生闪亮高光的动画的想法。后来,它变成了现在的样子;最重要的是,我通过预生成擦除器的图像蒙版并将其存储在缓存的位图中来优化此演示部分。这让我有机会展示内存 DC 的另一种用途。别担心,由于每一帧都执行了大量的形状绘制,直接绘制方法仍然存在大量的闪烁。
在开发擦除器时,我学到了很多。不幸的是,其中大部分与内存 DC 毫无关系。因此,我必须将这个故事留到另一篇文章中。我想说的是,最终为了达到我想要的效果,解决方案与我的直觉相悖。然后,我发现自己深入研究了 GradientFill
和 AlphaBlend
。我还认为,很难找到清晰解释如何使用这些函数(除了最显而易见的使用方式)的文章或示例。所以我想我可能找到了我的下一个话题。
在屏幕上滑动的擦除器将显示两种颜色,以指示更新显示的方法
- CodeProject 绿色:通过绘制到内存 DC 后备缓冲区实现双缓冲。
- CodeProject 橙色:在显示设备上直接缓冲绘制。
这是擦除器与两组形状混合在一起的近距离示例,展示了演示应用程序的最终外观。
关于代码
正如我在文章前面提到的,该程序使用标准的 C/C++ 访问 Win32 API 调用编写。我将项目包含为一个 VS2008 项目。对于 VS2010,您应该能够导入并转换该项目。对于早期版本的 Visual Studio,如果可能,您真的应该升级。我已经将大部分为演示编写的代码与生成的项目分开了,称为 MemDcUsage.cpp。
我将所有演示代码封装在 article::
命名空间中,以帮助不熟悉 Win32 GDI 的人能够区分我编写的函数和 API 调用。我尝试使用全局命名空间运算符引用所有 Win32 API 调用,例如 ::CreateCompatibleDC(...)
。希望我没有遗漏任何。
...
// Handle Window Size changes.
case WM_SIZE:
case WM_SIZING:
// Flush the back buffer,
// The size of the image that needs to be painted has changed.
article::FlushBackBuffer();
// Allow the default processing to take care of the rest.
return DefWindowProc(hWnd, message, wParam, lParam);
case WM_PAINT:
{
hdc = ::BeginPaint(hWnd, &ps);
article::PaintAnimation(hWnd, hdc);
::EndPaint(hWnd, &ps);
break;
}
...
从演示中,您可能无法直接将任何函数或文件“弹出”到您自己的应用程序中。但是,有很多代码片段可以根据您的应用程序的上下文进行修改以使其有用。我将这些代码块标记为 //!!!
,并附带一个简短的注释,描述您可以从中获取的内容。
//!!! Save DC / Restore DC Sample
int ctx = ::SaveDC(hDC);
// Draw the set of rectangles.
::SelectObject(hBufferDC, g_hPenBlack);
::SelectObject(hBufferDC, ::GetStockObject(WHITE_BRUSH));
// Draw Right Side:
DrawShapes(hBufferDC, hRightRgn, width, height, g_group1, g_isGroup1Active);
// Draw Left Side:
DrawShapes(hBufferDC, hLeftRgn, width, height, g_group2, !g_isGroup1Active);
::RestoreDC(hDC, ctx);
历史
- 2011 年 7 月 21 日:修复了损坏的下载文件。
- 2011 年 7 月 16 日:添加了关于
HBITMAP
和资源管理的被忽略的细节。 - 2011 年 7 月 10 日:首次发布。