先看最终效果,有个直观感受。
这个是最普通的二级菜单,每个菜单项画了一张图片。
图一
我们经常看到下面这种菜单:左边是个小图标,右边是文字,这样的效果我们也可以做出来,见图三:
图二
唯一不同的是……我们的图片和菜单项……个头儿都很大……
图三
上边都是单列的,能不能多列呢?能!在资源编辑器中把要选择一个菜单项,设置其“Break”属性为"Column",那么这个菜单项的下一项将另起一列。代码里也能修改这个属性。效果见图四:
图四
有几个基本概念,后面会用上,请大家耐心看完。请看图:
图五
红框所在位置叫MenuBar,直译过来就是“菜单栏”。MainMenu、Menu2、SubMenuA这些叫MenuItem,直译就是“菜单项”
蓝框里的是被弹出来的叫SubMenu,直译就是“子菜单”,就是大家日常看到的、直接使用的东西。
步骤:
1. 在WM_CONTEXTMENU消息响应里,用LoadMenu载入菜单栏,用GetSubMenu载入要显示的子菜单,用TrackPopupMenu(或TrackPopupMenuEx)弹出菜单
2. 在WM_INITMENUPOPUP中修改各MenuItem为OwnerDraw风格
3. 在WM_MEASUREITEM中按个人需要修改MenuItem宽、高
4. 在WM_DRAWITEM中使用DRAWITEMSTRUCT结构体中的hDc和rcItem,结合GDI函数,绘制MenuItem
首先新建一个基于对话框的WTL项目,关于WTL开发环境的配置、向导的安装请自行google
然后在资源编辑器中新增一个菜单,为了演示的丰富一些,我们设计了一个二级菜单,中文和英文菜单项都有。我们菜单的ID是IDR_MENU1,如图所示:
图六
最后,在代码里响应WM_CONTEXT消息,在消息响应函数中用CMenu类加载菜单,用CMenuHandle类操作子菜单,弹出菜单。在WM_CONTEXT的响应函数里写上如下代码:
CMenumenu;
menu.LoadMenu(IDR_MENU1);//此处menu代表的是根菜单
//以IDR_MENU1为例,它里面包含两个子菜单,GetSubMenu就是按索引选择一个,用于显示
CMenuHandlemenuHandle = menu.GetSubMenu(0);//此处是IDR_MENU1下拉出来的子菜单 menuHandle.TrackPopupMenuEx(TPM_LEFTBUTTON|TPM_LEFTALIGN|TPM_TOPALIGN,GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam), m_hWnd);
之前说过,菜单即可以在资源编辑器中事先设计好,也可以用代码动态创建。我们的示例代码是事先编辑好的。
动态创建的话,只需要调用CMenuHandle::CreatePopupMenu()创建,InsertMenuItem函数插入菜单项,最后TrackPopuMenu显示菜单即可,这几个函数都是CMenuHandle都是对同名API的简单封装,具体用法请参见WTL源码与MSDN。
编译,执行。右键在我们程序上单击,菜单出来了。
既然是自绘,我们要想办法把菜单的风格改成OwnerDraw,不幸的是,VC的资源编辑器干不了这事儿,我们只能用代码修改,这也是MSDN上明确提到的。具体在哪儿改呢?
有两个地方:一是在TrackPopupMenu函数调用之前;另一个方法是在WM_INITMENUPOPUP消息响应函数中;
修改menu为自绘时,需要一个一个的修改MenuItem,没有集中修改的方法。在一个菜单中,自绘型菜单项和非自绘型(也即windows标准类型)可以并存。
修改为自绘菜单项时,用到一个结构体(MENUITEMINFO)和一个API(SetMenuItemInfo),由于WTL中有同名封装类,我们代码里就不直接调用API了。
此处示例选择在WM_INITMENUPOPUP消息响应函数中修改菜单项为自绘。代码示意如下:
CMenuHandle menuHandle = HMENU(wParam);
for (intn = 0; n < menuHandle.GetMenuItemCount(); n++)
{
CMenuItemInfo ItemTempInfo;
ItemTempInfo.fMask = MIIM_TYPE;
// 获取旧信息
menuHandle.GetMenuItemInfo(n, TRUE, &ItemTempInfo);
//设置自绘标志
ItemTempInfo.fType |= MFT_OWNERDRAW;
//保存到菜单项
menuHandle.SetMenuItemInfo(n, TRUE, &itemInfo);
}
上边代码大意就是:拿到菜单的句柄,然后挨个把它的MenuItem修改为MFT_OWNERDRAW。如果一个MenuItem有下级菜单(如示例中的SubMenuC),那这个菜单项的MENUITEMINFO::hSubMenu句柄将不为空,代表着其下级菜单,用上边同样的代码就可修改下级菜单为自绘。无论菜单有几级,都是这一原理。
修改完自绘风格后,菜单在真正的“显示在显示器上”之前,windows会先后给我们的程序发WM_MEASUREITEM和WM_DRAWITEM消息。WM_MEASUREITEM中用于设置菜单项的宽度和高度,WM_DRAWITEM中进行正式的绘制工作。
下面是WM_MEASUREITEM的响应代码
LPMEASUREITEMSTRUCTlpMeasureItemStruct = (LPMEASUREITEMSTRUCT)lParam;
if(lpMeasureItemStruct->CtlType == ODT_MENU)
{
lpMeasureItemStruct->itemHeight= 110;
lpMeasureItemStruct->itemWidth= 374;
}
把lParam转换为MEASUREITEMSTRUCT指针,这是MSDN里说明的标准流程,微软就这样设计的,没什么道理可讲。接下来就是设置这个结构体中的字段,我们用到宽度和高度,这里设置完后,显示出来的菜单项就是这个宽和高。关于结构体的详细说明,请自行MSDN。
如果你不关心宽度和高度,打算直接用默认的值,可以不管WM_MEASUREITEM消息。接下来我们响应WM_DRAWITEM消息,在响应函数中编写绘制代码。原理很简单:WM_DRAWITEM消息的lParam转换成DRAWITEMSTRUCT的指针,这个结构体中的菜单项的dc和菜单项所在的矩形,如我们系列文件的第一篇所说,有了DC和Rect,万事具备了就!剩下的就是用GDI函数开始画了。
下面是我们的绘制代码,在WM_DRAWITEM消息响应函数里。主要做了三件事:
第一件:从DRAWITEMSTRUCT中取到菜单项的DC;
第二件:载入一个位图,示例位图是我事先准备好的,大家按自己喜好找张位图就行。
第三件:用GDI函数画到DRAWITEMSTRUCT中指定的矩形范围内。
这里我们用到所谓的双缓冲技术,就是先往一个内存DC上画,全都画完后,再把整个画完的图直接Copy到目标DC
LPDRAWITEMSTRUCTlpdis = (LPDRAWITEMSTRUCT)lParam;
BOOLbSelected = lpdis->itemState & ODS_SELECTED;
//转换成类,便于调用GDI函数
CDCHandlehdc = lpdis->hDC;
CDCmemDc;
memDc.CreateCompatibleDC(hdc);
CBitmapbmpBk;
HBITMAPhOld = NULL;
hdc.SetBkMode(TRANSPARENT);//画文字时背景透明
if(bSelected)
{
if(bmpBk.LoadBitmap(IDB_BITMAP1) == NULL)
{
strError.Format(_T("error%d"), GetLastError());
}
}
else
{
if(bmpBk.LoadBitmap(IDB_BITMAP2) == NULL)
{
strError.Format(_T("error%d"), GetLastError());
}
}
hOld= memDc.SelectBitmap(bmpBk);
SIZEsize;
bmpBk.GetSize(size);
hdc.StretchBlt(lpdis->rcItem.left,lpdis->rcItem.top, lpdis->rcItem.right-lpdis->rcItem.left,lpdis->rcItem.bottom-lpdis->rcItem.top, memDc, 0, 0, size.cx, size.cy ,SRCCOPY);
memDc.SelectBitmap(hOld);
最后绘制的效果如本文开头所示。
本例出于简单、直观的目的,没有什么封装的东西,几乎都是最直接的代码。涉及到的什么CDCHandle,CDC都是WTL封装的类,里面都是直接调用的同名API,大家可以去看源码。
至此,我们WML-Menu自绘的主要内容就讲完了。主体思路如前文所述:先修改菜单项为OwnerDraw风格,然后在WM_MEASUREITEM和WM_DRAWITEM中设置宽高、具体绘制。
示例绘制代码中直接画了一张图片在菜单项上,主要是为了简单起见,防止代码过长影响阅读、影响理解。
MENUITEMINFO结构体中有个dwItemData字段,它是个指针,指向的就是菜单原来显示的文字
其实不光是菜单自绘,只要是OwnerDraw风格的控件,都会涉及这两个消息,但要注意一点,就是有的控件自绘时不会触发WM_MEASUREITEM消息,只有WM_DRAWITEM消息,关于这一点,MSDN中有详细说明。在我们系列文章总结篇中,也将提及这一点,请大家留意,谨记!
最后给出源码工程链接:http://pan.baidu.com/share/link?shareid=2407747361&uk=1980187499
注意:要想编译运行,请自行google找办法搭建vs2008+wtl8.1的开发环境。