也許有很多人曾和我一樣, 對Office XP裡面的菜單的陰影效果羨慕不已,它不需要在Windows XP 中就可以在菜單後面顯示陰影, 當然在Windows XP中, 已經完全支持菜單陰影了。雖然我們不一定很有必要自己來實現這個較難實現的效果。但是正如有很多人想實現那種IE風格的菜單欄一樣,盡管它 們並不能為我們帶來更多實用的功能, 卻可以使我們的程序看起來與眾不同。:)
菜單也是一個窗口, 假如我們能得到它的窗口的句柄, 要實現像添加陰影這樣的效果, 就不會很難了。可惜我們根本找不到這個窗口是在哪裡被創建的,也沒辦法很容易地取得它的窗口句柄,甚至幾乎難以相信它是一個窗口,因為我實在找不到它的窗口句柄啊。經過對許多別人已經做好的類的源代碼的"研究", 我終於找到了一個方法。那就是萬能的鉤子,如果說在Windows裡面抓"人",連鉤子也辦不到的話,那我就不知道該用什麼方法實現了,呵呵。
下面我就一起來看看如何抓到這些"可惡"的家伙吧。為了便於移植,我們就寫一個專用的類吧,就取名為CMenuWndHook。添加兩個靜態成員先:
static CMap m_WndMenuMap; static HHOOK m_hMenuHook;被我們抓到的這些家伙肯定不止一個,我們需要一個映射模板類來保存它們的句柄和對應的CMenuWndHook 類對象的指針。m_hMenuHook則為我們將要創建的鉤子的鉤子句柄。再在CPP文件中初始化它們:
CMap CMenuWndHook::m_WndMenuMap; HHOOK CMenuWndHook::m_hMenuHook = NULL;下面再添加兩個函數來做安裝與卸載hook之用, 它們都是靜態函數:
void CMenuWndHook::InstallHook() { if (m_hMenuHook == NULL) { m_hMenuHook = ::SetWindowsHookEx(WH_CALLWNDPROC, WindowHook, AfxGetApp()->m_hInstance, ::GetCurrentThreadId()); } }Windows之下一般用上面的SetWindowsHookEx API函數來安裝HOOK,它的函數原型如下:
HHOOK SetWindowsHookEx(int idHook, //鉤子的類型,即它處理的消息類型 HOOKPROC lpfn, //子函數的入口地址,當鉤子鉤到任何消息後先調用這個函數。 // (如果dwThreadId參數為0,或是一個由別的進程創建的線程的標識, //lpfn必須指向DLL中的鉤子子程。除此以外,lpfn可以指向當前進 //程的一段鉤子子程代碼) HINSTANCE hMod, //應用程序實例的句柄。標識包含lpfn所指的子程的DLL。 // 如果dwThreadId標識當前進程創建的一個線程, //而且子程代碼位於當前進程,hMod必須為NULL。 //可以很簡單的設定其為本應用程序的實例句柄。 DWORD dwThreadId //與安裝的鉤子子程相關聯的線程的標識符。 //如果為0,鉤子子程與所有的線程關聯,即為全局鉤子。 //但這時,你鉤子只能是放在DLL中。 );
函數成功則返回鉤子子程的句柄,失敗返回NULL。 我們用到的是WH_CALLWNDPROC類型的鉤子,它使你可以監視發送到窗口過程的消息, 系統在消息發送到 接收窗口過程之前會調用你指定的WH_CALLWNDPROC Hook 子程,這樣你就可以等它們自投羅網,然後就可以 對它們為所欲為了。 卸載鉤子就簡單多了,只需要調用UnhookWindowsHookEx即可,當然,我們還需要額外做一點清理工作:
void CMenuWndHook::UnInstallHook() { POSITION pos = m_WndMenuMap.GetStartPosition(); while (pos != NULL) { HWND hwnd; CMenuWndHook *pMenuWndHook; m_WndMenuMap.GetNextAssoc(pos, hwnd, pMenuWndHook); delete pMenuWndHook; pMenuWndHook= NULL; } m_WndMenuMap.RemoveAll(); if (m_hMenuHook != NULL) { ::UnhookWindowsHookEx(m_hMenuHook); } }在介紹如何安裝鉤子時,提到要一個鉤子子程,這個子程必須按下面的格式聲明,否則不能使用:
LRESULT CALLBACK WindowHook(int code, WPARAM wParam, LPARAM lParam); 函數名隨意,同樣把它聲明為靜態函數,下面各位注意了,我們的逮捕行動就是在這個函數中展開的:
LRESULT CALLBACK CMenuWndHook::WindowHook(int code, WPARAM wParam, LPARAM lParam) { //如果你安裝的是WH_CALLWNDPROC類型的鉤子的話,系統就會傳遞一個這個家伙的指針: CWPSTRUCT* pStruct = (CWPSTRUCT*)lParam; while (code == HC_ACTION) { HWND hWnd = pStruct->hwnd; // 截獲 WM_CREATE 消息, 為了保證不抓錯"人",我們必須嚴格確定這是否是我們要抓的家伙, // 這樣我們就可以在它們剛出頭就把它們逮住: if(pStruct->message != WM_CREATE &&pStruct->message != 0x01E2) { break; } // 是否為菜單類 ---------------------------------------- TCHAR strClassName[10]; int Count = ::GetClassName(hWnd, strClassName, sizeof(strClassName) / sizeof(strClassName[0])); // 再次確認它的身份(菜單窗口類的類名為"#32768",且為6個字符長): if (Count != 6 || _tcscmp(strClassName, _T("#32768")) != 0 ) { // 對不起,認錯人了,pass :-) break; } //是否已經被子類化------------------------------------ // 我們抓到一個之後,會給它用SetProp掛個牌(後面會介紹) if(::GetProp(pStruct->hwnd, CoolMenu_oldProc) ! = NULL ) { // 已經在編? pass. break; } // 抓到一個,給它登記注冊(這個函數我會在後面介紹), 而且不能登記失敗, :) VERIFY(AddWndHook(pStruct->hwnd) != NULL); //下面該叫它去洗心革面了----------------- //取得原來的窗口過程 ---------------------------------- WNDPROC oldWndProc = (WNDPROC)(long)::GetWindowLong(pStruct->hwnd, GWL_WNDPROC); if (oldWndProc == NULL) { break; } ASSERT(oldWndProc != CoolMenuProc); //這個過程一樣不能出錯 // 保存到窗口的屬性中 ---------------------------------- // 哈哈,給它打個記號吧 (SetProp API函數是用來給一個窗口加上一個屬性的, // RemoveProp 則是刪除一個屬性,GetProp 是取得一個屬性的值) // CoolMenu_oldProc 為一字符數組, 我在CPP文件的開頭聲明了它,表示你要 // 添加的屬性名: const TCHAR CoolMenu_oldProc[]=_T("CoolMenu_oldProc"); // 這裡保存的是它的原來的窗口過程,這種該隨身帶的東西還是讓它自己拿著比較好 if (!SetProp(pStruct->hwnd,CoolMenu_oldProc, oldWndProc)) { break; } // 子類化---------------------------------------------- // 這個不用我說了吧,這裡我們用了偷梁換柱的方法,呵呵,這可是子類化的慣技了: if (!SetWindowLong(pStruct->hwnd, GWL_WNDPROC,(DWORD)(ULONG)CoolMenuProc) ) { //沒有成功!!唉,就放過他吧,雖然忙了半天了,不過這種情況我想是不可能發生的! ::RemoveProp(pStruct->hwnd, CoolMenu_oldProc); break; } } // 這句可是絕對不能少的,叫那些閒雜人等該干什麼就干什麼去,不要? // 嘿嘿,看你的程序怎麼死吧! return CallNextHookEx(m_hMenuHook, code, wParam, lParam); }我們再來看看,怎麼"登記"它們:
CMenuWndHook* CMenuWndHook::AddWndHook(HWND hwnd) { CMenuWndHook* pWnd = NULL; if (m_WndMenuMap.Lookup(hwnd, pWnd)) { // 有這個人了,不用再登記了。 return pWnd; } // 給它分配個房間(牢房! 嘿嘿) pWnd = new CMenuWndHook(hwnd); if (pWnd != NULL) { m_WndMenuMap.SetAt(hwnd, pWnd); } return pWnd; } // 另外還可有一個對應的查找函數: CMenuWndHook* CMenuWndHook::GetWndHook(HWND hwnd) { CMenuWndHook* pWnd = NULL; if (m_WndMenuMap.Lookup(hwnd, pWnd)) { return pWnd; } return NULL; }上面的函數和變量大部分都是靜態成員,因為hook系統只要有一套就可以了到 這裡為止,堅巨的任務已經完成了一半,做下面的事,就得心應手多了。下面是窗口的新過程,依然為一個靜態的函數。
LRESULT CALLBACK CMenuWndHook::CoolMenuProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldWndProc = (WNDPROC)::GetProp(hWnd, CoolMenu_oldProc); CMenuWndHook* pWnd = NULL; switch (uMsg) { // 計算非客戶區的大小-------------------------- case WM_NCCALCSIZE: { LRESULT lResult = CallWindowProc(oldWndProc, hWnd, uMsg, wParam, lParam); if ((pWnd = GetWndHook(hWnd)) != NULL) { pWnd->OnNcCalcsize((NCCALCSIZE_PARAMS*)lParam); } return lResult; } break; // 當窗口的位置將要發生改變, 在這裡它一般發生在菜單被彈出之前, // 給你最後一次機會設置它的位置. case WM_WINDOWPOSCHANGING: { if ((pWnd = GetWndHook(hWnd)) != NULL) { pWnd->OnWindowPosChanging((LPWINDOWPOS)lParam); } } break; // 為什麼要響應這個消息呢? 我也不知道啊,我只知道,當菜單是以動畫的方式彈出的時候 // 系統是通過發送這個消息來繪制菜單的,wParam是對應的設備上下文句柄,不過我也不知 // 道它到底是屬於誰的. case WM_PRINT: { LRESULT lResult = CallWindowProc(oldWndProc, hWnd, uMsg, wParam, lParam); if ((pWnd = GetWndHook(hWnd)) != NULL) { pWnd->OnPrint(CDC::FromHandle((HDC)wParam)); } return lResult; } break; //這個就不同說了吧. case WM_NCPAINT: { if ((pWnd = GetWndHook(hWnd)) != NULL) { pWnd->OnNcPaint(); return 0; } } break; // 菜單窗口被隱藏的時候,我也不知道這種情況會不會發生, :(, 主要是看到人家這樣處理了. case WM_SHOWWINDOW: { if ((pWnd = GetWndHook(hWnd)) != NULL) { pWnd->OnShowWindow(wParam != NULL); } } break; // 菜單窗口被銷毀的時候 case WM_NCDESTROY: { if ((pWnd = GetWndHook(hWnd)) != NULL) { pWnd->OnNcDestroy(); } } break; } return CallWindowProc(oldWndProc, hWnd, uMsg, wParam, lParam); }下面就看如何慢慢實現這些消息的響應函數吧:
void CMenuWndHook::OnWindowPosChanging(WINDOWPOS *pWindowPos) { if (!IsShadowEnabled()) { //加一塊區域來顯示陰影------- pWindowPos->cx += 4; pWindowPos->cy += 4; } // 為了繪制陰影,我們須要先保存這個區域的圖像,以便繪制半透明的陰影. if (!IsWindowVisible(m_hWnd) && !IsShadowEnabled()) { if (m_bmpBack.m_hObject != NULL) { m_bmpBack.DeleteObject(); } m_bmpBack.Attach(GetScreenBitmap(CRect(pWindowPos->x, pWindowPos->y, pWindowPos->cx, pWindowPos->cy))); } } void CMenuWndHook::OnNcCalcsize(NCCALCSIZE_PARAMS* lpncsp) { if (!IsShadowEnabled()) { //留出一點區域來顯示陰影------- lpncsp->rgrc[0].right -= 4; lpncsp->rgrc[0].bottom -= 4; } }上面我用到了兩個全局函數, 其中IsShadowEnabled是檢測系統是否開啟了菜單陰影(主要針對於Windows XP, Windows 2003及他更高的版本) 如果系統已經給我們開啟了陰影,我們還忙乎什麼哦。
BOOL WINAPI IsShadowEnabled() { BOOL bEnabled = FALSE; if (SystemParametersInfo(SPI_GETDROPSHADOW, 0, bEnabled,0)) { return bEnabled; } return FALSE; }其中 SPI_GETDROPSHADOW 在VC6裡面沒有被聲明,你需要自已聲明它:
本文示例代碼或素材下載