漂亮菜单的实现原理与技巧

漂亮菜单的实现原理与技巧

Elliot Liu

(aisnote@gmail.com)
http://aisnote.com
http://bbs.aisnote.com

clip_image002

摘要:概述性的介绍了如何自画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图的比较:

clip_image004

从上面可以看出,左边的menubar还是原来的颜色,只是自画了菜单子项。右边的是本文运用commandbar来模拟的menubar,可以动态的改变menbar的颜色。当然,不用commandbar也是可以做到更改menubar的颜色的,但coding起来比较困难,而且动态更改有一定的难度,本文也没有去实现过,如果有谁实现过这样的code,希望能分享一下。

本文从具体的project出发,简单介绍运用WTLcommandbar来制作精美的菜单,以及运用hook技术如何写一个通用的popup menu的类,以方便project的开发。

2、实现原理

WTL中的源代码已经非常详细的告诉你实现的原理了,无非是以下几条关键的信息:

l Commandbar是一个从toolbar继承下来的窗口。

l 既然是toolbar,那就有button,这些button就是主菜单的一级菜单项。

在创建commandbar的时候,把原来的系统默认的菜单去掉,然后把这个菜单资源给commandbarcommandbar遍历这个menu,把第一级的菜单项变为commandbar上的button,如图:

clip_image006

图的下面是 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 Popupmenucommandbar是一个模拟的菜单。有了第一级的button后,就是一个toolbar的操作了,无非处理一些mousemovechar等系统消息。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一般只需CBThook就行了,判断一下窗口是不是菜单,如果是菜单就可以进行你自己的处理,本文使用的是先写一个class,对menuwindow进行subclasscode如下:

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、单独封装一个Popupmenuclass

上面讲述的是适合主窗口的菜单,把其中弹出菜单的一部分进行封装,就可以用来做右键弹出菜单了。

4、其他的一些技巧:

l Sidebar[1][1]就是在菜单的左边画一个竖条,如图:

clip_image008

这个sidebar的实现只需在WM_MEASUREITEM里指定你要画的item的宽度,并把它的高度设置为0(这样做的目的是为了方便编辑sidebaritem,如图:

clip_image010),

然后在WM_DRAWITEM里运用api GetClipBox(&rct) 取得当前DC的裁剪区域,然后自己画上去就行了,当然你也可以贴一个图上去,那样会更加的好看。

l 菜单的伸缩:

clip_image012

然后点击那个红圈,就可以展开新的菜单,如下图:

clip_image014

本文的实现是这样的:箭头只要是ownerdraw就可以了,点击箭头那个item的时候,把箭头的item删掉,然后同时插入2个新的menu item,再trackPopupMenu一下。我查看了word2003的伸缩菜单,估计差不多也是这个实现方法,如果谁有更好的实现方法,不吝赐教。

l 关于disable的有子项的menuitem的右边那个箭头:有个bug一直放到现在:我们现在的meetingmgr里还存在的,见图:

clip_image016

详见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/


附录:例子图片:

clip_image018

Refer tohttp://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);

}

}

要注意这个菜单的创建:需要自己包装一下:(MFCcode 来自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,创建一个隐藏的窗口来转发消息,这样就不需要定义一个宏来转消息,直接一个接口就搞定。

,
17 comments to “漂亮菜单的实现原理与技巧”

Comments are closed.