菜单是windows应用程序经常使用的资源之一。从今天开始,把有关菜单的创建及使用等各方面的基本用法总结到一起,算是做个笔记吧!准备一天写一点,陆陆续续完成!
一、菜单的创建:
(一)VC++菜单通过编辑资源文件直接创建菜单
可以自己编写菜单资源文件来完成来手工创建菜单,这与编程方法创建菜单不同。用这种方法创建菜单分两步:
1、在rc资源文件中编写菜单资源:
首先,要用其它文字编辑软件打开rc文件,然后在里面手工编写菜单资源。举例如下:
IDR_MENU1 MENU PRELOAD DISCARDABLE
(注释:"IDR_MENU1"-->菜单ID名; "MENU"-->资源类名)
BEGIN
(注释:以BEGIN开始,END结束菜单定义)
POPUP "&File"
(注释:POPUP--> 表明是一个弹出菜单 ;“&File"-->为菜单名字。"&"定义紧接后面的字母F快捷键。)
BEGIN
MENUITEM "&New" ID_FILE_NEW
(注释:MENUITEM-->子菜单项;ID_FILE_NEW是ID值。)
MENUITEM "&Edit" ID_FILE_EDIT
MENUITEM SEPARATOR
(注释:SEPARATOR-->分隔水平线;后面不需要跟ID值。)
MENUITEM "&Copy" ID_FILE_COPY
MENUITEM "E&xit" ID_FILE_EXIT
END
END
其次,编辑resource.h文件,对菜单中的各种ID进行定义。
#define ID_FILE_NEW 0X8001
#define ID_FILE_EDIT 0X8002
#define ID_FILE_COPY 0X8003
#define ID_FILE_EXIT 0X8004
通过以上两步,就已经成功编辑了一个名为”File",ID为IDR_MENU1的菜单,此菜单弹出时,有“New","Edit","Copy","Exit"等四项菜单命令组成的子菜单。
定义一个CMenu变量,通过CMenu.LoadMenu()方法可以加载上面手工编成的菜单:
CMenu menu;
menu.loadMenu(IDR_MENU1);
然后通过menu变量就可以进一步操作菜单了。
也可以通过CFrameWnd::LoadFrame 加载菜单,直接把菜单挂载到框架窗口上面。如:
((CMainFrame *)pMainWnd)->LoadFrame(IDR_MENU1);
有些菜单是系统编制的,如程序的“系统菜单”。这种菜单你只可以在系统的基础上加以修改、添加和删除一些命令。
通过CMenu *pMenu=CWnd::GetSystemMenu(FALSE);即可获取系统菜单,然后可以对pMenu菜单指针进一步操作了。
(二)、通过编程软件visual studio中的资源视图中创建菜单:
这个比较简单易懂,进入编程环境中的“资源视图”中,右键“添加资源”,选择“Menu",即可开始手工创建工菜单。
直接上图:
(三)、编程创建菜单
使用CMenu::CreateMenu,CMenu::CreatePopupMenu();CMenu::AppendMenu()可以编程创建菜单。
CMenu mainMenu;//创建一个CMenu变量,用于指向顶级菜单。
mainMenu.CreateMenu();//创建一个空的顶级菜单
CMenu popupMenu;//创建一个CMenu变量,用于指向弹出菜单——子菜单。
popupMenu.CreatePopupMenu();//创建一个空的弹出菜单——子菜单。
popupMenu.AppendMenu(MF_STRING,ID_EDIT_COPY,_T("&Copy"));//添加一个菜单子项。
popupMenu.AppendMenu(MF_STRING,ID_EDIT_CUT,_T("C&ut"));//添加一个菜单子项。
popupMenu.AppendMenu(MF_SEPARATOR);//添加一个菜单子项:水平分隔线。
mainMenu.AppendMenu(MF_POPUP,(UINT_PTR)popupMenu.detach(),_T("&Edit"));//在顶级菜单中挂载一个弹出菜单,其名为:Edit。//有些版本是强制转化为UINT
popupMenu.AppendMenu(MF_STRING,ID_FILE_NEW,_T("&New"));//添加一个菜单子项。
popupMenu.AppendMenu(MF_STRING,ID_FILE_OPEN,_T("&Open"));//添加一个菜单子项。
mainMenu.AppendMenu(MF_POPUP,(UINT_PTR)popupMenu.detach(),_T("&File));//有些版本是强制转化为UINT
setMenu(&mainMenu);//把菜单挂载到框架窗口上。
mainMenu.detach();//及时分离,防止因变量mainMenu的生命周期结束而影响菜单,如果不detatch,要影响到菜单不能正确显示。
菜单到此,编程的代码基本完成了。但是,上述代码中涉及的各种“ID”,需要在程序资源里面全部定义为常量才算全部完成。
ID_FILE_NEW,ID_FILE_OPEN,ID_FILE_EXIT,ID_EDIT_COPY,ID_EDIT_CUT等是系统已经定义的ID,不需要重新定义(当然,正常情况下不要使用这些ID)。如果ID为自定义的,需要你自己定义为常量。
二、菜单的挂载
有了菜单,当然要挂载到框架窗口上面。挂载的方法有以下几种:
(一)框架窗口创建时挂载
CFrameWnd::Create
BOOL Create( LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle = WS_OVERLAPPEDWINDOW, const RECT& rect = rectDefault, CWnd* pParentWnd = NULL, LPCTSTR lpszMenuName = NULL, DWORD dwExStyle = 0, CCreateContext* pContext = NULL );
使用这种方法挂载菜单时,菜单一般为拥有 ID,不能直接在此函数中使用。这时需要用到 MAKEINTRESOURCE()宏,其参数为WORD,返回值为:LPTSTR,这就能在函数中使用此菜单参数了。
(二)使用框架窗口类的虚函数CFrameWnd::LoadFrame()挂载菜单
CFrameWnd::LoadFrame
virtual BOOL LoadFrame( UINT nIDResource, DWORD dwDefaultStyle = WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, CWnd* pParentWnd = NULL, CCreateContext* pContext = NULL );
此函数直接使用菜单的ID挂载。
(二)使用窗口类的SetMenu()函数挂载菜单
CWnd::SetMenu
BOOL SetMenu( CMenu* pMenu );
此函数要使用菜单指针,所以多用于编程形成的菜单的挂载。
三、菜单的修改
(一)菜单的插入
CMenu::InsertMenu
BOOL InsertMenu( UINT nPosition, UINT nFlags, UINT_PTR nIDNewItem = 0, LPCTSTR lpszNewItem = NULL );
菜单的插入,使用CMenu::InsertMenu就可以了。怎样使用它呢?
假设程序的现有菜单如下图所示:


现在我们可以这样插入:
1、插入“分隔线”:
插入的位置在File菜单的第2个菜单项之后:
CMenu *pMenu=GetMenu();//获取当前菜单指针
pMenu->GetSubMenu(0)->InsertMenu(2,MF_BYPOSITION|MF_SEPARATOR);//在第一个子菜单第3个菜单项之前插入“分隔线”
第一个参数nPosition 要参照第二个参数nFlags的设置来确定含义。当nFlags包含MF_BYPOSITION的值时,nPosition 就为基于0开始的索引值。如上面语句中,2即为第三个菜单项FileThree的索引(从0开始的索引)值。
nFlags缺省为MF_STRING的值。如果设置包含MF_SEPARATOR,第三、四两个参数就忽略。如上所示。因此,nFlags既包含对nPostion的解释的设置,又包含对后面插入对象的设置。二者之间用逻辑符号 | 连结。
如果nFlag=MF_BYCOMMAND|MF_SEPARATOR的设置,则nPostion应为菜单项的 ID值,而不是基于位置0的索引。
效果图如下:

2、插入新的菜单项:
CMenu *pMenu=GetMenu();//获取当前菜单指针
pMenu->GetSubMenu(0)->InsertMenu(ID_FILE_FILEONE,MF_BYCOMMAND|MF_STRING,0X8001,_T("File000"));
其中,ID_FILE_FILEONE为菜单FileOne的ID。即要此项前插入一个新的菜单项:名字为“File000",ID为0x8001。效果图如下:

3、插入已经存在的弹出菜单为子菜单:
CMenu *pMenu=GetMenu();//获取当前菜单指针
CString strMenu;
pMenu->GetMenuString(1,strMenu,MF_BYPOSTION);//获取第2个子菜单(弹出菜单)的名称。
pMenu->GetSubMenu(0)->InsertMenu(
0, //插入的位置是基于0的索引,不是ID。与第二个参数设置为MF_BYPOSITION有关。
MF_BYPOSITION|MF_POPUP, //设置插入的类型为”弹出菜单”,与前两种不一样。
(UINT_PTR)(pMenu->GetSubMenu(1)->m_hMenu), //插入的菜单的句柄
strMenu //插入后菜单仍然为原来的名字
);
效果如下:

这是把第二个菜单Edit的全部内容挂载到第一个菜单File下面的第一个位置,并作为其子菜单。
实现这个效果,需要在nFlags选项设置MF_POPUP,然后在第三个参数中设为要插入的子菜单的菜单句柄。菜单句柄可以通过菜单对象的成员变量m_hMenu获取。然后转换为UINT_PTR类型即可。第四个参数为菜单插入后的名字。(可以新取一个名字,也可以通过CMenu::GetMenuString()去获取原来的名字。
(二)修改菜单:
CMenu::ModifyMenu
BOOL ModifyMenu( UINT nPosition, UINT nFlags, UINT_PTR nIDNewItem = 0, LPCTSTR lpszNewItem = NULL );
1、把菜单项修改成“弹出菜单”为子菜单:
CMenu *pMenu = GetMenu();
pMenu->GetSubMenu(0)->ModifyMenu(
0, //基于0的索引
MF_BYPOSITION | MF_POPUP, //基于位置,插入的类型为MF_POPUP(弹出菜单)
(UINT_PTR)(pMenu->GetSubMenu(1)->m_hMenu), //要插入的菜单的句柄并转换为UINT_PTR
_T("New") //插入的子菜单名称为新建的,不是原来的(也可以保留原来的)
);

注意:修改菜单时,原来的旧菜单项被替换。如上面:FileOne菜单项就被替换为弹出菜单了。
2、修改原来的菜单为“分隔线”:
CMenu *pMenu=GetMenu();
pMenu->GetSubMenu(0)->ModifyMenu(
1,
MF_BYPOSITION|MF_SEPARATOR
);
效果如下:

可以看到,File下面的第2个菜单项FileTwo被替换为了“分隔线”。
3、修改菜单为新的菜单项(或改名或修改ID,或二者兼改)
CMenu *pMenu=GetMenu();
pMenu->GetSubMenu(0)->ModifyMenu(
2,
MF_BYPOSITION|MF_STRING,
0x8002,
_T("NEWITEM")
);
效果图如下:

注意:File第3个菜单项FileThree已经被替换为:NewItem了。同时,其ID值也变了。
(三)删除菜单
CMenu::RemoveMenu
BOOL RemoveMenu( UINT nPosition, UINT nFlags );
CMenu::DeleteMenu
BOOL DeleteMenu( UINT nPosition, UINT nFlags );
这两个函数用法差不多,如下:
pMenu->GetSubMenu(1)->RemoveMenu(0, MF_BYPOSITION);
pMenu->GetSubMenu(1)->DeleteMenu(0, MF_BYPOSITION);
第二个参数如果是MF_BYCOMMAND时,第一个参数就要设置为菜单项的ID了。
两个函数还是有区别的:DeleteMenu不但删除对应的菜单项,如果此项有子菜单,除了菜单项之外,它还要删除子菜单。而
RemoveMenu只删除菜单项,会把子菜单留在内存中。以实际代码为例:
CMenu *pMenu = GetMenu();
pMenu->GetSubMenu(0)->InsertMenuW(0,
MF_BYPOSITION|MF_POPUP,
(UINT_PTR)(pMenu->GetSubMenu(1)->m_hMenu),
_T("New")
);
pMenu->GetSubMenu(0)->DeleteMenu(0, MF_BYPOSITION);
//pMenu->GetSubMenu(0)->RemoveMenu(0, MF_BYPOSITION);
以上代码,当使用DeleteMenu时,程序第二菜单不能正常使用,会显示错误。因为DeleteMenu已经把子菜单Edit从内存中删除了;如果DeleteMenu换成RemoveMenu,则Edit菜单依然可以正常使用。请年效果图:
使用RemoveMenu时:


使用DeleteMenu时:


因为DeleteMenu会从内存中删除Edit子菜单,所以程序立刻出现错误了。
四、菜单的自定义
菜单的自定义是指菜单的外观显示不是由程序自动更新维护的,而是由你自己手动绘制并更新的。
首先,一般的菜单,外观都是文字显示的。其实,菜单也可以以图形显示。
CMenu *pMenu = GetMenu();
CBitmap bitmap;
bitmap.LoadBitmapW(IDB_CAI);
pMenu->GetSubMenu(0)->ModifyMenuW(
0,
MF_BYPOSITION,
ID_FILE_FILEONE,
&bitmap
);
上面的代码,是把菜单File中的第一个菜单项FileOne修改成位图显示。一个名为“IDB_CAI"的位图被显示在原来菜单项的位置。效果图如下:

特别要注意:位图变量bitmap最好定义为框架窗口的成员变量,而不能只定义在窗口的某一个函数中的局部变量。因为一旦bitmap变量的生命周期短于窗口,它的生命周期一结束,菜单将不能正常显示,菜单位置将出现空白显示。
上面的菜单项虽然是图形显示的,但它仍然是由程序自动绘制并更新的。其实菜单的绘制与更新。还可以由你自定义,也就是说,由你自己绘制并更新。这就是菜单的自定义,不管绘制的是文字、符号还是位图,都由你决定并实施。
实现菜单自定义,要通过以下几步才能实现:
(一)第一步:使用ModifyMenu()修改菜单风格,由程序自动绘制变为自己绘制。
在CWinApp::InitInstance()成员函数中修改菜单项(只一项,也可以全部修改)风格为自绘制:MF_OWNERDRAW。
CMenu *pMenu = m_pMainWnd->GetMenu();
pMenu->GetSubMenu(0)->ModifyMenuW(
0,
MF_BYPOSITION | MF_OWNERDRAW,
ID_FILE_FILEONE
);
这样,FileOne这个菜单项将不能自动绘制并显示了,而由你自己处理绘制过程。
(二)第二步:处理WM_MEASUREITEM消息,并在OnMeasureItem消息处理函数中处理与绘制菜单相关的信息。
CWnd::OnMeasureItem
afx_msg void OnMeasureItem( int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct );
菜单的自绘制,首先要通过响应这个消息函数确定经绘制的菜单的宽度和高度值。
在这个消息处理函数中,如果是因菜单自绘制引起的消息,则参数nIDCtl没有用,因为它是响应控件自绘制时使用的。
这个消息处理函数涉及到一个结构:MEASUREITEMSTRUCT结构。其原型为:
typedef struct tagMEASUREITEMSTRUCT {UINT CtlType; UINT CtlID; UINT itemID; UINT itemWidth; UINT itemHeight; ULONG_PTR itemData;
} MEASUREITEMSTRUCT;
其中,CtrlType为消息引发的对象的类型。如果是菜单自绘制引起的,它的值为:ODT_MENU。当然,它还有其它值:
| ODT_BUTTON | Owner-drawn button |
| ODT_COMBOBOX | Owner-drawn combo box |
| ODT_LISTBOX | Owner-drawn list box |
| ODT_LISTVIEW | Owner-draw list view control |
这些值是引发消息的其它对象:如CButton……等。这里暂时不讨论。
如果是菜单自绘制引起的,这个结构中:CtlID和itemData不能使用。
itemID是要绘制的菜单的ID,可以使用。当要自绘制的菜单不止一项时,用它区别不能的菜单项。
这里的重点是:itemWidth和itemHeight两个变量。它告诉我们要绘制的菜单的宽度和高度值。这是绘制菜单必须首先知道的。
怎么确定这个值呢?最好使用以下办法:
lpmis->itemWidth=::GetSystemMetrics(SM_CYMENU)*4;
lpmis->itemHeight=::GetSystemMetrics(SM_CYMENU)*2;//正常时候不需要*2
SM_CYMENU是系统给顶级菜单画的菜单栏的高度。通过在该值的基础上确定自绘制菜单的宽度和高度值,能保证自绘制菜单项与Windows画的菜单项的尺寸比例大致协调。
(三)第三步:也是最后一步,处理WM_DRAWITEM消息,并在OnDrawItem()消息处理函数中完成实际绘制菜单。
CWnd::OnDrawItem
afx_msg void OnDrawItem( int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct );
这个消息处理函数涉及一个结构:DRAWITEMSTRUCT,其原型如下:
typedef struct tagDRAWITEMSTRUCT {UINT CtlType; UINT CtlID; UINT itemID; UINT itemAction; UINT itemState; HWND hwndItem; HDC hDC; RECT rcItem; ULONG_PTR itemData;
} DRAWITEMSTRUCT;
同样,如果消息是菜单自绘制引起的,CtlID和itemData没有用。
itemID仍然为经要绘制的菜单项的ID;
hDC是要绘制的菜单项所在的设备描述表的句柄。
rcItem是包括菜单项所在的矩形的坐标。你要绘制的内容就在此矩形范围内完成的。
hwndItem是包含要绘制的菜单项的菜单的句柄。
itemState是描述要绘制的菜单项的状态:选中还是没选中,有效还是无效……
itemAction是绘制的动作。有ODA_DRAWENTIRE,要求菜单要全部绘制;也可以是ODA_SELECT,绘制菜单选择状态或失去选择状态;ODA_FOCUS,绘制菜单获得焦点或失去焦点的状态。
弄明白以上参数的含义后,我们需要明白:菜单要绘制的内容有哪些?
它应该包括两个内容:一是菜单项本身要显示的内容;二是菜单项的状态信息要表现出来(绘制菜单的状态),比如是否处于选择状态,是否处于高亮状态等等。
假设我们的菜单本身是一个位图,并处于选择和高亮状态,怎样实现绘制呢?代码如下:
BITMAP bm;CBitmap bitmap;bitmap.LoadOEMBitmap(OBM_CHECK);//系统内定义的菜单处于复选状态的位图:勾。bitmap.GetObject(sizeof(bm), &bm);//获得此位图的相关信息给bm:宽度、高度、颜色等。CDC dc;dc.Attach(lpDrawItemStruct->hDC);//从DRAWITEMSTRUCT结构中的成员变量hDC句柄得到CDC//绘制菜单的高亮状态或正常状态CBrush *pBrush = new CBrush(::GetSysColor((lpDrawItemStruct->itemState&ODS_SELECTED) ? COLOR_HIGHLIGHT : COLOR_MENU));//绘制一个矩形框,矩形就是菜单绘制的区域dc.FrameRect(&(lpDrawItemStruct->rcItem), pBrush);delete pBrush;//使用CDC::BitBlt()绘制菜单的“复选”状态。CDC dcMem;dcMem.CreateCompatibleDC(&dc);//构造一个内存CDC,并与菜单的dc相适应。CBitmap *pOldBitmap=dcMem.SelectObject(&bitmap);//把位图选择进dcMem并保存以前的位图dc.BitBlt(lpDrawItemStruct->rcItem.left + 4,lpDrawItemStruct->rcItem.top + (((lpDrawItemStruct->rcItem.bottom -lpDrawItemStruct->rcItem.top) - bm.bmHeight) / 2),bm.bmWidth,bm.bmHeight,&dcMem,0,0,SRCCOPY);dcMem.SelectObject(pOldBitmap);//绘制菜单本身显示的内容,这里它是一个位图:IDB_CAIbitmap.DeleteObject();bitmap.LoadBitmapW(IDB_CAI);bitmap.GetObject(sizeof(bm), &bm);pOldBitmap=dcMem.SelectObject(&bitmap);dc.BitBlt(lpDrawItemStruct->rcItem.left + 4 + bm.bmWidth + 4,lpDrawItemStruct->rcItem.top+1,bm.bmWidth,bm.bmHeight,&dcMem,0,0,SRCCOPY);dcMem.SelectObject(pOldBitmap);dc.Detach();//使用了Attach(),不用时就要Detach()。
效果如下:

特别提醒:复选状态应该根据菜单项自身是否处于复选状态来确定绘制与否。如:
if (lpDrawItemStruct->itemState&ODS_CHECKED) {
}
上述代码没有加上这个条件,是因为菜单还没有加上维护菜单状态的代码,菜单状态无法正确及时地更新,所以直接绘制为复选状态了。正常情况下不应该这样。
到此,菜单的自定义绘制就完成了!!!
五、菜单命令的响应
单击菜单命令时,会产生菜单消息。菜单消息分为两类:
系统菜单:产生WM_SYSCOMMAND消息;
一般菜单和上下文菜单(右键产生的菜单),产生的是WM_COMMAND消息。
为了执行菜单命令,需要映射菜单产生的消息及消息处理函数。比如:
消息映射:ON_COMMAND(ID_FILE_FILEONE, &CMainFrame::OnFileFileone)
第一个参数为菜单命令的ID,第二个为消息处理函数。
消息处理函数:void OnFileFileone() 它没有参数。然后在消息处理函数中处理消息产生后要完成的工作。
系统菜单的消息映射为:ON_WM_SYSCOMMAND()
系统消息处理函数:void OnSysCommand(UINT nID, LPARAM lParam)
参数nID是命令的类型,为以下值之一:
SC_CLOSE Close the CWnd object.
SC_HOTKEY Activate the CWnd object associated with the application-specified hot key. The low-order word of lParam identifies the HWND of the window to activate.
SC_HSCROLL Scroll horizontally.
SC_KEYMENU Retrieve a menu through a keystroke.
SC_MAXIMIZE (or SC_ZOOM) Maximize the CWnd object.
SC_MINIMIZE (or SC_ICON) Minimize the CWnd object.
SC_MOUSEMENU Retrieve a menu through a mouse click.
SC_MOVE Move the CWnd object.
SC_NEXTWINDOW Move to the next window.
SC_PREVWINDOW Move to the previous window.
SC_RESTORE Restore window to normal position and size.
SC_SCREENSAVE Executes the screen-saver application specified in the [boot] section of the SYSTEM.INI file.
SC_SIZE Size the CWnd object.
SC_TASKLIST Execute or activate the Windows Task Manager application.
SC_VSCROLL Scroll vertically.
菜单命令是鼠标选择时,参数lParam鼠标的x,y坐标。低阶字包含x坐标,高阶字包含y坐标。否则,不使用此参数。
一个菜单命令映射一个函数,但如果几个菜单命令之间有关联,它们的ID号也是相连的,这种情况也可以把这几个命令映射到同一个函数来处理。如:
ON_COMMAND(ID_FILE_FILEONE,OnFile)
ON_COMMAND(ID_FILE_FILETWO,OnFile)
ON_COMMAND(ID_FILE_FILETHREE,OnFile)
它们共同的消息处理函数为:
void OnFile() 函数没有参数。
那如何分别是哪一个命令产生的消息呢?这个就要借助CWnd::GetCurrentMessage 函数了:
UINT nID=(UINT)LOWORD(GetCurrentMessage()->wParam);
然后根据菜单ID与nID比较,就能知道是哪个命令产生的消息了。
上面这种情况,还有更好的解决方案。
ON_WM_COMMAND_RANGE(ID_FILE_FILEONE,ID_FILE_FILETHREE,OnFile)
其映射的函数为:
void OnFile(UINT nID),其中,nID为命令消息产生的菜单ID。根据此ID在消息函数中完成需要完成的任务。
还有一种情况比较特殊:当程序在上下文中使用的弹出菜单为系统菜单时,单击菜单项产生的消息不是期望的:
WM_SYSCOMMAND消息,而是WM_COMMAND消息。怎么办呢?
void CMainWindow::OnContextMenu (CWnd* pWnd, CPoint point)
{
CRect rect;
GetClientRect (&rect);
ClientToScreen (&rect);
if (rect.PtInRect (point)) {
CMenu* pMenu = GetSystemMenu (FALSE); //获取系统菜单
UpdateSystemMenu (pMenu);
int nID = (int) pMenu->TrackPopupMenu (TPM_LEFTALIGN |
TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_RETURNCMD, point.x,
point.y, this);
if (nID > 0)
SendMessage (WM_SYSCOMMAND, nID, 0);
return;
}
CFrameWnd::OnContextMenu (pWnd, point);
}
借助SendMessage()或PostMessage()实现WM_SYSCOMMAND消息的产生。
下面这个函数是手动更新系统菜单命令的内容:因为系统菜单变为弹出菜单时,其状态不能及时更新。
要进行以下设置,需要先设:
CFrameWnd::m_bAutoMenuEnable =FALSE;不让程序自动更新菜单的状态。
void CMainWindow::UpdateSystemMenu (CMenu* pMenu)
{
static UINT nState[2][5] = {
{ MFS_GRAYED, MFS_ENABLED, MFS_ENABLED,
MFS_ENABLED, MFS_DEFAULT },
{ MFS_DEFAULT, MFS_GRAYED, MFS_GRAYED,
MFS_ENABLED, MFS_GRAYED }
};
if (IsIconic ()) // Shouldn't happen, but let's be safe
return;
int i = 0;
if (IsZoomed ())
i = 1;
CString strMenuText;
pMenu->GetMenuString (SC_RESTORE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_RESTORE, MF_STRING | nState[i][0], SC_RESTORE,
strMenuText);
pMenu->GetMenuString (SC_MOVE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_MOVE, MF_STRING | nState[i][1], SC_MOVE,
strMenuText);
pMenu->GetMenuString (SC_SIZE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_SIZE, MF_STRING | nState[i][2], SC_SIZE,
strMenuText);
pMenu->GetMenuString (SC_MINIMIZE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_MINIMIZE, MF_STRING | nState[i][3], SC_MINIMIZE,
strMenuText);
pMenu->GetMenuString (SC_MAXIMIZE, strMenuText, MF_BYCOMMAND);
pMenu->ModifyMenu (SC_MAXIMIZE, MF_STRING | nState[i][4], SC_MAXIMIZE,
strMenuText);
SetMenuDefaultItem (pMenu->m_hMenu, i ? SC_RESTORE :
SC_MAXIMIZE, FALSE);
}
六、菜单命令的状态更新与维护
首先应该明白菜单命令的状态有哪些?菜单命令状态的变化与什么因素相关?改变菜单状态变化的方法有哪些?
(一)菜单的状态:
1、有效或无效状态;
2、选择或未选择状态;
3、复选或未复选状态;
4、单选或未单选状态;
(二)菜单状态变化的因素:
1、菜单是否有效:
如果CFrameWnd::m_bAutoMenuEnable =TRUE,程序内部有自动更新机制。如:菜单没有自动更新函数ON_UPDATE_COMMAND_UI(),菜单会自动无效。
如果CFrameWnd::m_bAutoMenuEnable =FALSE,菜单是否有效,需要根据程序的要求,手动更新。
以系统菜单为例:当框架窗口最大化时,系统菜单的:最大化、大小、移动等三个命令应该处于“无效”状态;此时,“恢复”命令应该处于“有效”状态;当不是最大化时,“恢复”命令应该无效,其它所有命令都是“有效”的!如图:
因此,菜单命令的有效与否,除部分由系统决定,自动更新以外,更多时候由程序根据自己的需要,设置有效与否的条件,然后根据此条件手动更新。
2、是否处于选择状态:
当鼠标指向某个菜单项命令时,当前菜单项就是处于:选择状态;其它菜单命令就是未选择状态。当鼠标从当前菜单命令移开并指向另一个菜单命令时,被指向的菜单处于选择状态,其它所有菜单命令处于未选择状态。
3、是否处于复选状态:
是否复选状态,完全取决于程序自己的要求以及根据要求设置的条件来决定。复选从含义上说,处于“多选”状态,就是有几个命令同时处于选择状态(与选择不是上述的鼠标选择状态)。
4、是否处于单选状态:
有些时候,几个菜单命令之间是相关联的,但其中只能选择一个,此时就是“单选状态”确定的时候。比如:当我们从菜单选择不同的颜色命令时,不可能同时有多种颜色被选择,只能是任选其一。
(三)改变菜单状态的方法:
1、菜单的有效与否:
(1)由系统自动更新(缺省的)
(2)由程序手动更新
CFrameWnd::m_bAutoMenuEnable =FALSE时。
具体更新的方法有以下几种:
CMenu::EnableMenuItem
UINT EnableMenuItem( UINT nIDEnableItem, UINT nEnable );
如以下代码:
BOOL SimpleApp::InitInstance()
{
// TODO: 在此添加专用代码和/或调用基类
m_pMainWnd = new CMainFrame();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
CMenu* pMenu = ((CMainFrame *)m_pMainWnd)->GetMenu();
pMenu->GetSubMenu(0)->EnableMenuItem(ID_FILE_FILEONE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
return CWinApp::InitInstance();
}
下面这个函数用于WM_UPDATE_COMMAND_UI消息处理函数中。
CCmdUI::Enable
virtual void Enable( BOOL bOn = TRUE );
如以下代码:
void CMainFrame::OnUpdateFileFileone(CCmdUI *pCmdUI)
{
// TODO: 在此添加命令更新用户界面处理程序代码
pCmdUI->Enable(FALSE);
}
还有一个方法也能修改菜单是否有效
CMenu::ModifyMenu
BOOL ModifyMenu( UINT nPosition, UINT nFlags, UINT nIDNewItem = 0, LPCTSTR lpszNewItem = NULL );
BOOL ModifyMenu( UINT nPosition, UINT nFlags, UINT nIDNewItem, const CBitmap* pBmp );
如果有以下代码:
CMenu *pMenu = m_pMainWnd->GetMenu();
CString strMenu;
pMenu->GetSubMenu(0)->GetMenuStringW(0, strMenu, MF_BYPOSITION);
pMenu->GetSubMenu(0)->ModifyMenuW(
0,
MF_BYPOSITION|MFS_DISABLED ,
ID_FILE_FILEONE,
strMenu
);
以上几种代码,结果都如下图,File菜单下的第一个命令项就变为“不可用”状态了。

2、菜单是否处于被选择状态:
这个状态,一般情况下是由程序自动更新的,不需要去处理它。
但如果菜单处于MF_OWERDRAW风格,在响应WM_DRAWITEM消息时,菜单的选择状态需要程序在消息处理函数OnDrawItem()中手动绘制的。如以下代码:
void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CDC dc;
dc.Attach(lpDrawItemStruct->hDC);//从DRAWITEMSTRUCT结构中的成员变量hDC得到CDC
//绘制菜单的高亮状态或正常状态
CBrush *pBrush = new CBrush(
::GetSysColor(
(lpDrawItemStruct->itemState&ODS_SELECTED) ? COLOR_HIGHLIGHT : COLOR_MENU
)
);
dc.FrameRect(&(lpDrawItemStruct->rcItem), pBrush);
delete pBrush;
}
3、菜单是否处于复选状态:
在visual studio编程软件中,在资源视图中编辑菜单时,可以选择菜单命令处于“复选”状态。如下图:

另外:
CMenu::CheckMenuItem
UINT CheckMenuItem( UINT nIDCheckItem, UINT nCheck );
也是可以设置“复选”状态的。
CMenu* pMenu = ((CMainFrame *)m_pMainWnd)->GetMenu();
pMenu->GetSubMenu(1)->CheckMenuItem(ID_FILE_FILEONE, MF_BYCOMMAND | MFS_CHECKED);
同样,
CMenu::ModifyMenu
BOOL ModifyMenu( UINT nPosition, UINT nFlags, UINT nIDNewItem = 0, LPCTSTR lpszNewItem = NULL );
BOOL ModifyMenu( UINT nPosition, UINT nFlags, UINT nIDNewItem, const CBitmap* pBmp );
也是可以修改复选状态的:
BOOL SimpleApp::InitInstance()
{
// TODO: 在此添加专用代码和/或调用基类
m_pMainWnd = new CMainFrame();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
CMenu *pMenu = m_pMainWnd->GetMenu();
CString strMenu;
pMenu->GetSubMenu(0)->GetMenuStringW(0, strMenu, MF_BYPOSITION);
pMenu->GetSubMenu(0)->ModifyMenuW(
0,
MF_BYPOSITION|MFS_CHECKED ,
ID_FILE_FILEONE,
strMenu
);
return CWinApp::InitInstance();
}
还有一个更新复选状态的地方:WM_UPDATE_COMMAND_UI消息产生时,在其消息处理函数中也可以设置“复选”状态:
void CMainFrame::OnUpdateFileFileone(CCmdUI *pCmdUI)
{
// TODO: 在此添加命令更新用户界面处理程序代码
pCmdUI->SetCheck(TRUE);
}
如果菜单是自绘制的,那么在响应WM_DRAWITEM消息的消息处理函数OnDrawItem()中,需要程序手动绘制菜单的复选状态的:
void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
BITMAP bm;
CBitmap bitmap;
bitmap.LoadOEMBitmap(OBM_CHECK);//系统内定义的菜单处于选择状态的位图:勾。
bitmap.GetObject(sizeof(bm), &bm);//获得此位图的相关信息给bm:宽度、高度、颜色等。
CDC dc;
dc.Attach(lpDrawItemStruct->hDC);//从DRAWITEMSTRUCT结构中的成员变量hDC得到CDC
//使用CDC::BitBlt()绘制菜单的“复选”状态。
CDC dcMem;
dcMem.CreateCompatibleDC(&dc);//构造一个内存CDC,并与菜单的dc相适应。
CBitmap *pOldBitmap = dcMem.SelectObject(&bitmap);//把位图选择进dcMem并保存以前的位图
if (lpDrawItemStruct->itemState&ODS_CHECKED) {
dc.BitBlt(
lpDrawItemStruct->rcItem.left + 4,
lpDrawItemStruct->rcItem.top + (((lpDrawItemStruct->rcItem.bottom - lpDrawItemStruct->rcItem.top) - bm.bmHeight) / 2),
bm.bmWidth,
bm.bmHeight,
&dcMem,
0,
0,
SRCCOPY
);
dcMem.SelectObject(pOldBitmap);
}
4、菜单是否处于单选状态:
菜单处于单选状态时,没有一个系统定义的常量代号与之对应。也就是说,菜单处于复选时,对应的状态应该是MF_CHECKED
其值为8;处于单选状态时,其值是520,即十六进制的0x208。怎么得来的呢?看看系统定义的常量:
#define MF_UNCHECKED 0x00000000L
#define MF_CHECKED 0x00000008L
#define MF_USECHECKBITMAPS 0x00000200L
可以看出:单选状态时,是MF_CHECKED|MF_USECHECKBITMAPS=0x208=520。
因此判断是否处于单选状态应该依据状态值是否=MF_CHECKED|MF_USECHECKBITMAPS。
怎样才能设置单选状态呢?
CMenu::CheckMenuRadioItem
BOOL CheckMenuRadioItem( UINT nIDFirst, UINT nIDLast, UINT nIDItem, UINT nFlags );
CCmdUI::SetRadio
virtual void SetRadio( BOOL bOn = TRUE );
以上两个函数都能设置“单选”状态。
![Windows 编程[18] - 更换菜单项: ModifyMenu](http://static.oschina.net/uploads/img/201409/26153645_SGEV.gif)















