|
|
一、前言
首先,这是我第一次写类似的文章,亦是学习Windows编程所选的第一个学习项目, 本文的阅读对象应该是像我一样的刚涉及Windows编程的初学者,文中如有文法不通、叙述不清之处还请前辈们见谅,并请指正。
通过本文,我们能够实现很酷的、像Office XP里面的那种菜单,虽然,在Windows XP系统里面已经提供了一种很通用的菜单阴影效果了,而我们也能通过常规的派生CMenu类并重载MeasureItem()和DrawItem()函数来实现我们通过TrackPopupMenu()显示的自绘制菜单项,但对于给应用程序默认菜单添加阴影和自绘制菜单项,网路上给的例子太复杂 ,且几乎没有系统介绍实现带阴影的自绘制菜单的教学文章(sorry,应该是我没有找到吧!),对于初学之来说要实现这样的功能确实可望而不可及。不过呢,既然MS能够做到这一点,我们这些刚入门的菜鸟假使通过学习前辈的作品自然亦能够自己实现,如果真的做到了,那是不是会有那么一点点的成就感呢?话虽然这么讲,但对于初学者来说,要做到这一点,相信还是有一点难度的,它需要一点系统菜单实现的相关知识,还需要一点Windows GDI绘图的基础知识,希望我这篇文章能给初学者带一个路,展示一下大师们实现完美效果的一种途径,继而根据自己的需要,来绘制自己的完美菜单。
在此列出我学习过的几个关于菜单特效的工程例子:《XPStyle》、《Professional User Interface Suite 21》、《CMenuXP - Office XP风格菜单和控件》、《Owner Drawn Menu with Icons, Titles and Shading》、《利用钩子实现菜单阴影效果》,它们在www.vckbase和www.vccode.com上能够找到,希望初学者能从中学到更多。
本文对附后的工程示例实现进行了全文注释,几乎每一个函数、变量都有相关注释,故工程示例中有不解之处可对照本文阅读。
二、实现方法概述
本项目的实现都是建立在Windows消息机制的基础上的,我将菜单的消息机制传递与处理分离在了两个分离的类中:第一个CMenuEx是实现本项目的基础,它截获并传递了实现菜单效果需要的几个Windows消息。
Class CMenuEx{public: CMenuEx() { } ~CMenuEx() { } CMenuEx(HWND hWnd) { m_hWnd = hWnd; } static void InstallMenuEx ( ); //安装钩子,用来钩取窗口的 WM_CREATE 消息 static void UnInstallMenuEx ( ); //卸载钩子,应用程序结束时必须卸载资源private: static HHOOK m_hMenuHook; //存储咱们安装的钩子句柄 static LRESULT CALLBACK WindowHook ( int code, WPARAM wParam, LPARAM lParam ); //安装的钩子的回调函数 static LRESULT CALLBACK MenuWndNewProc ( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); //菜单窗口的新处理过程,以绘制菜单阴影 static LRESULT CALLBACK AnysWndNewProc ( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); //其它窗口的新处理过程,以传递窗口内的菜单消息public: HWND m_hWnd; //保存了当前这个菜单的窗口句柄 BOOL OnInitMenuPopup( HWND hWnd, HMENU hMenu, UINT nIndex, BOOL bSysMenu ); //响应WM_INITMENUPOPUP消息 LRESULT OnMenuChar ( HWND hWnd, HMENU hMenu, UINT nChar, UINT nFlags ); //响应WM_MENUCHAR消息 BOOL OnMeasureItem ( HWND hWnd, LPMEASUREITEMSTRUCT lpMIS ); //响应WM_MEASUREITEM消息 BOOL OnDrawItem ( HWND hWnd, LPDRAWITEMSTRUCT lpDIS ); //响应WM_DRAWITEM消息 BOOL OnNcCalcsize ( HWND hWnd, NCCALCSIZE_PARAMS* lpncsp ); //响应WM_NCCALCSIZE消息 BOOL OnPrint ( HWND hWnd, CDC* pDC ); //响应WM_PRINT消息 BOOL OnNcPaint ( HWND hWnd ); //响应WM_NCPAINT消息 BOOL OnWindowPosChanging ( HWND hWnd, WINDOWPOS* pWindowPos ); //响应WM_WINDOWPOSCHANGING消息 BOOL OnShowWindow ( HWND hWnd, BOOL bShow ); //响应WM_SHOWWINDOW消息 BOOL OnNcDestroy ( HWND hWnd ); //响应WM_NCDESTROY消息}
第二个类我们可以理解为CMenuEx的接口,通过这个接口,可以在这里写我们实现特效的具体代码,而用不着再去打扰CMenuEx它老人家。在下面我将实现菜单阴影部分封装在CMenuShadow类里面,而把实现菜单项的特效部分封装在了CMenuItem类里面,这样各个类分工明确,互不干扰,可读性较好。
Class CMenuShaodw{ BOOL OnNcCalcsize ( HWND hWnd, NCCALCSIZE_PARAMS* lpncsp ); //响应WM_NCCALCSIZE消息 BOOL OnPrint ( HWND hWnd, CDC* pDC ); //响应WM_PRINT消息 BOOL OnNcPaint ( HWND hWnd ); //响应WM_NCPAINT消息 BOOL OnWindowPosChanging ( HWND hWnd, WINDOWPOS* pWindowPos ); //响应WM_WINDOWPOSCHANGING消息 BOOL OnShowWindow ( HWND hWnd, BOOL bShow ); //响应WM_SHOWWINDOW消息 BOOL OnNcDestroy ( HWND hWnd ); //响应WM_NCDESTROY消息}Class CMenuItem{ BOOL OnInitMenuPopup( HWND hWnd, HMENU hMenu, UINT nIndex, BOOL bSysMenu ); //响应WM_INITMENUPOPUP消息 LRESULT OnMenuChar ( HWND hWnd, HMENU hMenu, UINT nChar, UINT nFlags ); //响应WM_MENUCHAR消息 BOOL OnMeasureItem ( HWND hWnd, LPMEASUREITEMSTRUCT lpMIS ); //响应WM_MEASUREITEM消息 BOOL OnDrawItem ( HWND hWnd, LPDRAWITEMSTRUCT lpDIS ); //响应WM_DRAWITEM消息}
现在假设我们已经写好了实现菜单特效的CMenuShadow和CMenuItem两个类,就可以使用在CMenuEx中定义的几个宏将CMenuShadow、CMenuItem和CMenuEx类关联起来了:
CMenuEx_Implement_Shadow(CMenuShadow, CMenuEx); CMenuEx_ImpleMenu_Item(CMenuItem, CMenuEx);
OK,在介绍了这了菜单特效的实现框架以后,现在就开始来实现它们的特效细节Step by Step吧?
三、CMenuEx类实现
1、理解Windows消息机制
Windows消息机制该怎么理解呢?这么说吧,Windows可视化界面是以Window(窗口)为单位的, 平常我们在资源管理器里面点击鼠标右键时,系统会给它发送一个WM_RBUTTONDOWN的消息,而点击左键时,则会发送一个WM_LBUTTONDOWN消息,这只是我们平时最常见的,也最好理解。菜单也是一样,只是它比较特殊而以,比如我们在菜单”窗口“上点击鼠标右键时总不能再弹出一个菜单吧?这是因为菜单这个”窗口“没有处理我们的点击鼠标右键这个消息而已(WM_RBUTTONDOWN、WM_RBUTTONUP,当然要做到这一点是完全可以的啦,只需要让菜单这个特殊的“窗口”响应右键这个消息就可以了),既然菜单也是一个窗口,那它在创建时Windows就会给它的处理过程发送一个WM_CREATE消息,在它销毁时,Windows亦会再发送一个WM_DESTROY消息,而菜单这个窗口需要绘画时,同样也会处理WM_PAINT、WM_NCPAINT消息,如果我们能够截获这几个消息并给它来个偷梁换柱不就可以了吗?好了,现在先介绍接下来我们将会用到的几个Windows消息吧,就是因为它们才我们的菜单特效成为可能:
序号
消息名称
消息说明
1
WM_CREATE
在使用CreateWindow()或CreateWindowEx()函数创建窗口并且在函数返回前,Windows都会发送这个消息给应用程序,而新创建窗口的处理过程会收到这个消息并处理它,当新窗口收到消息时,窗口实际上已经被Create了,只是它还不可见(Invisible)而已
2
WM_NCCALCSIZE
窗口的非客户区域需要计算尺寸和位置时,该窗口会收到并处理这个消息
3
WM_WINDOWPOSCHANGING
当窗口被其它窗口使用SetWindowPos()函数操纵改变其尺寸、位置或排列顺序时,窗口会收到这个消息
4
WM_NCPAINT
当窗口需要绘画其非客户区域时,窗口会收到并处理这个消息
5
WM_PRINT
当窗口需要通过特定的设备上下文来绘画自己时,窗口会收到并处理这个消息
6
WM_DESTROY
当窗口被销毁前,窗口会收到并处理这个消息
7
WM_INITMENUPOPUP
在某个drop-down menu或submenu在被激活之前,系统允许应用程序在其被显示前修改这个菜单
8
WM_MEASRUEITEM
当菜单被创建前,应用程序会收到并处理这个消息,注意,此处的菜单被创建与前面所讲的菜单”窗口“被创建可不一样,前面的菜单”窗口“被创建是微观的,而这里的菜单则是可视化操作的一个组件
9
WM_DRAWITEM
当菜单项需要被绘画前,应用程序会收到并处理这个消息。注意,通过前面的几个窗口消息的拦截,我们可以绘画菜单”窗口“的阴影部分,而通过这个消息,我们可以绘画菜单项
10
WM_MENUCHAR
在用户使用键盘操作菜单时,应用程序会收到并处理这个消息
上面第1~6个消息都是我们处理菜单”窗口“时需要处理的非客户区域即所谓的frame时消息,而要让应用程序自动按照我们的需要绘画菜单项,则需要处理第7~10个消息。在这里所介绍的几个Windows消息仅仅是我们这个程序在实现过程中需要用到的几个消息,但对于整个Windows来说,这几个消息可真够少的。如果你需要了解更多的Windows消息,可以参阅MSDN,那里将会有你所想要的一切。
2、理解Windows钩子
Windows钩子就好像我们在钓鱼时用的钓鱼钩一样,我们要抓鱼总不能不用鱼钩让鱼儿自己跳上来吧?而Windows Hook恰好就提供了这种功能,在本文中将会用到的一个钩子叫做:WH_CALLWNDPROC,安装这个钩子可以在消息被系统发送到目标窗口过程前会先被抓过来为我们所用(先调用我们定义的lpfn子程),它的使用方法为:
HHOOK hHookSet = SetWindowHookEx( int idHook, //钩子类型,这里为WH_CALLWNDPROC HOOKPROC lpfn, //抓到窗口消息后的处理过程,我们定义为WndHookProc HINSTANCE hMode, //包含lpfn的应用程序实例句柄,一般可用AfxGetHInstance()获得 DWORD dwThreadId ); //与lpfn相关联的线程IDBOOL bClear = UnHookWindowsHookEx( hHookSet ); //卸载钩子成功后返回TRUELRESULT CALLBACK WndHookProc( int Code, WPARAM wParam, LPARAM lParam) //我们定义的钩子子程,抓到的消息都将在这里被处理{ if(当前这个消息是准备创建窗口) if(这个窗口是菜单“窗口”) 修改它的默认子程为MenuWndProc,这是我们定义的,菜单窗口的任何消息都会先调用我们的MenuWndProc,和上面的lpfn功能类似 if(这个窗口不是菜单“窗口”) 同样修改它的默认子程为AnyWndProc,这是我们定义的,它的任何消息都回先调用AnyWndProc Else 调用默认的窗口处理消息}Void CMenuEx::InstallMenuEx(){ m_hMenuHook = SetWindowHookEx( WH_CALLWNDPROC, WndHookProc, AfxGetApp()->AfxGetHInstanceHandle(), AfxGetCurrentThreadId());}Void CMenuEx::UnInstallMenuEx(){ if( m_hMenuHook ) UnHookWindowHookEx(m_hMenuHook);}
3、钩子回调函数
任何关于Window的窗口处理函数调用都将会经过这一关,而下面的代码则是对WM_CREATE消息的过滤,我们正是从这里开始菜单特效的。
LRESULT CALLBACK WndHookProc( int Code, WPARAM wParam, LPARAM lParam){ CWPSTRUCT* pStruct = (CWPSTRUCT*)lParam; HWND hWnd = pStruct->hwnd; //保存了被钩子抓过来的窗口的句柄 while (code == HC_ACTION) { if (pStruct->message != WM_CREATE && pStruct->message != 0x01E2) //确保下面的代码发生在窗口创建过程中 break; TCHAR strClassName[10]; //取得原窗口的类名 int Count = ::GetClassName (pStruct->hwnd, strClassName, sizeof(strClassName) / sizeof(strClassName[0])); WNDPROC OldProc = (WNDPROC)(long)::GetWindowLong(hWnd, GWL_WNDPROC); //取得原窗口的过程处理函数 if (Count == 6 && _tcscmp(strClassName, _T("#32768")) == 0 ){ //如果这个窗口是菜单类的话 if ( GetProp ( hWnd, strMenuOldProcName) == NULL ) //是否已经给它添加过“新的窗口处理过程函数”标记了? if( GetProp ( hWnd, strMenuDrawMenuPtr) == NULL ) //是否已经将CMenuEx类的指针添加到窗口中去了? if( SetProp ( hWnd, strMenuDrawMenuPtr, new CMenuEx(hWnd))) //将包含窗口句柄的CMenuEx类指针附加到窗口上去 if ((OldProc!=NULL) && (OldProc!=MenuWndNewProc)) //原有的窗口处理过程是已经替换掉了? if ( SetProp ( hWnd, strMenuOldProcName, OldProc)) //将它自己原有的窗口处理过程函数名存储到窗口中去,还会有用的 if( !SetWindowLong(hWnd, GWL_WNDPROC,(DWORD)(ULONG)MenuWndNewProc) ) //将新的窗口过程处理函数设为窗口默认的处理函数 RemoveProp ( hWnd, strMenuOldProcName ); }else{ if ( GetProp ( hWnd, strWndwOldProcName) == NULL ) //这下面的几句代码与上面相似,只是此时的窗口已经不是一个菜单窗口了 if( GetProp ( hWnd, strWndwMenuMsgsPtr) == NULL ) if( SetProp ( hWnd, strWndwMenuMsgsPtr, new CMenuEx(hWnd))) if ((OldProc!=NULL) && (OldProc!=MenuWndNewProc)) if ( SetProp ( hWnd, strWndwOldProcName, OldProc)) if( !SetWindowLong(hWnd, GWL_WNDPROC,(DWORD)(ULONG)AnysWndNewProc) ) RemoveProp ( hWnd, strWndwOldProcName ); } break; } return CallNextHookEx (m_hMenuHook, code, wParam, lParam); //好了,继续去抓其它的窗口过来玩玩}
当我们把窗口的过程处理函数替换掉以后,系统发送给窗口的任何消息都会先调用我们给它设置的MenuWndNewProc()和AnysWndNewProc(),而在这两个新的函数中,我们只需要重新处理上面提到的几个消息就可以了。
4、菜单窗口的消息处理
上面已经讲过,既然菜单也是一个窗口,那么它本身绘制菜单背景颜色、菜单文字、菜单边框等也是在Windows消息机制下运行的,经过上面的钩子函数,现在系统发送给菜单窗口的任何消息都将会先经过MenuWndNewProc()这一关了,在这个回调函数里面,我们只需要响应几个必要的消息,就能实现绘制菜单阴影了。
LRESULT CALLBACK CMenuEx::MenuWndNewProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ WNDPROC OldProc = (WNDPROC) GetProp(hWnd, strMenuOldProcName); //这个过程处理函数就是窗口本身的处理函数了。 CMenuEx* pWnd = (CMenuEx*)GetProp(hWnd, strMenuDrawMenuPtr); //这个CMenuEx类指针也是我们给它附加的。 if( pWnd == NULL ) goto defWndProc; switch (uMsg) { case WM_NCCALCSIZE: //当菜单窗口需要计算窗口尺寸、位置时我们替它响应了。 { LRESULT lResult = CallWindowProc(OldProc, hWnd, uMsg, wParam, lParam); pWnd->OnNcCalcsize(hWnd,(NCCALCSIZE_PARAMS*)lParam); return lResult; } break; case WM_WINDOWPOSCHANGING: //当菜单窗口的位置将会改变时,我们也替它相应了 if( pWnd->OnWindowPosChanging(hWnd,(LPWINDOWPOS)lParam)) return 0; break; case WM_PRINT: //当菜单窗口需要绘画时 { LRESULT lResult = CallWindowProc(OldProc, hWnd, uMsg, wParam, lParam); pWnd->OnPrint(hWnd,CDC::FromHandle((HDC)wParam)); return lResult; } break; case WM_NCPAINT: //当菜单窗口需要绘画非客户区域时 { if(pWnd->OnNcPaint( hWnd )) return 0; } break; case WM_SHOWWINDOW: //当菜单窗口需要显示或隐藏时 if( pWnd->OnShowWindow(hWnd,wParam != NULL) ) return 0; break; case WM_NCDESTROY: //当菜单窗口将被销毁时,将它的过程处理函数还给它 { WNDPROC MenuWndProc = (WNDPROC)::GetProp(hWnd, strMenuOldProcName); if (MenuWndProc != NULL) { ::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)(ULONG)MenuWndProc); ::RemoveProp(hWnd, strMenuOldProcName); } if( pWnd->OnNcDestroy(hWnd)) return 0; } break; } defWndProc: return CallWindowProc(OldProc, hWnd, uMsg, wParam, lParam); //调用它原有的过程处理函数处理其它的消息吧}
5、其它窗口的消息处理(传递菜单消息)
通过上面的MenuWndNewProc()函数,我们可以给菜单添加阴影,但是要绘制菜单项则需要通过菜单所在的窗口传递以下几个消息来实现了。
LRESULT CALLBACK CMenuEx::AnysWndNewProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ WNDPROC OldProc = (WNDPROC) GetProp(hWnd, strWndwOldProcName); //这个过程处理函数就是窗口本身的处理函数了。 CMenuEx* pWnd = (CMenuEx*)GetProp(hWnd, strWndwMenuMsgsPtr); //这个CMenuEx类指针也是我们给它附加的。 if( pWnd == NULL ) goto defWndProc; switch (uMsg) { case WM_INITMENUPOPUP: //当应用程序中的某个菜单将被激活时,通过这个接口函数我们可以在菜单显示前先修改它 { CallWindowProc(OldProc, hWnd, uMsg, wParam, lParam); pWnd->OnInitMenuPopup(hWnd,(HMENU)wParam, (UINT)LOWORD(lParam), (BOOL)HIWORD(lParam)); return 0; } break; case WM_MEASUREITEM: //当菜单需要计算尺寸时,我们可以在这里修改它 if( ((LPMEASUREITEMSTRUCT)lParam)->CtlType == ODT_MENU ) if( pWnd->OnMeasureItem( hWnd,(LPMEASUREITEMSTRUCT)lParam )) return TRUE; break; case WM_DRAWITEM: //通过这个接口函数,我们可以绘制想要的菜单项特效了 if( ((LPDRAWITEMSTRUCT)lParam)->CtlType == ODT_MENU ) if( pWnd->OnDrawItem( hWnd,(LPDRAWITEMSTRUCT)lParam )) return TRUE; break; case WM_MENUCHAR: //如果你需要响应用户的键盘操作,就是这里了 { LRESULT res = 0; if( (res = pWnd->OnMenuChar( hWnd, (HMENU)lParam, (TCHAR)LOWORD(wParam), (UINT)HIWORD(wParam))) != 0) return res; } break; case WM_NCDESTROY: //还是将它自己的东西自己带上吧 { WNDPROC AnyWndProc = (WNDPROC)::GetProp(hWnd, strWndwOldProcName); if (AnyWndProc != NULL) { ::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)(ULONG)AnyWndProc); ::RemoveProp(hWnd, strWndwOldProcName); } } break; } defWndProc: return CallWindowProc(OldProc, hWnd, uMsg, wParam, lParam); //调用它原有的过程处理函数处理其它的消息}
|
|