关于设备上下文(Device Context, 简称 DC) ,我想到这样一个原则:大多数情况下,窗口 DC 只是作为临时使用。
例如,如果你想在窗口中绘制些什么东西,你可以在 WM_PAINT 消息到来的时候,调用 BeginPaint,或者在其他时间点,调用 GetDC,但我们通常还是建议将绘制工作尽可能地放在 WM_PAINT 消息处理代码中。
当你调用上面说的两个函数后,窗口管理器会产生一个窗口对应的 DC 并返回给你。然后,你可以使用这个 DC 进行绘制,当绘制结束的时候,通过调用 EndPaint 或者 ReleaseDC,我们将 DC 恢复它原本的状态并返回给窗口管理器。
从内部实现的角度来看,窗口管理器保留了一小段 DC 缓存,当人们请求窗口 DC 时,它会读取该缓存,当 DC 返回时,它会返回到缓存中。由于窗口 DC 只是临时使用的,因此未完成使用的 DC 的数量通常不是很多,并且小型缓存足以满足正常运行系统中的 DC 需求。
如果注册窗口类并在类样式中包含 CS_OWNDC 标志,则窗口管理器将为窗口创建一个 DC,并使用特殊标记将其放入 DC 缓存中,该标记表示: “不要从 DC 缓存中清除此 DC,因为它是此窗口的 CS_OWNDC “。如果调用 BeginPaint 或 GetDC 来获取CS_OWNDC窗口的 DC,则始终会找到并返回该 DC(因为它被标记为“从不清除”)。这样做的后果有好有坏。
好的一方面是:由于 DC 是专门为窗口创建的并且永远不会被清除,因此你不必担心在将其返回到缓存之前会被清理掉。每当你调用 BeginPaint 或 GetDC 以获取CS_OWNDC窗口时,你总是会得到那个特殊的 DC。事实上,这就是 CS_OWNDC 窗口的全部意义:你可以创建一个 CS_OWNDC 窗口,获取其 DC,按照你喜欢的方式进行设置(选择字体、设置颜色等),即使你释放 DC 并稍后再次获取它,你也会得到相同的 DC,它将是你离开它的方式。
坏的一方面是:你正在获取本来应该暂时使用的东西(窗口 DC)并永久使用它。早期版本的 Windows 对 DC 的限制非常低(八个左右),因此在不需要 DC 时立即释放它们至关重要。自那时以来,这一限额已大幅提高,但基本原则仍然是:应该小心谨慎的使用 DC 并尽可能早地归还给窗口管理器。你可能已经注意到,CS_OWNDC 的实现仍然使用 DC 缓存,只是这些 DC 有一个特殊的标记,所以 DC 管理器知道要特别对待它们。这意味着大量 CS_OWNDC DC 最终会”污染” DC 缓存,从而减慢未来对需要搜索 DC 缓存的函数(如 BeginPaint 和 ReleaseDC)的调用。
(为什么DC 管理器不优化处理大量 CS_OWNDC DC 的情况?首先,正如我已经指出的,最初的 DC 管理器不必担心大量 DC 的情况,因为系统一开始甚至无法创建那么多 DC。其次,即使在提高了对 DC 数量的限制之后,重写 DC 管理器以优化 CS_OWNDC DC 的处理也没有多大意义,因为程序员已经被告知要谨慎使用 CS_OWNDC 。这是软件工程的实用性之一:你只能做这么多。你决定做的一切都是以牺牲其他东西为代价的。很难证明优化程序员被告知要避免的场景是合理的,而事实上他们已经在避免这种情况。你不会针对有人滥用你的系统的情况进行优化。这就像,花时间设计汽车的发动机,以便在汽车没有机油的情况下保持良好的油耗。)
更糟糕的是,大多数窗口框架库和几乎所有示例代码都假定你的窗口不是 CS_OWNDC 窗口。请考虑以下代码,该代码以两种字体绘制文本,使用第一种字体来指定字符在第二种字体中的位置。它看起来很好,不是吗?
我们得到两个用于窗口的 DC。首先,我们选择第一种字体;在第二个中,我们选择第二个。在第一个 DC 中,我们还将文本对齐方式设置为 TA_UPDATECP 这意味着传递给 TextOut 函数的坐标将被忽略。相反,文本将从“当前位置”开始绘制,“当前位置”将更新到字符串的末尾,以便对 TextOut 的下一次调用将从上一个调用中断的地方继续。
设置两个 DC 后,我们一次绘制一个字符的字符串。我们在第一个 DC 中查询当前位置,并以相同的 x 坐标(但略低)绘制第二种字体中的字符,然后以第一种字体绘制字符(这也推进当前位置)。
文本绘制循环完成后,我们将还原两个 DC 的状态,作为标准绘制流程的一部分。该函数的目的是绘制类似这样的内容,其中第一个字体大于第二个字体。
如果窗口没有设置 CS_OWNDC,则结果就是你想要的了。你可以通过从我们的临时程序中调用它。
但是,如果窗口设置了 CS_OWNDC,那么坏事就会发生。你可以将 wc.style = 0 修改成 wc.style = CS_OWNDC,你就会看到这样的效果:
当然,如果你了解 CS_OWNDC 的工作原理,这根本不出乎意料。理解的关键是:当窗口设置了 CS_OWNDC 时,无论你调用多少次,GetDC 都会返回相同的 DC。现在你所要做的就是查看 FunnyDraw 函数,并记住 hdc1 和 hdc2 实际上是一回事。
到目前为止,函数的执行是很正常的。
HDC hdc2 = GetDC(hwnd);
由于该窗口是 CS_OWNDC 窗口,因此在 hdc2 中返回的 DC 与在 hdc1 中返回的 DC 相同。换句话说,hdc1 == hdc2!现在事情变得令人兴奋了。
HFONT hfPrev2 = SelectFont(hdc2, hf2);
由于 hdc1 == hdc2,这真正做的是从 DC 中取消选择字体 hf1 并选择字体 hf2。
现在这个循环完全崩溃了。在第一次迭代中,我们从 DC 检索当前位置,它返回 (0, 0),因为我们还没有移动它。然后,我们将位置 (0, 30) 处的字母“H”绘制到第二个 DC 中。但由于第二个 DC 与第一个 DC 相同,因此真正发生的是我们将 TextOut 调用到处于 TA_UPDATECP 模式的 DC。因此,坐标被忽略,显示字母“H”(以第二种字体),并将当前位置更新为“H”之后。最后,我们将“H”绘制到第一个 DC(与第二个相同)。我们认为我们用第一种字体绘制它,但实际上我们用第二种字体绘制。我们认为我们在 (0, 0) 处绘制,但实际上我们在 (x, 0) 处绘制,其中 x 是字母“H”的宽度,因为对 TextOut(hdc2, …) 的调用更新了当前位置。
因此,每次通过循环时,字符串中的下一个字符都会显示两次,全部以第二种字体显示。
但是等等,灾难还没有结束。看看我们的清理代码:
SelectFont(hdc1, hfPrev1);
这会将原始字体还原到 DC 中。
SelectFont(hdc2, hfPrev2);
这将重新选择第一个字体!我们未能将 DC 还原到其原始状态,最终将“损坏”的 DC 放入缓存中。
这就是为什么我将 CS_OWNDC 描述为“更糟”。它采用过去有效的代码,并通过违反大多数人对 DC 做出的假设(通常没有意识到)来破坏它。
如果你觉得 CS_OWNDC 很糟糕了,没事,还有更糟的,下次我会谈谈被称为 CS_CLASSDC 的灾难。
总结
对于自己不了解的东西,要小心谨慎的尝试,决不能先入为主。像一个婴儿一样对待所有新生事物,正所谓:一叶障目也。