Office XP 風格選單陰影 (非 XP 系統也能顯示) 令人羨慕。本文揭秘核心技術:**全域鉤子 (WH_CALLWNDPROC) 攔截選單視窗 WM_CREATE 訊息 → 識別類別名 #32768 → 子類化 (SetWindowLong/GWL_WNDPROC) → 處理 WM_NCPAINT/WM_WINDOWPOSCHANGING/WM_PRINT 繪製半透明陰影**。完整類別 CMenuWndHook 實現代碼解析。
class CMenuWndHook
{
static CMap m_WndMenuMap; // HWND -> 實例
static HHOOK m_hMenuHook; // 全域鉤子句柄
HWND m_hWnd; // 當前子類化的選單視窗句柄
CBitmap m_bmpBack; // 陰影區域背景備份
// ... 其他成員 ...
public:
static void InstallHook(); // 安裝鉤子 (應用啟動時呼叫)
static void UnInstallHook(); // 卸載鉤子 (應用退出時呼叫)
// ... 訊息處理虛函數 ...
};
WindowHook)void CMenuWndHook::InstallHook()
{
if (!m_hMenuHook)
m_hMenuHook = ::SetWindowsHookEx(WH_CALLWNDPROC, WindowHook,
AfxGetApp()->m_hInstance, ::GetCurrentThreadId()); // 執行緒鉤子
}
LRESULT CALLBACK CMenuWndHook::WindowHook(int code, WPARAM wp, LPARAM lp)
{
if (code != HC_ACTION) return CallNextHookEx(m_hMenuHook, code, wp, lp);
CWPSTRUCT* pMsg = (CWPSTRUCT*)lp;
// 只關心 WM_CREATE (視窗建立) 和 0x01E2 (內部訊息,可能為 WM_NCCREATE 等)
if (pMsg->message != WM_CREATE && pMsg->message != 0x01E2)
return CallNextHookEx(m_hMenuHook, code, wp, lp);
HWND hWnd = pMsg->hwnd;
// 識別選單視窗類別名 "#32768" (長度 6)
TCHAR szClass[10];
if (::GetClassName(hWnd, szClass, 10) != 6 || _tcscmp(szClass, _T("#32768")) != 0)
return CallNextHookEx(m_hMenuHook, code, wp, lp);
// 檢查是否已子類化 (用屬性標記)
if (::GetProp(hWnd, _T("CoolMenu_oldProc")))
return CallNextHookEx(m_hMenuHook, code, wp, lp);
// 登記實例
CMenuWndHook* pHook = AddWndHook(hWnd); // 建立實例存入 Map
if (!pHook) return CallNextHookEx(m_hMenuHook, code, wp, lp);
// 偷梁換柱:保存原 WndProc,設定新 WndProc
WNDPROC oldProc = (WNDPROC)::GetWindowLong(hWnd, GWL_WNDPROC);
::SetProp(hWnd, _T("CoolMenu_oldProc"), (HANDLE)oldProc);
::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)CoolMenuProc); // 靜態新視窗程序
return CallNextHookEx(m_hMenuHook, code, wp, lp);
}
CoolMenuProc 分發關鍵訊息LRESULT CALLBACK CMenuWndHook::CoolMenuProc(HWND hWnd, UINT uMsg, WPARAM wp, LPARAM lp)
{
WNDPROC oldProc = (WNDPROC)::GetProp(hWnd, _T("CoolMenu_oldProc"));
CMenuWndHook* pHook = GetWndHook(hWnd); // 從 Map 取得實例
if (!pHook) return CallWindowProc(oldProc, hWnd, uMsg, wp, lp);
switch (uMsg)
{
case WM_NCCALCSIZE:
// 計算非客戶區尺寸:預留陰影寬度 (4px)
if (pHook) pHook->OnNcCalcsize((NCCALCSIZE_PARAMS*)lp);
return CallWindowProc(oldProc, hWnd, uMsg, wp, lp);
case WM_WINDOWPOSCHANGING:
// 視窗位置即將改變 (彈出前):擴大視窗矩形容納陰影、備份背景位圖
if (pHook) pHook->OnWindowPosChanging((WINDOWPOS*)lp);
break;
case WM_PRINT:
// 動畫彈出時系統繪製:繪製陰影
if (pHook) pHook->OnPrint(CDC::FromHandle((HDC)wp));
return CallWindowProc(oldProc, hWnd, uMsg, wp, lp);
case WM_NCPAINT:
// 非客戶區繪製:繪製陰影邊框
if (pHook) { pHook->OnNcPaint(); return 0; }
break;
case WM_SHOWWINDOW:
if (pHook) pHook->OnShowWindow(wp != 0);
break;
case WM_NCDESTROY:
// 視窗銷毀:清理實例、還原 WndProc、移除屬性
if (pHook) pHook->OnNcDestroy();
break;
}
return CallWindowProc(oldProc, hWnd, uMsg, wp, lp);
}
OnWindowPosChanging: 若系統未啟用陰影 (IsShadowEnabled() 檢查 SPI_GETDROPSHADOW),視窗寬高各增 4px,彈出前備份陰影區域螢幕位圖 (GetScreenBitmap) 供半透明混合。OnNcCalcsize: 客戶區右/下各縮 4px,騰出陰影空間。OnNcPaint / OnPrint: 使用 AlphaBlend 或手動像素混合,將備份背景與陰影色 (灰/黑) 按透明度合成繪製。OnNcDestroy: 從 Map 移除實例、delete this、還原 GWL_WNDPROC、RemoveProp。BOOL WINAPI IsShadowEnabled()
{
BOOL bEnabled = FALSE;
// SPI_GETDROPSHADOW 在 VC6 未定義,需自行定義 0x1024
return ::SystemParametersInfo(0x1024, 0, &bEnabled, 0) && bEnabled;
}
CWinApp::InitInstance 中呼叫 CMenuWndHook::InstallHook();ExitInstance 呼叫 UnInstallHook()。即可全局生效,無需修改任何選單代碼。
| 難點 | 解決方案 |
|---|---|
| 找不到選單視窗句柄 | 全域鉤子 WH_CALLWNDPROC 攔截 WM_CREATE,由類別名 #32768 識別 |
| 鉤子回調需在 DLL 中 (全域鉤子) | 使用執行緒鉤子 (GetCurrentThreadId()),回調可在 EXE 中 |
| 子類化後必須完美轉發訊息 | 保存原 WndProc 至視窗屬性 (SetProp),CallWindowProc 轉發 |
動畫彈出繪製 (WM_PRINT) | 攔截 WM_PRINT,DC 為系統傳入,繪製陰影後轉發 |
| 多選單實例管理 | 靜態 CMap 映射 HWND 到實例,WM_NCDESTROY 及時清理 |
這是一個經典的「鉤子 + 子類化」系統級 UI 擴展範例,展示了 Windows 訊息機制的深層操作技巧。