← Back to list

是誰動了我的HDC

視窗OnDestroy()的時候,沒能把它的HDC一併歸還,這當然是我們軟體中的程式碼錯誤,也是我們遇到的問題的直接原因。但故障現象的根本原因是什麼呢?

Superliufa · 2025-08-06 03:50 · 0 claps · 3.9 min read
#gdi #windows #software-development #中文 #編程
Open on Medium ↗

圖片由Google Gemini生成

圖片由Google Gemini生成

是誰動了我的HDC

最近工作上倒是查了不少問題,然而到了最後都發現只是一些低級錯誤,完全不好意思拿出來說。但今天遇到一個,還算有些意思,可以講講。

問題的表現是我們的軟體上某些部位偶爾會「花」掉,顯示的是先前覆蓋在上面的內容。有經驗的程式設計師一看就知道是GDI的問題,但到底是什麼問題呢?

剛開始的時候,以為是GDI洩漏了。曾經有過一個例子,漏到了好幾千,後來到了1萬,觸發了CEF的CollectGDIUsageAndDie被Dump了。還沒到1萬的時候,介面上的表現就是開始花,其實就是有部分GDI函數已經開始呼叫失敗了。

然而這次並不是,GDI數很正常。開發人員遠端調試跟了一下,發現是HDC拿不到。 CreateCompatibleDC()得到了一個NULL,因此MemDC創建不出來。

什麼情況下CreateCompatibleDC()會呼叫失敗呢? GDI數不多,記憶體也很充足。軟體中其它部位的MemDC是能正常創建的,因此全局性的因素都可以排除。什麼光柵設備、顯示卡驅動之類也就不可能是原因了。 除此之外,就只剩下了一個可能性:CreateCompatibleDC()傳進去的HDC參數有問題。

試了一下,亂傳一個不存在HDC,的確會導致CreateCompatibleDC()回傳NULL。如果傳的是個NULL進去,倒是還好一點。看來是視窗上的HDC有問題,當然也有可能是視窗本身就有問題。然而其它地方也在用同一個HWND創建MemDC,一切正常。所以還是某些HDC有問題。

HDC的值看起來並不奇怪,也沒有什麼好辦法確認它到底有什麼問題。 HWND還能用Spy++來看,HDC我是一籌莫展。還好可以OutputDebugString。日誌打出來,有意思的地方來了。

我看出問題了:當某個HDC失效的時候,它一定是被連續拿到兩次,中間沒有ReleaseDC()過。這兩次GetDC(),都得到了同一個HDC,肯定不正常。 我把this指針和HWND的值也加到了日誌裡面,這下看得更清楚了。兩次GetDC()分別是不同的HWND。而HDC失效之時,就是當它被最終ReleaseDC()的時候。只不過這次ReleaseDC()的HWND是一個舊的視窗,那個視窗此時應該已經被銷毀了。

視窗OnDestroy()的時候,沒能把它的HDC一併歸還,這當然是我們軟體中的程式碼錯誤,也是我們遇到的問題的直接原因。但故障現象的根本原因是什麼呢?我們確實沒有依照規範「一借一還」,至少視窗還「在世」的時候沒有。不過ReleaseDC()不管三七二十一,只要有人拿著某個HDC來釋放,它就答應嗎?都不用看看HWND對不對嗎?

微軟關於ReleaseDC()的API函數說明在這一點上就有點語焉不詳了。大概它沒想到有人會這樣去實踐?的確也沒人問這種問題,完全找不到資料,只好自己做了個實驗:

HDC hDC = ::GetDC(hWnd); int nRet = ::ReleaseDC(hWnd + 0x1000, hDC);

接著再在這個DC上用GDI函數畫東西,確實畫不出來。 nRet也的確是1,依照微軟的說法,回傳值1表示DC被釋放,這倒是沒有騙人。我把hDC也加了個數字,然後再跑一遍,這次nRet變成0了。

所以說,微軟是在ReleaseDC()的時候搞了個「容錯」邏輯?只要HDC對得上,就給釋放,不管HWND對不對得上號?我一開始跟Google Gemini探討這個問題的時候,它還不相信,直到我告訴它測試結果。

回過頭來看,為什麼兩次GetDC()能得到同一個HDC呢? 我們的主視窗,經歷了銷毀後重建的過程。在OnDestroy()的時候,沒有及時執行ReleaseDC()。但Windows可能認為,視窗不在了,DC也就沒了。於是另一個新建起來的視窗又透過GetDC()拿到了同一個HDC。等到主視窗的C++物件開始析構,呼叫ReleaseDC()的時候,新視窗拿到的HDC就被背刺了。系統的DC應該是放一個池子裡面,所以是有可能被重複使用的。這當然需要「運氣」,也正因為如此,故障現像不是很穩定。

會遇到這種問題的人,應該不多。現在還在用GDI做開發的專案本來也就不多了。我在網路上沒能找到什麼可以參考的訊息,還好勉強算能夠重現,就抓住機會解決了問題。經驗值又+1了。

最後說明一下:我這個實驗是用VS2013在Win10下面做的。不同的OS以及VS版本可能會有不一樣的情況。畢竟是所謂的「未定義」行為。


메타데이터
post_id
052eb8eeab77
slug
是誰動了我的hdc-052eb8eeab77
url
https://medium.com/@superliufa/%E6%98%AF%E8%AA%B0%E5%8B%95%E4%BA%86%E6%88%91%E7%9A%84hdc-052eb8eeab77
canonical_url
https://medium.com/@superliufa/%E6%98%AF%E8%AA%B0%E5%8B%95%E4%BA%86%E6%88%91%E7%9A%84hdc-052eb8eeab77
author_url
https://medium.com/@superliufa
status
ok
fetched_at
2026-06-25 07:00:49