[MFC]利用CMenu手工(非资源脚本)处理菜单、系统菜单

时间:2023-01-15 05:01:33

1. 创建菜单:

    1) 主要利用CMenu的三个成员函数CreateMenu、CreatePopupMenu、AppendMenu、SetMenu创建菜单;

    2) BOOL CMenu::CreateMenu();

         i. 该函数可以直接创建一个空的顶层菜单栏;

         ii. 返回值表示创建是否成功;

         iii. 该创建只是内存里的资源的创建,它在创建顶层菜单栏资源的同时将该资源与CMenu对象挂钩起来:

CMenu menuMain;
menuMain.CreateMenu(); // 创建了一个空的顶层菜单栏并将该资源和CMenu对象menuMain挂钩
    3) BOOL CMenu::CreatePopupMenu();

         i. 该函数也是创建一个菜单,返回值意义等也和CreateMenu一样;

         ii. 只不过该函数创建的是一个可弹出的顶层菜单项,同样也是空的,里面没有内容;

    4) BOOL CMenu::AppendMenu(UINT nFlags, UINT nIDNewItem = 0, LPCTSTR lpszNewItem = NULL);

         i. 用于在菜单的末尾处添加一个菜单项;

         ii. nFlags:表示添加的项目的类型,以MF_前缀打头,即Menu Flag的缩写,主要有这几项:

MF_STRING:表示加入的是一个普通的具有命令的命令菜单项,此时第三个参数就是该惨淡项上的文本,而第二个参数就是该菜单项的ID;

MF_POPUP:表示加入的是一个顶层菜单项,此时第三个参数就表示该顶层菜单项的文本,但是要注意的是!第二个参数就变成了该顶层菜单项的句柄了!!但是由于第二个参数在原型中已经定义为了UINT型,所以还要对菜单句柄进行强制类型转换,转换成UINT型再传入;

MF_SEPARATOR:表示添加的是一条分界线,此时第二个参数和第三个参数就没意义了,直接取默认值0和NULL即可;


!!注意菜单资源的挂接和挂钩:

        a. 挂接是指将顶层菜单栏接在框架窗口上,以及将顶层菜单项接在顶层菜单栏上的过程;

        b. 挂钩:也叫绑定,在过去没有面向对象模式之前,菜单资源被创建在内存后特别不方便管理,需要用很多函数去维护,而现在有了CMenu类,就可以将原来一大堆的不方便全部都包装在CMenu对象中了,用户可以不必去面对这些底层的复杂性,因此就叫做菜单资源绑定在CMenu对象中,或者说是菜单资源和CMenu对象挂钩;

        c. 通常第一步都是创建资源并挂钩,然后顶层菜单栏通过SetMenu函数挂接到框架窗口上,而顶层菜单项通过AppendMenu挂接到顶层菜单栏中;

        d. 但是MFC存在一个大问题,那就是挂钩到CMenu对象中的菜单资源会随着CMenu对象超出作用域而和对象一起被释放掉,这通常不符合正常的需求,因为菜单栏必须一直存在于框架窗口中,直到整个程序被关闭为止,所以在完成挂接动作后一定要解除CMenu对象和菜单资源的绑定(解绑),这样就不会因为CMenu对象被释放而导致菜单资源也被释放了,解绑的函数就是Detach:HMENU CMenu::Detach();

!!该函数能做到真正的解绑的原理在于它将CMenu内部维护的数据成员m_hMenu赋值为NULL,并将解除绑定的菜单资源的句柄作为返回值返回;

!因此利用AppendMenu挂接顶层菜单项时可以这样调用:menuMain.AppendMenu(MF_POPUP, (UINT)menuPopup.Detach(), "&File");

!此时第二个参数刚好就是顶层菜单项的句柄,同时也完成了解绑;


    5) BOOL CWnd::SetMenu(CMenu* pMenu);

         i. 可以看到该函数属于CWnd,就是用于将pMenu做代表的顶层菜单栏挂接到窗口上;

         ii. 返回值表示挂接是否成功;

         iii. 注意!该函数调用完毕后也要对顶层菜单栏的CMenu对象进行解绑,SetMenu并不会替你完成解绑的!!

    6) 示例:手工建立一个和Shapes程序中一样的菜单栏

CMenu menuMain;
menuMain.CreateMenu(); // 创建并绑定顶层菜单栏与对象中

CMenu menuPopup; // 顶层菜单项,即可弹出子菜单的菜单项
menuPopup.CreatePopupMenu(); // 创建并绑定
menuPopup.AppendMenu(MF_STRING, ID_FILE_EXIT, "E&xit"); // 该子菜单中添加一个“退出”菜单项
menuMain.AppendMenu(MF_POPUP, (UINT)menuPopup.Detach(), "&File"); // 将popup顶层菜单项挂接到main菜单栏上

menuPopup.CreatePopupMenu(); // 由于已经Detach过了,所以又能创建新的菜单资源了
menuPopup.AppendMenu(MF_STRING, ID_SHAPE_CIRCLE, "&Circle\tF7");
menuPopup.AppendMenu(MF_STRING, ID_SHAPE_TRIANGLE, "&Triangle\tF8");
menuPopup.AppendMenu(MF_STRING, ID_SHAPE_SQUARE, "&Square\tF9");
menuMain.AppendMenu(MF_POPUP, (UINT)menuPopup.Detach(), "&Shape");

SetMenu(&menuMain); // 挂接顶层菜单栏
menuMain.Detach(); // 解绑顶层菜单栏
!!如果在SetMenu的时候窗口就已经被显示出来了,则需要调用DrawMenuBar将菜单栏画出来:void CWnd::DrawMenuBar();


2. 修改菜单:

    1) 要修改菜单一定要就一定要现获取菜单,由于菜单在挂接后统统都解绑了,所以必须通过某种手段来获取,那就是GetMenu函数:CMenu* CWnd::GetMenu() const;

         i. 该函数获得是顶层菜单栏的指针;

         ii. 菜单栏中所有的子菜单和菜单项都能通过这个指针检索到;

    2) 添加菜单项:还是使用AppendMenu;

    3) 删除菜单项:有两个,一个是DeleteMenu还有一个是RemoveMenu

         i. BOOL CMenu::Delete(UINT nPosition, UINT nFlags);

         ii. BOOL CMenu::Remove(UINT nPosition, UINT nFlags);

!返回值都表示是否调用成功;

!两者最大的区别就是,如果删除的是一个子菜单(即顶层菜单项),Delete会将子菜单中的菜单项也删除,而Remove则不删除子菜单里的菜单项,如果菜单项有所保留有待后用,则时候Remove,但通常Delete使用得更多;

!nPosition表示目标菜单项的位置,而nFlags则决定nPosition是怎么定义,如果是MF_BYCOMMAND,则nPosition就是菜单项的ID,如果是MF_BYPOSITION,则nPosition就是菜单项的0级索引;

!0级索引就是从0开始计的索引号,既然菜单项ID可以精确定位一个菜单项那为什么还要索引号呢?原因很简单,那些具有菜单命令的菜单项有ID,而那些没有命令的菜单项是没有ID的,像顶层菜单项就没有ID,既然没有ID就肯定不能通过ID来检索了,因此MFC就想出了这么一个0级索引,在顶层菜单栏中顶层菜单项从左到右索引号从0递增,而在每个子菜单中,各个菜单项的索引号按照从上到下的顺序从0递增;

!!所以,如果要删除顶层菜单项,就必须获得顶层菜单栏的指针,而如果要删除子菜单中的菜单项,则需要得到子菜单(即顶层菜单项)的指针,因为0级索引是按层次级别划分的,而不是统一划分的;

    4) 那怎样获取子菜单的指针呢?第一步肯定是先获取顶层菜单栏的指针(GetMenu),然后在调用顶层菜单栏的GetSubMenu来获取子菜单的指针就行了:

         i. 函数原型:CMenu* CMenu::GetSubMenu(int nPos) const;

         ii. nPos是0级索引,返回的是子菜单的指针;

         iii. 调用示例:

CMenu* pSubMenu = GetMenu()->GetSubMenu(1);
pSubMenu->DeleteMenu(2, MF_BYPOSITION);
!起始如果删除的时候是按照ID来检索的,则通过顶层菜单栏的指针也能直接删除的!

!通过ID检索的都能通过上级或更上级的那些菜单指针来检索!

!但是0级索引就不行,因为0级索引是按层级排列的;

    5) 修改菜单:

         i. 使用ModifyMenu来修改:BOOL CMenu::ModifyMenu(UINT nPosition, UINT nFlags, UINT nIDNewItem = 0, LPCTSTR lpszNewItem = 0);

         ii. 返回表示是否成功;

         iii. nPosition是0级索引还是ID有nFlags决定;

         iv. 如果要修改菜单项的文本,则需要在nFlags中OR一个MF_STRING,然后lpszNewItem就是新的文本;

         v. nIDNewItem是新的ID,一般情况下不用改变;

         vi. 如果修改的是顶层菜单项,则nFlags里OR的是MF_POPUP,那lpszNewItem就是新顶层菜单项的文本了,但第三个参数就是新子菜单的句柄了!!和AppendMenu其实是如出一辙的,因此该句柄还需要用(UINT)强制转换一下;

         vii. 示例:pMenu->ModifyMenu(2, MF_STRING | MF_BYPOSITION, ID_SHAPE_TRIANGLE, _T("&Three-Sided-Polygon"));


3. 系统菜单:

    1) 就是程序窗口左上角点击图标后弹出的菜单,里面有一些退出、最大化、最小化等菜单项;

    2) 可能大家都只满足于让Windows自己操作该菜单而不去管它,但有时却非常有用,比如你的程序只是一个实用小程序,没有菜单栏,但是又需要有一个About MyApp的菜单项来介绍自己这款App或是声明版权等,这时就没必要只为了这么一个按钮还大费周折添加一个菜单栏,这时就可以把该菜单项加入系统菜单中了;

    3) 操作系统菜单第一步还是需要先得到系统菜单的指针:CMenu* CWnd::GetSystemMenu(BOOL bRevert) const;

!Revert表示可恢复的,这是什么意思呢?因为系统菜单中提供的是最基本的功能,MFC当然已经定义过了这些最基本的功能(最大化、最小化、退出等),如果用户自定义时混乱了,导致MFC预设的基本功能被破坏或丢失那肯会带来不好的结局,因此MFC将最原始的系统菜单保存为一个模板,如果想自定义,则传bRevert = FALSE得到一个模板的副本,然后随意在模板上修改,修改后的版本作为程序的系统菜单,但是如果认为修改的不好,就可以再调一遍该函数,bRevert穿TRUE,则将再次得到不可修改的模板样本重回原始状态,所以要自定义一定要传FALSE;

    4) 接下来添加和删除菜单项就和普通菜单一样了,都是AppendMenu、DeleteMenu、ModifyMenu等;

    5) 注意!系统菜单中的菜单项除了MF_SEPARATOR外都必须具有ID,并且!系统菜单项ID的低4位是Windows保留用的,因此你取的ID必须都是16的倍数,如果低4位你占用了可能会发生意想不到的后果:

ON_WM_SYSCOMMAND() // 从SYSCOMMAND到OnSysCommand的映射MFC预定义好了,因此不需要宏参数

void CWindow::OnSysCommand(UINT nID, LPARAM lParam)
{ // 系统菜单中各菜单项的命令映射不能用户逐个自己定义,只能统一用给一个ON_WM_SYSCOMMAND定义
  // 因此nID就用来标识系统菜单项的ID了
	switch (nID & 0xFFF0) // 一定要清除掉Windows在后四位中添加的标记!!
	{
	case ID_SYSMENU_ABOUT_MYAPP:
		...
		return ;
	case ID_SYSMENU_MYCOMMAND_1:
		...
		return ;
	case ...
		...
	default: // 其余默认菜单项交由基类函数处理,该消息要在主框架窗口中响应!!
		CFrameWnd::OnSysCommand(nID, lParam);
	}
}

4. 使菜单项无效:

    1) 这个可能都明白,只要使用Unable、Greyed等函数就可以让菜单项失效,但这里还需要介绍几个常用的方法;

    2) 只要不给菜单项定义消息映射和消息响应函数Windows就会自动让菜单项无效(并且显示时是灰化的);

    3) 使系统菜单中预定义好的菜单项无效:

         i. 使用Enable等函数就不用说了,这里介绍一种更加高效美观的方法;

         ii. 在OnSysCommand中添加逻辑控制即可:

void CWindow::OnSysCommand(UINT nID, LPARAM lParam)
{ 
	if ((nID & 0xFFF0) != SC_CLOSE)
		CFrameWnd::OnSysCommand(nID, lParam);
}
!系统预定义菜单项以SC_打头,即System Command的缩写,因此自定义的时候也建议使用这种规范的写法;

         iii. 传统方法也展示一下好了:

CMenu* pMenu = GetSystemMenu(FALSE);
pMenu->EnableMenuItem(SC_CLOSE, MF_BYCOMMAND | MF_DISABLE);
pMenu->Delete(SC_CLOSE, MF_BYCOMMAND);
!一般只有流氓软件才会这么干,去关掉“关闭”按钮,平时不要这么干。。

!!修改、添加系统菜单项最好都在主框架窗口的OnCreate函数中进行!!