應用程式「換膚」本質上是對視窗控制項進行子類化,攔截繪製訊息實現自訂外觀。本文系統梳理七種主流方案,深入剖析子類化衝突、系統對話框處理、滾動條換膚等實戰難題,並提供自訂 CWndNew 類別解決重複子類化問題。
| 方案 | 核心技術 | 優點 | 缺點 |
|---|---|---|---|
| 1. 直接繼承 | 衍生 CButtonXP : CButton,DDX_Control 或 SubclassDlgItem | 簡單、MFC 原生、穩定 | 需修改每個控制項變數宣告,擴充性差 |
| 2. Hook + 視窗類 | SetWindowsHookEx(WH_CALLWNDPROC) 攔截 WM_CREATE,動態 SubclassWindow | 無需修改原視窗代碼,集中管理 | 易與 MFC 子類化衝突,需處理模組邊界 |
| 3. Hook + 視窗程序 | Hook 中 SetWindowLong(GWL_WNDPROC) 替換 WndProc | 無重複子類化問題,控制力最強 | 需手寫巨大 Switch-Case,維護困難 |
| 4. 列舉子視窗 | GetWindow(GW_CHILD) + GetWindow(GW_HWNDNEXT) 遍歷子類化 | 無 Hook 開銷 | 需修改原視窗 OnInitDialog,時機把握難 |
| 5. Manifest | 嵌入 XML 資訊清單啟用 ComCtl32 v6 | 零代碼、系統原生風格 | 僅限 XP+,功能受限,無法自訂 |
| 6. Skin++ 庫 | 商業/開源換膚庫 | 功能豐富、支援複雜控制項 | 體積大、學習曲線、授權成本 |
| 7. WindowBlinds | 系統級換膚工具 | 全系統生效 | 非開發者可控,屬使用者端工具 |
MFC DDX_Control 內部呼叫 SubclassWindow,若 Hook 中再次 SubclassWindow 同一 HWND,會觸發 ASSERT 或崩潰。
解決:
new CButtonXP,但無法透過 DDX 管理。SetWindowLong(GWL_WNDPROC) 繞過 MFC 子類化機制(本文採用)。WM_NCDESTROY 清理多層子類化鏈中,必須確保反向順序清理。關鍵原則:
WM_NCDESTROY(會搶先於 MFC)。WindowProc 中處理 WM_NCDESTROY,檢查當前 GWL_WNDPROC 是否為自己,決定是否 Unsubclass 並傳遞訊息。case WM_NCDESTROY:
WNDPROC cur = (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC);
if (cur == CWndNew::StaticWindowProc) {
HWND h = m_hWnd;
UnsubclassWindow(); // 恢復舊 WndProc
return CallWindowProc(m_oldProc, h, uMsg, wParam, lParam);
} else {
LRESULT ret = CallWindowProc(m_oldProc, m_hWnd, uMsg, wParam, lParam);
if (cur == (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC))
UnsubclassWindow();
delete this;
return ret;
}
#32770 為對話框,但主視窗類別名不可預測)。CFileDialog 中 CStatic 背景重繪異常:需在 OnPaint 明確繪製背景或呼叫 CStatic::OnPaint()。#32770、ToolbarWindow32 等)精準攔截,避免全域攔截副作用。內建滾動條非獨立視窗,Spy++ 可見其本質。常見做法:隱藏原滾動條(ModifyStyle(WS_VSCROLL,0)),自繪或使用 CScrollBar 控制項替代。Skin++ 等商業庫可能攔截 WM_NCPAINT 實現無縫換膚。
為解決重複子類化、簡化訊息分發,作者實作 CWndNew:
std::map<HWND, CWndNew*> 維護 HWND 與實例對應。StaticWindowProc 為靜態回呼,查表轉發至虛擬 WindowProc。SubclassWindow/UnsubclassWindow、虛擬 PresubclassWindow/PostunsubclassWindow 鉤子。CWndNew,集中管理、隔離衝突。WM_NCDESTROY 是最後清理良機。完整代碼與討論串請參考文末連結。歡迎交流更優雅的換膚架構設計。