傳統換膚需重寫每個控件類別 (如 CButtonEx, CEditEx),侵入性強、維護難。本文示範 **全域視窗鉤子 (WH_CALLWNDPROC)** 攔截系統建立標準控件 (Button/Edit/Combo/ListBox 等) 的瞬間,替換其 WndProc 為自訂繪製函數,實現 **零侵入、兩行代碼整合** 的全域換膚,原理同商業庫 SkinMagic/EasySkin。
// 全域鉤子控制碼
HHOOK g_hHook = NULL;
// 啟動鉤子 (App::InitInstance)
BOOL IRStartup(HINSTANCE hMod, DWORD dwThreadId)
{
g_hHook = SetWindowsHookEx(WH_CALLWNDPROC, IRCallWndProc, hMod, dwThreadId);
return TRUE;
}
// 釋放鉤子 (App::ExitInstance)
BOOL IRComplete()
{
UnhookWindowsHookEx(g_hHook);
return TRUE;
}
LRESULT CALLBACK IRCallWndProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode < 0) return CallNextHookEx(g_hHook, nCode, wParam, lParam);
CWPSTRUCT* pcs = (CWPSTRUCT*)lParam;
HWND hWnd = pcs->hwnd;
if (!hWnd) return CallNextHookEx(g_hHook, nCode, wParam, lParam);
// 只處理標準 Button 類別
char szClass[256];
GetClassNameA(hWnd, szClass, 255);
if (strcmp(szClass, "Button") != 0)
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
// 只處理標準按鈕樣式 (BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP = 0x50010000)
CWnd* pWnd = CWnd::FromHandle(hWnd);
DWORD dwStyle = pWnd->GetStyle();
if (dwStyle != 0x50010000)
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
// 取得原始 WndProc
WNDPROC oldProc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC);
// 若已被我們替換過,跳過
if (CButtonExt::s_oldProc && oldProc == CButtonExt::s_oldProc)
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
// 替換為自訂 WndProc
if (oldProc != CButtonExt::DefWindowProc) {
CButtonExt::s_oldProc = oldProc; // 保存原始
SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)CButtonExt::DefWindowProc);
}
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
class CButtonExt
{
public:
static WNDPROC s_oldProc; // 原始 WndProc
static UINT s_nState; // 狀態: NORMAL/HOVER/DOWN
// 統一訊息分發入口
static LRESULT CALLBACK DefWindowProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
CWnd* pWnd = CWnd::FromHandle(hWnd);
switch (msg) {
case WM_PAINT: return OnPaint(pWnd);
case WM_LBUTTONDOWN:return OnLButtonDown(pWnd, wp, lp);
case WM_LBUTTONUP: return OnLButtonUp(pWnd, wp, lp);
case WM_LBUTTONDBLCLK:return OnLButtonDblClk(pWnd, wp, lp);
case WM_MOUSEMOVE: return OnMouseMove(pWnd, wp, lp);
case WM_ERASEBKGND: return OnEraseBkgnd(pWnd, (CDC*)wp);
}
return CallWindowProc(s_oldProc, hWnd, msg, wp, lp);
}
// 狀態機 + 圓角矩形繪製 (OnPaint)
static LRESULT OnPaint(CWnd* pWnd) {
CPaintDC dc(pWnd);
CRect rc; pWnd->GetClientRect(&rc);
CString txt; pWnd->GetWindowText(txt);
// 依狀態選色
COLORREF bg, border, text;
if (s_nState == STATUS_DOWN) { bg=RGB(160,160,160); border=RGB(100,100,100); text=RGB(50,50,250); }
else if (s_nState == STATUS_HOVER) { bg=RGB(60,60,180); border=RGB(0,0,0); text=RGB(250,250,50); }
else { bg=RGB(240,240,240); border=RGB(120,120,120); text=RGB(50,50,50); }
CBrush br(bg); CPen pn(PS_SOLID,1,border);
dc.SelectObject(&br); dc.SelectObject(&pn);
dc.SetBkMode(TRANSPARENT); dc.SetTextColor(text);
CPoint pt(2,2);
dc.RoundRect(&rc, pt); // 圓角矩形
// 區域裁剪 (可選)
HRGN rgn = CreateRoundRectRgn(rc.left,rc.top,rc.right,rc.bottom,4,4);
pWnd->SetWindowRgn(rgn, TRUE);
DeleteObject(rgn);
dc.DrawText(txt, &rc, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
return 0;
}
// 滑鼠進入/離開/按下/釋放 狀態切換 + Invalidate
static LRESULT OnMouseMove(CWnd* pWnd, WPARAM, LPARAM lp) {
CPoint pt(LOWORD(lp), HIWORD(lp));
HRGN rgn = CreateRectRgn(0,0,0,0);
pWnd->GetWindowRgn(rgn);
BOOL in = PtInRegion(rgn, pt.x, pt.y);
DeleteObject(rgn);
if (in) {
if (s_nState == STATUS_DOWN) return 0;
if (s_nState != STATUS_HOVER) { s_nState = STATUS_HOVER; pWnd->Invalidate(); pWnd->UpdateWindow(); pWnd->SetCapture(); }
} else {
if (s_nState == STATUS_HOVER) { s_nState = STATUS_NORMAL; pWnd->Invalidate(); pWnd->UpdateWindow(); ReleaseCapture(); }
}
return 0;
}
static LRESULT OnLButtonDown(CWnd* pWnd, WPARAM, LPARAM) {
s_nState = STATUS_DOWN; pWnd->Invalidate(); pWnd->UpdateWindow(); return 0;
}
static LRESULT OnLButtonUp(CWnd* pWnd, WPARAM, LPARAM) {
if (s_nState != STATUS_NORMAL) {
s_nState = STATUS_NORMAL; pWnd->Invalidate(); pWnd->UpdateWindow();
// 發送 WM_COMMAND 給父視窗模擬點擊
::SendMessage(GetParent(pWnd->m_hWnd), WM_COMMAND,
MAKEWPARAM(pWnd->GetDlgCtrlID(), BN_CLICKED), (LPARAM)pWnd->m_hWnd);
}
return 0;
}
static LRESULT OnEraseBkgnd(CWnd*, CDC*) { return TRUE; } // 防閃爍
};
// 靜態成員定義
WNDPROC CButtonExt::s_oldProc = NULL;
UINT CButtonExt::s_nState = 0;
// MyApp.cpp
BOOL CMyApp::InitInstance()
{
// ... 原有初始化 ...
IRStartup(GetModuleHandle(NULL), GetCurrentThreadId()); // 第 1 行
return TRUE;
}
int CMyApp::ExitInstance()
{
IRComplete(); // 第 2 行
return CWinApp::ExitInstance();
}
CEditExt, CListBoxExt, CComboBoxExt 等,在 IRCallWndProc 中依 GetClassName 分派。CButtonExt 讀取配置實現多主題切換。GetWindowLong/SetWindowLong 改為 GetWindowLongPtr/SetWindowLongPtr。WndProc 可能與其他子類化庫 (如 MFC 內部、BCG、Skin++) 衝突,需測試相容性。WH_CALLWNDPROC 只在訊息**發送前**通知,不阻塞;但大量視窗建立時會增加開銷。完整原始碼含 IRHook.h/cpp, ButtonExt.h/cpp, 測試專案,VC6.0/VS2005+ 編譯通過。