淺談 CS_OWNDC 標志位的作用,你學會了嗎?
關于設備上下文(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?,F在你所要做的就是查看 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, …) 的調用更新了當前位置。
因此,每次通過循環時,字符串中的下一個字符都會顯示兩次,全部以第二種字體顯示。
但是等等,災難還沒有結束??纯次覀兊那謇泶a:
SelectFont(hdc1, hfPrev1);
這會將原始字體還原到 DC 中。
SelectFont(hdc2, hfPrev2);
這將重新選擇第一個字體!我們未能將 DC 還原到其原始狀態,最終將“損壞”的 DC 放入緩存中。
這就是為什么我將 CS_OWNDC 描述為“更糟”。它采用過去有效的代碼,并通過違反大多數人對 DC 做出的假設(通常沒有意識到)來破壞它。
如果你覺得 CS_OWNDC 很糟糕了,沒事,還有更糟的,下次我會談談被稱為 CS_CLASSDC 的災難。
總結
對于自己不了解的東西,要小心謹慎的嘗試,決不能先入為主。
像一個嬰兒一樣對待所有新生事物,正所謂:一葉障目也。