漂亮菜单的实现原理与技巧
Elliot Liu
(aisnote@gmail.com)
https://aisnote.com
http://bbs.aisnote.com
摘要:概述性的介绍了如何自画windows上的菜单以及相关的技巧。本文适合对象为有一定Windows编程基础的人。
1、引言
菜单是Windows标准控件之一,菜单几乎在每个应用软件中都存在。MS提供的默认菜单是比较难看的,但MS自己制作的程序比如Office等,用的菜单却很漂亮,而且一般不是默认的标准菜单(运用DrawMenuBar)。
从office97开始,用的是rebar里面嵌入一个CommandBar来模拟菜单。这个可以用spy++抓一下就知道了,这样做的好处是可以到处停靠(Dock),熟悉Windows的人都知道,停靠是标准控件Toolbar的一个普通属性,所以我们可以猜测到CommandBar应该就是一个toolbar。当然从WTL的源码里也可以找到。
如何字画菜单的文章网上有很多:
http://www.vckbase.com/document/listdoc.asp?mclsid=3&sclsid=303
这个URL都是关于如何自画菜单的,但对于初学者来说或者不熟悉Windows的人来说,还是有一定的难度的。但大致都分为如下二个步骤:
l 处理WM_DRAWITEM 消息:判断是不是来自菜单,然后根据一个item,一特item的信息进行绘制,前提是你得了解一些GDI的基本绘制函数。这个过程是比较简单的。
l 为了处理菜单的边框,需要安装一个HOOK,去抓菜单的窗口,MSDN提供的菜单的Class Name是“#32768”。这个过程需要一定的Windows编程基础,以及HOOK的使用方式。有点经验的人只需看看MSDN,或者GOOGLE个例子来,就行了,上面的URL也有类似的文章。
上述的自画菜单虽然比较漂亮,但有一定的缺陷:
l 不能控制MenuBar的位置。
l 很难改变Menubar的颜色。如下2图的比较:
从上面可以看出,左边的menubar还是原来的颜色,只是自画了菜单子项。右边的是本文运用commandbar来模拟的menubar,可以动态的改变menbar的颜色。当然,不用commandbar也是可以做到更改menubar的颜色的,但coding起来比较困难,而且动态更改有一定的难度,本文也没有去实现过,如果有谁实现过这样的code,希望能分享一下。
本文从具体的project出发,简单介绍运用WTL的commandbar来制作精美的菜单,以及运用hook技术如何写一个通用的popup menu的类,以方便project的开发。
2、实现原理
WTL中的源代码已经非常详细的告诉你实现的原理了,无非是以下几条关键的信息:
l Commandbar是一个从toolbar继承下来的窗口。
l 既然是toolbar,那就有button,这些button就是主菜单的一级菜单项。
在创建commandbar的时候,把原来的系统默认的菜单去掉,然后把这个菜单资源给commandbar,commandbar遍历这个menu,把第一级的菜单项变为commandbar上的button,如图:
图的下面是 VC 资源编辑器里的菜单。一般在mainframe 的OnCreate里有如下的code(一般可以由WTL 向导自动生成)。
// create command bar window
m_pCmdBar = new CWbxMenu; // CWbxMenu就是一个commandbar
HWND hWndCmdBar = m_pCmdBar->Create(m_hWnd, rcDefault, NULL, WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | CBRWS_NODIVIDER | /*CBRWS_NORESIZE | */CBRWS_NOPARENTALIGN);
// attach menu
m_pCmdBar->AttachMenu(GetMenu());
// load command bar images
m_pCmdBar->LoadImages(IDR_MAINFRAME);
// remove old menu
SetMenu(NULL);
CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);
BOOL retTmp = m_pCmdBar->SetButtonSize(CSize(0, 25));
AddSimpleReBarBand(hWndCmdBar);
m_pCmdBar->Prepare();
知道上面的原理后,你就可以把commandbar放到dialog等你想放的窗口里去。
l Popupmenu:commandbar是一个模拟的菜单。有了第一级的button后,就是一个toolbar的操作了,无非处理一些mousemove,char等系统消息。Click一个button的时候就弹出一个popupmenu。当然模拟标准菜单的一些操作还是比较复杂的,需要处理好多消息,摘录一些WTL的源码如下:
class CCommandBarCtrlBase : public CToolBarCtrl
// Message map and handlers
BEGIN_MSG_MAP(CCommandBarCtrlImpl)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_INITMENU, OnInitMenu)
MESSAGE_HANDLER(WM_INITMENUPOPUP, OnInitMenuPopup)
MESSAGE_HANDLER(WM_EXITMENULOOP, OnExitMenuLoop) // elliot add.07/01/2004
MESSAGE_HANDLER(WM_ENTERMENULOOP, OnEnterMenuLoop) // elliot add.07/01/2004
MESSAGE_HANDLER(WM_MENUSELECT, OnMenuSelect)
MESSAGE_HANDLER(GetAutoPopupMessage(), OnInternalAutoPopup)
MESSAGE_HANDLER(GetGetBarMessage(), OnInternalGetBar)
MESSAGE_HANDLER(WM_SETTINGCHANGE, OnSettingChange)
MESSAGE_HANDLER(WM_MENUCHAR, OnMenuChar)
MESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown)
MESSAGE_HANDLER(WM_KEYUP, OnKeyUp)
MESSAGE_HANDLER(WM_CHAR, OnChar)
MESSAGE_HANDLER(WM_SYSKEYDOWN, OnSysKeyDown)
MESSAGE_HANDLER(WM_SYSKEYUP, OnSysKeyUp)
MESSAGE_HANDLER(WM_SYSCHAR, OnSysChar)
// public API handlers – these stay to support chevrons in atlframe.h
MESSAGE_HANDLER(CBRM_GETMENU, OnAPIGetMenu)
MESSAGE_HANDLER(CBRM_TRACKPOPUPMENU, OnAPITrackPopupMenu)
MESSAGE_HANDLER(CBRM_GETCMDBAR, OnAPIGetCmdBar)
MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)
MESSAGE_HANDLER(WM_FORWARDMSG, OnForwardMsg)
ALT_MSG_MAP(1) // Parent window messages
NOTIFY_CODE_HANDLER(TBN_HOTITEMCHANGE, OnParentHotItemChange)
NOTIFY_CODE_HANDLER(TBN_DROPDOWN, OnParentDropDown)
MESSAGE_HANDLER(WM_INITMENUPOPUP, OnParentInitMenuPopup)
MESSAGE_HANDLER(GetGetBarMessage(), OnParentInternalGetBar)
MESSAGE_HANDLER(WM_SYSCOMMAND, OnParentSysCommand)
MESSAGE_HANDLER(CBRM_GETMENU, OnParentAPIGetMenu)
MESSAGE_HANDLER(WM_MENUCHAR, OnParentMenuChar)
MESSAGE_HANDLER(CBRM_TRACKPOPUPMENU, OnParentAPITrackPopupMenu)
MESSAGE_HANDLER(CBRM_GETCMDBAR, OnParentAPIGetCmdBar)
MESSAGE_HANDLER(WM_DRAWITEM, OnParentDrawItem)
MESSAGE_HANDLER(WM_MEASUREITEM, OnParentMeasureItem)
MESSAGE_HANDLER(WM_ACTIVATE, OnParentActivate)
NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnParentCustomDraw)
ALT_MSG_MAP(2) // MDI client window messages
// Use CMDICommandBarCtrl for MDI support
ALT_MSG_MAP(3) // Message hook messages
MESSAGE_HANDLER(WM_MOUSEMOVE, OnHookMouseMove)
MESSAGE_HANDLER(WM_SYSKEYDOWN, OnHookSysKeyDown)
MESSAGE_HANDLER(WM_SYSKEYUP, OnHookSysKeyUp)
MESSAGE_HANDLER(WM_SYSCHAR, OnHookSysChar)
MESSAGE_HANDLER(WM_KEYDOWN, OnHookKeyDown)
MESSAGE_HANDLER(WM_NEXTMENU, OnHookNextMenu)
MESSAGE_HANDLER(WM_CHAR, OnHookChar)
END_MSG_MAP()
l 上面的三个步骤基本上已经模拟好菜单了,接下来的工作无非是把popupmenu进行自画,画成你想要的效果。如何自画一个menu是一件简单的事情,引言里已经介绍过。关键的一点是如何画菜单的边框以及菜单的阴影等效果。这个可以使用hook来做。在弹出菜单的时候安装hook去抓菜单的窗口,然后对这个窗口进行绘制,在菜单退出时,卸载hook。在coding的时候,可以自己封装一个TrackPopupMenu的函数,示例如下:
BOOL CCommandBarXPCtrl::DoTrackPopupMenu
{
……
::EnterCriticalSection(&_Module.m_csWindowCreate);
ATLASSERT(s_hCreateHook == NULL);
s_pCurrentBar = static_cast<CCommandBarCtrlBase*>(this);
//install hook
s_hCreateHook = ::SetWindowsHookEx(WH_CBT, MyCreateHookProc, _Module.GetModuleInstance(), GetCurrentThreadId());
ATLASSERT(s_hCreateHook != NULL);
m_bPopupItem = false;
m_bMenuActive = true;
BOOL bTrackRet = menuPopup.TrackPopupMenuEx(uFlags, x, y, m_hWnd, lpParams);// 真正弹出菜单
m_bMenuActive = false;
::UnhookWindowsHookEx(s_hCreateHook); //uninstall hook
s_hCreateHook = NULL;
s_pCurrentBar = NULL;
::LeaveCriticalSection(&_Module.m_csWindowCreate);
ATLASSERT(m_stackMenuWnd.GetSize() == 0);
}
安装的hook一般只需CBT的hook就行了,判断一下窗口是不是菜单,如果是菜单就可以进行你自己的处理,本文使用的是先写一个class,对menu的window进行subclass,code如下:
LRESULT CALLBACK CCommandBarXPCtrl::MyCreateHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
LRESULT lRet = 0;
TCHAR szClassName[7];
if( nCode == HCBT_CREATEWND )
{
HWND hWndMenu = (HWND)wParam;
::GetClassName(hWndMenu, szClassName, 7);
// menu window hander is 32768.This tip was added by elliot
if( ::lstrcmp(_T(“#32768”), szClassName) == 0 ) {
if (s_pCurrentBar != NULL)
{
s_pCurrentBar->m_stackMenuWnd.Push(hWndMenu);
//s_pCurrentBar->m_stackMenuWndMagic.Push(hWndMenu) ; // for recorder crash
}
// Subclass to a flat-looking menu
CFlatMenuWindow* wnd = new CFlatMenuWindow(m_rcButton.right – m_rcButton.left, g_sPopMenuColors.m_clrFrame, g_sPopMenuColors.m_clrBackground, g_sPopMenuColors.m_clrMenu,m_rcTrueButton);
wnd->SubclassWindow(hWndMenu);
wnd->SetWindowPos(HWND_TOP, 0,0,0,0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_DRAWFRAME);
::SetRectEmpty(&m_rcButton);
}
}
else if( nCode == HCBT_DESTROYWND )
{
HWND hWndMenu = (HWND)wParam;
::GetClassName(hWndMenu, szClassName, 7);
if( ::lstrcmp(_T(“#32768”), szClassName) == 0 )
{
if (s_pCurrentBar != NULL)
{
ATLASSERT(hWndMenu == s_pCurrentBar->m_stackMenuWnd.GetCurrent());
s_pCurrentBar->m_stackMenuWnd.Pop();
//s_pCurrentBar->m_stackMenuWndMagic.Pop() ;
}
}
}
else if( nCode < 0 )
{
lRet = ::CallNextHookEx(s_hCreateHook, nCode, wParam, lParam);
}
return lRet;
}
3、单独封装一个Popupmenu的class
上面讲述的是适合主窗口的菜单,把其中弹出菜单的一部分进行封装,就可以用来做右键弹出菜单了。
4、其他的一些技巧:
l Sidebar[1][1]:就是在菜单的左边画一个竖条,如图:
这个sidebar的实现只需在WM_MEASUREITEM里指定你要画的item的宽度,并把它的高度设置为0(这样做的目的是为了方便编辑sidebar的item,如图:
然后在WM_DRAWITEM里运用api GetClipBox(&rct) 取得当前DC的裁剪区域,然后自己画上去就行了,当然你也可以贴一个图上去,那样会更加的好看。
l 菜单的伸缩:
然后点击那个红圈,就可以展开新的菜单,如下图:
本文的实现是这样的:箭头只要是ownerdraw就可以了,点击箭头那个item的时候,把箭头的item删掉,然后同时插入2个新的menu item,再trackPopupMenu一下。我查看了word2003的伸缩菜单,估计差不多也是这个实现方法,如果谁有更好的实现方法,不吝赐教。
l 关于disable的有子项的menuitem的右边那个箭头:有个bug一直放到现在:我们现在的meetingmgr里还存在的,见图:
详见bug #187407,现在还是open的。
,因为一时半会解决不了,因为那个箭头你在WM_DRAWITEM里画完之后,系统会给你再画一遍,气死你。后来有个文章被我查到了[2],其实只要我们画好那个箭头,把那个箭头的当前区域从dc里裁剪掉就ok了,当然你得透明画个位图,否则样子比较难看,具体见codeguru的文章或查看uilib的源代码。
5、结束语
菜单还是ownerdraw的好,自己用窗口模拟一个编码会比较麻烦,如果大家有好的想法或更好的实现方式,可以和我联系。
参考文献:
1. http://www.codeproject.com/wtl/sidebarmenu.asp
2. http://www.codeguru.com/cpp/controls/menu/miscellaneous/article.php/c13017/
附录:例子图片:
Refer to:http://www.codeproject.com/menu/menuch.asp
右边的图形选择其实也是个菜单。MF_BREAK ,WM_DRAWITEM中响应一个参数
void CMenuCH::DrawColorMenu(LPDRAWITEMSTRUCT lpDIS)
{
CDC* pDC = CDC::FromHandle(lpDIS->hDC);
CMenuItem* pItem = reinterpret_cast<CMenuItem *>(lpDIS->itemData);
CRect rect(&lpDIS->rcItem);
if (lpDIS->itemAction & ODA_DRAWENTIRE)
{
// paint the brush and color item in requested
pDC->FrameRect(rect,&CBrush(GetSysColor(COLOR_3DFACE)));
rect.DeflateRect(3,3,3,3);
pDC->FrameRect(rect,&CBrush(RGB(128,128,128)));
rect.DeflateRect(1,1,1,1);
// draw a rectangle palette
pDC->FillSolidRect(rect,(COLORREF) atol(pItem->m_szText));
}
if ((lpDIS->itemState & ODS_SELECTED) &&
(lpDIS->itemAction & (ODA_SELECT | ODA_DRAWENTIRE)))
{
// item has been selected – raised frame
pDC->DrawEdge(rect, EDGE_RAISED, BF_RECT);
m_SelColor = (COLORREF) atol(pItem->m_szText);
m_curSel = lpDIS->itemID;
}
if (!(lpDIS->itemState & ODS_SELECTED) &&
(lpDIS->itemAction & ODA_SELECT))
{
// item has been de-selected — remove frame
CBrush br(GetSysColor(COLOR_3DFACE));
pDC->FrameRect(rect, &br);
rect.DeflateRect(1,1,1,1);
pDC->FrameRect(rect, &br);
}
}
要注意这个菜单的创建:需要自己包装一下:(MFC的code 来自codeproject)
ColorMenu.CreatePopupMenu();
ColorMenu.SetMenuHeight(18);
ColorMenu.SetMenuWidth(6);
ColorMenu.SetMenuType(MIT_COLOR);
char clrValue[64];
for(int i=1; i<=16; i++)
{
wsprintf(clrValue,”%d”,rgbColors[i-1]);
if( i%4 == 1 )
ColorMenu.AppendMenu(MF_MENUBREAK|MF_ENABLED,i,clrValue);
else
ColorMenu.AppendMenu(MF_ENABLED,i,clrValue);
}
m_ElementMenu.AppendMenu(MF_POPUP,(UINT)ColorMenu.m_hMenu,”Colors”);
另外:可以把菜单单独封装成一个dll,创建一个隐藏的窗口来转发消息,这样就不需要定义一个宏来转消息,直接一个接口就搞定。
good article.
very good. just for comment test
test reply