也許有很多人曾和我一樣, 對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;
下面再添加兩個函數來做安裝與卸載hook之用, 它們都是靜態函數:
HHOOK CMenuWndHook::m_hMenuHook = NULL;void CMenuWndHook::InstallHook()
Windows之下一般用上面的SetWindowsHookEx API函數來安裝HOOK,它的函數原型如下:
{
if (m_hMenuHook == NULL)
{
m_hMenuHook = ::SetWindowsHookEx(WH_CALLWNDPROC,
WindowHook,
AfxGetApp()->m_hInstance,
::GetCurrentThreadId());
}
}
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)
上面的函數和變量大部分都是靜態成員,因為hook系統只要有一套就可以了到 這裡為止,堅巨的任務已經完成了一半,做下面的事,就得心應手多了。下面是窗口的新過程,依然為一個靜態的函數。
{
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;
}
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)
上面我用到了兩個全局函數, 其中IsShadowEnabled是檢測系統是否開啟了菜單陰影(主要針對於Windows XP, Windows 2003及他更高的版本) 如果系統已經給我們開啟了陰影,我們還忙乎什麼哦。
{
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;
}
}BOOL WINAPI IsShadowEnabled()
其中 SPI_GETDROPSHADOW 在VC6裡面沒有被聲明,你需要自已聲明它:
{
BOOL bEnabled = FALSE;
if (SystemParametersInfo(SPI_GETDROPSHADOW, 0, bEnabled,0))
{
return bEnabled;
}
return FALSE;
}
#ifndef SPI_GETDROPSHADOW
#define SPI_GETDROPSHADOW 0x1024
#endif
另外還有 GetScreenBitmap 函數用於截取屏幕上指定區域內的圖像: HBITMAP WINAPI GetScreenBitmap (LPCRECT pRect)
下面這兩個函數要做的事就差不多了:
{
HDC hDC;
HDC hMemDC;
HBITMAP hNewBitmap = NULL;
if ((hDC = ::GetDC(NULL)) != NULL )
{
if ((hMemDC = ::CreateCompatibleDC(hDC)) != NULL)
{
if ((hNewBitmap = ::CreateCompatibleBitmap(hDC,
pRect->right - pRect->left,
pRect->bottom - pRect->top)) != NULL)
{
HBITMAP hOldBitmap = (HBITMAP)::SelectObject(hMemDC, hNewBitmap);
::BitBlt(hMemDC, 0, 0, pRect->right - pRect->left, pRect->bottom - pRect->top,
hDC, pRect->left, pRect->top, SRCCOPY);
::SelectObject(hMemDC, (HGDIOBJ)hOldBitmap);
}
::DeleteDC(hMemDC);
}
::ReleaseDC(NULL, hDC);
}
return hNewBitmap;
}void CMenuWndHook::OnNcPaint()
在指定的矩形區域內繪制陰影的全局函數(當然這些函數不一定都要做成全局函數,我把它們寫成了全局函數是因為在好幾個類中都用到了它們, 寫成全局函數便於調用) 也許你會覺得這不符合面向對象編程的思想,其實面向過程的編程思想,並不一定就比面向對象的思想落後,我把這些比較獨立的函數寫成全局函數,當作API函數用,還是覺得很方便的,如果硬要將它們塞到一個類裡面,反而覺得很郁悶 。:-).
{
CWindowDC dc(CWnd::FromHandle(m_hWnd));
OnPrint(&dc);
}
void CMenuWndHook::OnPrint(CDC *pDC)
{
CRect rc;
GetWindowRect(m_hWnd, &rc);
rc.OffsetRect(-rc.TopLeft());
// 繪制陰影
if (!IsShadowEnabled())
{
CDC cMemDC;
cMemDC.CreateCompatibleDC (pDC);
HGDIOBJ hOldBitmap = ::SelectObject (cMemDC.m_hDC, m_bmpBack);
pDC->BitBlt (0, rc.bottom - 4, rc.Width() - 4, 4, &cMemDC, 0, rc.bottom - 4, SRCCOPY);
pDC->BitBlt (rc.right - 4, 0, 4, rc.Height(), &cMemDC, rc.right - 4, 0, SRCCOPY);
DrawShadow(pDC, rc);
rc.right -= 4;
rc.bottom -= 4;
}
// 繪制邊框
pDC->Draw3dRect(rc, m_crFrame[0], m_crFrame[1]);
rc.DeflateRect (1, 1);
pDC->Draw3dRect(rc, m_crFrame[2], m_crFrame[3]);
}void DrawShadow(CDC *pDC, CRect rect);
這麼復雜? 唉! 還不是想讓它把陰影畫得更好看一點, 速度?...在我機子上還過得去。畢竟菜單是不會被頻繁地重畫的. 這樣實現陰影確實有點笨拙,且在意外的時候可能會出現一些不愉快的繪圖上的bug. 但是要實現Windows XP 那樣完美的菜單陰影還是很難的。我希望已經知道的高手,能指點指點! 謝了先。
void DrawShadow(CDC *pDC, CRect rect)
{
COLORREF oldcolor = RGB(255, 255, 255);
BYTE newValR, newValG, newValB;
BYTE AlphaArray[] = {140, 170, 212, 240};
BYTE AlphaArray2[] = {170, 205, 220, 240, 240, 250, 255};
// 底部的陰影 -----------------------------------------
int i, j;
for (j = 0; j < 4; j++)
{
for (i = 6; i <= rect.right - 5; i++)
{
oldcolor = pDC->GetPixel(i, rect.bottom - (4 - j));
newValR = GetRValue(oldcolor) * AlphaArray[j] / 255;
newValG = GetGValue(oldcolor) * AlphaArray[j] / 255;
newValB = GetBValue(oldcolor) * AlphaArray[j] / 255;
pDC->SetPixel(i, rect.bottom - (4 - j), RGB(newValR, newValG, newValB));
}
}
// 右邊的陰影 -----------------------------------------
for (i = 0; i < 4; i++)
{
for (j = 6; j <= rect.bottom - 5; j++)
{
oldcolor = pDC->GetPixel(rect.right - (4 - i), j);
newValR = GetRValue(oldcolor) * AlphaArray[i] / 255;
newValG = GetGValue(oldcolor) * AlphaArray[i] / 255;
newValB = GetBValue(oldcolor) * AlphaArray[i] / 255;
pDC->SetPixel(rect.right - (4 - i), j, RGB(newValR, newValG, newValB));
}
}
// 角上的陰影 --------------------------------------
for (i = 0; i < 4; i++)
{
for (j = 0; j < 4; j++)
{
if ((i + j) > 6) break;
oldcolor = pDC->GetPixel(rect.right - 4 + i, rect.bottom - 4 + j);
newValR = GetRValue(oldcolor) * AlphaArray2[i + j] / 255;
newValG = GetGValue(oldcolor) * AlphaArray2[i + j] / 255;
newValB = GetBValue(oldcolor) * AlphaArray2[i + j] / 255;
pDC->SetPixel(rect.right - 4 + i,
rect.bottom - 4 + j,
RGB(newValR,
newValG,
newValB));
oldcolor = pDC->GetPixel(rect.right - 4 + i, rect.top + 5 - j);
newValR = GetRValue(oldcolor) * AlphaArray2[i + j] / 255;
newValG = GetGValue(oldcolor) * AlphaArray2[i + j] / 255;
newValB = GetBValue(oldcolor) * AlphaArray2[i + j] / 255;
pDC->SetPixel(rect.right - 4 + i,
rect.top + 5 - j,
RGB(newValR,
newValG,
newValB));
oldcolor = pDC->GetPixel(rect.left - i + 5, rect.bottom - 4 + j);
newValR = GetRValue(oldcolor) * AlphaArray2[i + j] / 255;
newValG = GetGValue(oldcolor) * AlphaArray2[i + j] / 255;
newValB = GetBValue(oldcolor) * AlphaArray2[i + j] / 255;
pDC->SetPixel(rect.left - i + 5,
rect.bottom - 4 + j,
RGB(newValR,
newValG,
newValB));
}
}
}
下面是處理清理工作了: void CMenuWndHook::OnNcDestroy()
... ..., 好狠哦! 嘿嘿!
{
delete this; // 錯誤!
}
void CMenuWndHook::OnShowWindow(BOOL bShow)
{
if (!bShow)
{
delete this; // 錯誤2!
}
}
掃尾工作還由是~CMenuWndHook它老人家做, 在delete自己的時候會自動調用它的: CMenuWndHook::~CMenuWndHook()
這個類基本上寫完了,如果我還有什麼沒講清的地方,你就再去看看我的源代碼吧。我們可以在APP類裡面調用它:
{
WNDPROC oldWndProc = (WNDPROC)::GetProp(m_hWnd, CoolMenu_oldProc);
if (oldWndProc != NULL)
{
::SetWindowLong(m_hWnd, GWL_WNDPROC,(DWORD)(ULONG)oldWndProc);
::RemoveProp(m_hWnd, CoolMenu_oldProc);
}
m_WndMenuMap.RemoveKey(m_hWnd);
if (m_bmpBack.m_hObject != NULL)
{
m_bmpBack.DeleteObject();
}
}............
使用這個類,再加上一個自繪菜單類,你一定可以做出一個非常的精美的菜單來。看看我做的最後成品的截圖:
#include "MenuWndHook.h"
...........
BOOL CNewmenuApp::InitInstance()
{
.......
CMenuWndHook::InstallHook();
}
int CNewmenuApp::ExitInstance()
{
CMenuWndHook::UnInstallHook();
return CWinApp::ExitInstance();
}
我時常聽見人說 Delhpi 程序界面比VC程序的界面如何如何好? 如果是說默認的那些控件的外觀,VC確實不如Delphi,(微軟也真小氣,自已產品的界面做得那麼"華麗"(像Office XP/2003, Windows XP,VS.NET...), 而給我們用的這些控件的外觀卻這麼"老土")...總之真正的精美的有個性的界面是大家自已做出來的,這正是我鐘愛VC的理由之一。呵呵。