Office XP 的選單陰影效果令人羨慕:無需 Windows XP 即可顯示陰影,而在 XP 系統則由原生支援。雖非核心功能,但能讓程式外觀脫穎而出。本文示範如何透過全域 Hook 攔截選單視窗建立,實現自繪陰影。
選單本質是一個視窗(類別名 #32768),若能取得其 HWND,子類化後即可自繪非客戶區(陰影)。但選單視窗由系統內部建立,應用層無從獲取句柄。解決之道:安裝 WH_CALLWNDPROC Hook,在 WM_CREATE 或特定訊息(0x01E2)時刻攔截、驗證類別名,隨即子類化並註冊管理。
static CMap m_WndMenuMap; // HWND -> CMenuWndHook*
static HHOOK m_hMenuHook; // Hook 句柄
void CMenuWndHook::InstallHook()
{
if (m_hMenuHook == NULL)
{
m_hMenuHook = ::SetWindowsHookEx(WH_CALLWNDPROC,
WindowHook, AfxGetApp()->m_hInstance, ::GetCurrentThreadId());
}
}
void CMenuWndHook::UnInstallHook()
{
POSITION pos = m_WndMenuMap.GetStartPosition();
while (pos)
{
HWND hwnd; CMenuWndHook* p;
m_WndMenuMap.GetNextAssoc(pos, hwnd, p);
delete p;
}
m_WndMenuMap.RemoveAll();
if (m_hMenuHook) ::UnhookWindowsHookEx(m_hMenuHook);
}
LRESULT CALLBACK CMenuWndHook::WindowHook(int code, WPARAM wParam, LPARAM lParam)
{
CWPSTRUCT* pStruct = (CWPSTRUCT*)lParam;
while (code == HC_ACTION)
{
HWND hWnd = pStruct->hwnd;
// 僅關心 WM_CREATE 或 0x01E2
if (pStruct->message != WM_CREATE && pStruct->message != 0x01E2) break;
// 驗證類別名 "#32768"(6 字元)
TCHAR szClass[10];
int len = ::GetClassName(hWnd, szClass, 10);
if (len != 6 || _tcscmp(szClass, _T("#32768")) != 0) break;
// 已子類化則跳過
if (::GetProp(hWnd, CoolMenu_oldProc)) break;
// 註冊管理
VERIFY(AddWndHook(hWnd) != NULL);
// 子類化:偷梁換柱
WNDPROC oldProc = (WNDPROC)::GetWindowLong(hWnd, GWL_WNDPROC);
if (!oldProc || oldProc == CoolMenuProc) break;
if (!::SetProp(hWnd, CoolMenu_oldProc, oldProc)) break;
if (!::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)CoolMenuProc))
{
::RemoveProp(hWnd, CoolMenu_oldProc);
break;
}
break;
}
return CallNextHookEx(m_hMenuHook, code, wParam, lParam);
}
CoolMenuProc 處理 WM_NCCALCSIZE(預留陰影區域)、WM_WINDOWPOSCHANGING(擴大視窗含陰影)、WM_PRINT/WM_NCPAINT(繪製陰影與邊框)、WM_NCDESTROY(清理)等。核心繪製邏輯在 OnPrint 與全域 DrawShadow:
void DrawShadow(CDC* pDC, CRect rect)
{
// 底部、右側、四角漸變陰影,使用 AlphaArray 控制透明度
// 逐像素 GetPixel/SetPixel 實現半透明混合
// 速度可接受(選單重繪不頻繁)
}
BOOL WINAPI IsShadowEnabled()
{
BOOL b = FALSE;
SystemParametersInfo(SPI_GETDROPSHADOW, 0, &b, 0); // VC6 需自行定義 SPI_GETDROPSHADOW 0x1024
return b;
}
若系統已啟用陰影,直接跳過自繪。
#include "MenuWndHook.h"
BOOL CNewmenuApp::InitInstance()
{
// ...
CMenuWndHook::InstallHook();
return TRUE;
}
int CNewmenuApp::ExitInstance()
{
CMenuWndHook::UnInstallHook();
return CWinApp::ExitInstance();
}
搭配自繪選單類,即可打造極具質感的選單介面。

VC 預設控件外觀確實較「老土」,但精美介面終究靠開發者自行打造。Hook 技術雖屬偏門,善用之可突破框架限制。歡迎高手指點優化。