利用钩子实现菜单阴影效果2

来源:互联网 发布:家居网络布线 编辑:程序博客网 时间:2024/04/29 15:36

我们再来看看,怎么"登记"它们:

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的理由之一。呵呵。

 

补充一点:点击菜单发送的消息顺序如下:

//////////////////////////////////////////////////////////////////////////////
// When mouse click menu bar item , message send sequence
// WINXP
//      visual effect no:
//            WM_NCPAINT  --  WM_ERASEBKGND -- WM_DRAWITEM
//      visual effect fade effect
//       WM_ERASEBKGND -- WM_DRAWITEM -- WM_PRINT
//      scroll effect
//            WM_ERASEBKGND -- WM_DRAWITEM -- WM_PRINT -- WM_NCPAINT -- WM_ERASEBKGND -- WM_DRAWITEM
// WIN2k
//      visual effect no:
//            WM_NCPAINT  --  WM_ERASEBKGND -- WM_DRAWITEM
//      visual effect fade effect
//     WM_PRINT -- WM_ERASEBKGND -- WM_DRAWITEM 
//      scroll effect
//            WM_PRINT -- WM_ERASEBKGND -- WM_DRAWITEM  -- WM_NCPAINT -- WM_ERASEBKGND -- WM_DRAWITEM
// there are three messages before all the case:
//       WM_WINDOWPOSCHANGING -- WM_NCCALCSIZE -- WM_WINDOWPOSCHANGING
///////////////////////////////////////////////////////////////////////////////

其顺序与菜单的过渡效果有关,以xp为例,可以在“显示器\属性\外观\效果”来设定它的过渡效果:

 

原创粉丝点击