What does the CS_OWNDC class style do? - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20060601-06/?p=31003
Raymond Chen 2006年06月01日
简要
本文讨论了
CS_OWNDC
窗口类样式的影响,它让窗口管理器为窗口创建一个永久的设备上下文(DC),并始终返回同一个DC。这会导致代码中假设每次调用GetDC
会得到不同DC的逻辑出现问题,因为实际上多次调用可能返回相同的DC,从而破坏了依赖于此假设的绘图代码。复制再试一次分享
正文
回想一下,窗口DC(设备上下文)通常只是临时使用。如果你需要在窗口中绘图,你可以调用BeginPaint
,或者在绘制周期之外调用GetDC
,尽管通常应避免在绘制周期之外进行绘制。窗口管理器为窗口生成一个DC并返回它。你使用这个DC,然后将其恢复到原始状态,并用EndPaint
(或ReleaseDC
)将其返回给窗口管理器。在内部,窗口管理器保持一个小的DC缓存,当有人请求窗口DC时,它会从这个缓存中提取,当DC被返回时,它会重新进入缓存。由于窗口DC只是临时使用,通常未处理的DC数量不会超过几个,一个小的缓存就足以满足正常运行系统中DC的需求。
如果你注册一个窗口类并在类样式中包含CS_OWNDC
标志,那么窗口管理器会为窗口创建一个DC,并将其放入DC缓存中,并打上一个特殊标记,意味着“不要从DC缓存中清除这个DC,因为它是这个窗口的CS_OWNDC
”。如果你调用BeginPaint
或GetDC
来获取一个CS_OWNDC
窗口的DC,那么那个DC将总是被找到并返回(因为它被标记为“永不清除”)。这样做的后果有好有坏,还有更坏的。
好的方面是,由于DC是专门为窗口创建的,并且永远不会被清除,所以你不必担心在将其返回到缓存之前“清理DC”。无论你何时为一个CS_OWNDC
窗口调用BeginPaint
或GetDC
,你总是能得到那个特殊的DC回来。实际上,这就是CS_OWNDC
窗口的全部意义所在:你可以创建一个CS_OWNDC
窗口,获取它的DC,按照你喜欢的方式设置它(选择字体、设置颜色等),即使你释放了DC并在以后再次获取它,你将得到同一个DC回来,它将和你离开时一样。
不好的方面是,你正在使用一个本应只临时使用的东西(一个窗口DC),并永久地使用它。早期版本的Windows对DC的数量有非常低的限制(大约八个左右),因此当DC不再需要时,尽快释放它们至关重要。尽管这个限制自那以后已经显著提高,但背后的原理仍然存在:不应轻率地分配DC。你可能已经注意到CS_OWNDC
的实现仍然使用DC缓存;只是这些DC得到了一个特殊的标记,以便DC管理器知道要特别对待它们。这意味着大量的CS_OWNDC
DC最终会“污染”DC缓存,减缓未来调用函数如BeginPaint
和ReleaseDC
的速度,这些函数需要搜索DC缓存。
为什么DC管理器没有被优化以处理大量
CS_OWNDC
DC的情况?首先,正如我已经指出的,原始的DC管理器不需要担心大量DC的情况,因为系统根本就不可能首先创建那么多DC。其次,即使DC的数量限制提高后,重写DC管理器以优化CS_OWNDC
DC的处理也没有太多意义,因为程序员已经被告知要节制使用CS_OWNDC
。这是软件工程的一个实际问题:你能做的只有这么多。你决定做的每件事都是以牺牲其他事情为代价的。很难证明优化程序员被告知要避免并且事实上已经在避免的场景是合理的。你不会为某人滥用你的系统的情况优化。这就像花时间设计汽车发动机,以便在汽车没有油的情况下保持良好的燃油效率。
更糟糕的是,大多数窗口框架库和几乎所有示例代码都假定你的窗口不是CS_OWNDC
窗口。考虑以下代码,它使用两种字体绘制文本,使用第一种字体引导第二种字体中字符的位置。它看起来完全没问题,不是吗?
void FunnyDraw(HWND hwnd, HFONT hf1, HFONT hf2) {HDC hdc1 = GetDC(hwnd);HFONT hfPrev1 = SelectFont(hdc1, hf1);UINT taPrev1 = SetTextAlign(hdc1, TA_UPDATECP);MoveToEx(hdc1, 0, 0, NULL);HDC hdc2 = GetDC(hwnd);HFONT hfPrev2 = SelectFont(hdc2, hf2);for (LPTSTR psz = TEXT("Hello"); *psz; psz++) {POINT pt;GetCurrentPositionEx(hdc1, &pt);TextOut(hdc2, pt.x, pt.y + 30, psz, 1);TextOut(hdc1, 0, 0, psz, 1);}SelectFont(hdc1, hfPrev1);SelectFont(hdc2, hfPrev2);SetTextAlign(hdc1, taPrev1);ReleaseDC(hwnd, hdc1);ReleaseDC(hwnd, hdc2);
}
我们为窗口获取了两个DC。在第一个中我们选择了我们的第一个字体;在第二个中,我们选择了第二个。在第一个DC中,我们还设置了文本对齐为TA_UPDATECP
,这意味着传递给TextOut
函数的坐标将被忽略。相反,文本将从“当前位置”开始绘制,并且“当前位置”将更新为字符串的末尾,这样下一次调用TextOut
将从上一次结束的地方继续。 一旦两个DC设置好,我们就逐个字符绘制我们的字符串。我们查询第一个DC以获取当前位置,并在第二个字体中绘制相同x坐标(但稍低一些)的字符,然后我们用第一个字体绘制字符(这也推进了当前位置)。 完成文本绘制循环后,我们作为标准记账的一部分恢复两个DC的状态。
函数的意图是绘制类似这样的东西,其中第一个字体比第二个大。
如果窗口不是CS_OWNDC
,那就是你得到的。你可以通过从我们的临时程序中调用它来尝试它:
HFONT g_hfBig;
BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs) {LOGFONT lf;GetObject(GetStockFont(ANSI_VAR_FONT),sizeof(lf),&lf);lf.lfHeight *= 2;g_hfBig = CreateFontIndirect(&lf);return g_hfBig != NULL;
}
void
OnDestroy(HWND hwnd) {if (g_hfBig) DeleteObject(g_hfBig);PostQuitMessage(0);
}
void
PaintContent(HWND hwnd, PAINTSTRUCT *pps) {FunnyDraw(hwnd, g_hfBig,GetStockFont(ANSI_VAR_FONT));
}
但如果窗口是CS_OWNDC
,那么坏事就发生了。你自己试试,将这一行wc.style = 0;
改为wc.style = CS_OWNDC;
你会得到以下意想不到的输出:
当然,如果你了解CS_OWNDC
的工作原理,这一点也不意外。理解的关键是要记住,当窗口是CS_OWNDC
时,无论调用多少次GetDC
,GetDC
都只是返回同一个DC。现在你只需要记住FunnyDraw
函数,hdc1
和hdc2
实际上是同一件事。
void FunnyDraw(HWND hwnd, HFONT hf1, HFONT hf2) {HDC hdc1 = GetDC(hwnd);HFONT hfPrev1 = SelectFont(hdc1, hf1);UINT taPrev1 = SetTextAlign(hdc1, TA_UPDATECP);MoveToEx(hdc1, 0, 0, NULL);
到目前为止,函数的执行相当正常。
HDC hdc2 = GetDC(hwnd);
由于窗口是一个CS_OWNDC
窗口,返回给hdc2
的DC与hdc1
返回的是同一个。换句话说,hdc1 == hdc2
!现在事情变得有趣了。
HFONT hfPrev2 = SelectFont(hdc2, hf2);
由于hdc1 == hdc2
,这实际上做的是从DC中取消选择字体hf1
,而选择字体hf2
。
for (LPTSTR psz = TEXT("Hello"); *psz; psz++) {POINT pt;GetCurrentPositionEx(hdc1, &pt);TextOut(hdc2, pt.x, pt.y + 30, psz, 1);TextOut(hdc1, 0, 0, psz, 1);}
现在这个循环完全崩溃了。在第一次迭代中,我们从DC获取当前位置,返回(0, 0),因为我们还没有移动它。然后我们在第二个DC中的位置(0, 30)绘制字母“H”。但由于第二个DC与第一个相同,实际上我们是在TA_UPDATECP
模式的DC中调用TextOut
。因此,坐标被忽略,字母“H”被显示出来(在第二种字体中),当前位置被更新为“H”之后。最后,我们绘制第一个DC中的“H”(它与第二个相同)。我们以为我们用第一个字体绘制,但实际上我们用第二个字体绘制。我们以为我们在(0, 0)绘制,但实际上我们在(x, 0)绘制,其中_x_是字母“H”的宽度,因为对TextOut(hdc2, ...)
的调用更新了当前位置。
因此,每次循环,字符串中的下一个字符都会以第二种字体显示两次。
但等等,灾难还没有结束。看看我们的清理代码:
SelectFont(hdc1, hfPrev1);
这将DC中的原始字体恢复。
SelectFont(hdc1, hfPrev2);
这将重新选择第一个字体!我们未能将 DC 恢复到其原始状态,结果把一个 "损坏的 "DC 放进了缓存。
这就是我将 CS_OWNDC 描述为 "更糟糕 "的原因。它采用了曾经有效的代码,并违反了大多数人(通常没有意识到)对 DC 的假设,从而破坏了代码。
你可能以为 CS_OWNDC 很糟糕。下次我将谈谈 CS_CLASSDC ,那更是场灾难。