程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 利用鉤子實現菜單陰影效果

利用鉤子實現菜單陰影效果

編輯:關於VC++

也許有很多人曾和我一樣, 對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裡面沒有被聲明,你需要自已聲明它:

#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()
{
  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]);
}
  在指定的矩形區域內繪制陰影的全局函數(當然這些函數不一定都要做成全局函數,我把它們寫成了全局函數是因為在好幾個類中都用到了它們, 寫成全局函數便於調用) 也許你會覺得這不符合面向對象編程的思想,其實面向過程的編程思想,並不一定就比面向對象的思想落後,我把這些比較獨立的函數寫成全局函數,當作API函數用,還是覺得很方便的,如果硬要將它們塞到一個類裡面,反而覺得很郁悶 。:-). void DrawShadow(CDC *pDC, CRect rect);
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));
    }
  }
}
這麼復雜? 唉! 還不是想讓它把陰影畫得更好看一點, 速度?...在我機子上還過得去。畢竟菜單是不會被頻繁地重畫的. 這樣實現陰影確實有點笨拙,且在意外的時候可能會出現一些不愉快的繪圖上的bug. 但是要實現Windows XP 那樣完美的菜單陰影還是很難的。我希望已經知道的高手,能指點指點! 謝了先。

下面是處理清理工作了: void CMenuWndHook::OnNcDestroy()
{
  delete this; // 錯誤!
}
void CMenuWndHook::OnShowWindow(BOOL bShow)
{
  if (!bShow)
  {
    delete this; // 錯誤2!
  }
}
... ..., 好狠哦! 嘿嘿!

掃尾工作還由是~CMenuWndHook它老人家做, 在delete自己的時候會自動調用它的: CMenuWndHook::~CMenuWndHook()
{
  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();
  }
}
這個類基本上寫完了,如果我還有什麼沒講清的地方,你就再去看看我的源代碼吧。我們可以在APP類裡面調用它: ............
#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的理由之一。呵呵。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved