关于设备上下文(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 的状态,作为标准绘制流程的一部分。
该函数的目的是绘制类似这样的内容,其中第一个字体大于第二个字体。