基础语法篇_5——菜单命令响应函数、菜单命令的路由、基本菜单操作、动态菜单操作、电话本实例

MFC 同时被 2 个专栏收录
35 篇文章 9 订阅
84 篇文章 5 订阅

🔳🔳 绘制线条 、画刷绘图、绘制连续线条、绘制扇形效果的线条


🔳🔳 插入符【文本插入符|图形插入符】、窗口重绘、路径、字符输入【设置字体|字幕变色】


🔳🔳 菜单命令响应函数、菜单命令的路由、基本菜单操作、动态菜单操作、电话本实例


🔳🔳 对话框的创建与显示、动态创建按钮、控件的访问【控件调整|静态文本控件|编辑框控件】、对话框伸缩功能、输入焦点的传递、默认按钮的说明


🔳🔳 MFC对话框:逃跑按钮、属性表单、向导创建


🔳🔳 在对话框程序中让对话框捕获WM_KEYDOWN消息


🔳🔳修改应用程序窗口的外观【窗口光标|图标|背景】、模拟动画图标、工具栏编程、状态栏编程、进度栏编程、在状态栏上显示鼠标当前位置、启动画面


🔳🔳设置对话框、颜色对话框、字体对话框、示例对话框、改变对话框和控件的背景及文本颜色、位图显示


菜单栏、工具栏和状态栏是组成Windows程序图形界面的三个主要元素。


先创建一个单文档的应用程序。

一、菜单命令响应函数

运行上面创建的应用程序,可以发现MFC已经帮我们创建了一些菜单。

点击资源视图选项卡,可以看到Menu菜单下的一个名为IDR_MAINFRAME的菜单资源。

这是MFC AppWizard为Menu单文档程序自动创建的一个主菜单。双击这个菜单资源名称,即可在VC++开发界面右边窗格中打开菜单编辑器。

若我们想为自己的程序添加自己的菜项,可以在这个菜单中直接添加。

对于刚刚创建的打开菜单项,它的ID是不可编辑的。用鼠标分别点击
文件编辑视图帮助菜单项,发现它们的ID项都是灰色,不可编辑。当观察编辑的子菜单项撤销时,发现ID是可以进行编辑的。

仔细观察,发现每个主菜单项属性中的PopUp选项都是True状态。而子菜单的PopUp状态是False状态。MFC中,把PopUp设置为True的菜单称为弹出式菜单,VC++默认顶层菜单为弹出式菜单,这种菜单不能响应命令。但是,当顶层菜单的PopUp设置为False时,该菜单就不在是弹出式菜单,就能够响应命令,设置ID了。


下面为打开菜单添加命令响应,其ID设置为ID_32771

  • 方法一:在此菜单上单击鼠标右键,点击添加事件处理程序按钮。

    类列表选择CMainFrame,消息类型选择COMMAND,函数名一般以On开头。

  • 方法二:右键项目,点击类向导选择类名为CMainFrame,搜索打开菜单的ID号,选中并选中消息类型为COMMAND,点击添加处理程序,编辑函数名称。

在响应函数中添加代码如下:

void CMainFrame::OnOpen()
{
	// TODO: 在此添加命令处理程序代码
	MessageBox(_T("MainFrame  Clicked"));
}

二、菜单命令的路由

2.1 程序类对菜单命令的响应顺序

是不是菜单项命令只能由CMainFrame这个类来捕获呢?可以打开类向导,在该对话框的对象ID (Object IDs)列表中选择ID_32771,在Class name下拉列表框中选择除CMainFrame之外的其他类,并添加MessageBox函数。由于CMenuDoc和CMenuApp都不是从Wnd类派生的,所以没有MessageBox函数,需要使用全局MessageBox函数或者使用应用程序框架的函数:AfxMessageBox,声明如下:

int AfxMessageBox(LPCTSTR lpszText,UINT nType=MB_OK,UINT nIDHelp=0)
  • CMenuApp
    在这里插入图片描述
    在这里插入图片描述

    void CMenuApp::OnOpen()
    {
    	// TODO: 在此添加命令处理程序代码
    	AfxMessageBox(_T("CMenuApp Open"));
    }	
    
  • CMenuDoc

    void CMenuDoc::OnOpen()
    {
    	// TODO: 在此添加命令处理程序代码
    	AfxMessageBox(_T("CMenuDoc  Clicked"));
    }
    
  • CMenuView

    void CMenuView::OnOpen()
    {
    	// TODO: 在此添加命令处理程序代码
    	MessageBox(_T("CMenuView  Clicked"));
    }
    

打开这个菜单项就有了四个命令响应函数,那么在程序运行时,当单击这个打开菜单项后,应该由哪个函数来响应呢?还是这四个函数都会响应?

运行程序,发现弹出的信息是“CMenuView Clicked”。其他响应函数都未起作用。
1)删除视类CMenuView的OnOpen函数删除,再次运行程序。文档类做出响应。


2)删除文档类CMenuDoc的OnOpen函数删除,再次运行程序。文档类做出响应。


3)删除框架类CMainFrame的OnOpen函数,再次运行程序。应用程序类做出响应。


由此可以看出响应打开菜单项命令的顺序依次为:视类->文档类->框架类->应用程序类

2.2 Windows消息的分类

菜单命令也是一种消息。Windows消息分成以下三种:
1)⭕标准消息
WM_COMMEND之外,所有以WM_开头的消息都是标准消息。从CWnd派生的类都可以接收此类消息。

2)⭕命令消息
来自菜单、加速键、或者工具栏按钮的消息。这类消息以WM_COMMAND形式呈现MFC中,通过菜单项的标识(ID)来区分不同的命令消息;在SDK中,通过消息的wParam参数识别。CCmdTaget派生的类,都可以接收到此类消息。

3)⭕通告消息
由控件产生的消息,例如按钮的单击、列表框的选择等都会产生此类消息,目的是:向父窗口(通常指对话框)通知事件的发生。此类消息也以WM_COMMAND形式呈现。 从CCmdTaget派生的类,都可以接收到此类消息。


CWnd类实际上派生于CCmdTarget类。也就是说,凡是从CWnd派生的类,它们既可以接收标准消息,也可以接收命令消息和通告消息。而对于那些从CCmdTarget派生的类,则只能接收命令消息和通告消息,不能接收标准函数

2.3 菜单命令的路由

Menu程序视类添加Open菜单项的命令响应函数。添加后,程序变化的有三个地方:
🔲 视类头文件中,两个AFX_MSG注释宏之间添加了命令消息响应函数原型。

🔲 视类源CPP文件两处改变:
     ◼两个AFX_MSG_MAP注释宏之间添加了ON_COMMEND宏,将菜单ID与命令响应函数关联起来。

BEGIN_MESSAGE_MAP(CMenuView, CView)
	// 标准打印命令
	ON_COMMAND(ID_FILE_PRINT, &CView::OnFilePrint)
	ON_COMMAND(ID_FILE_PRINT_DIRECT, &CView::OnFilePrint)
	ON_COMMAND(ID_FILE_PRINT_PREVIEW, &CMenuView::OnFilePrintPreview)
	ON_WM_CONTEXTMENU()
	ON_WM_RBUTTONUP()
	//新增
    ON_COMMAND(ID_32771, &CMenuView::OnOpen)
    
END_MESSAGE_MAP()

     ◼命令消息响应函数的实现代码。

void CMenuView::OnOpen()
{
	// TODO: 在此添加命令处理程序代码
}

菜单命令消息响应函数的映射与标准消息的映射是一样"的,只是命令消息使用的是ON_COMMAND宏。不过,命令消息和标准消息的路由过程还是有所区别的:

MFC在后台把窗口过程函数替换成了AfxWndProc函数,由这个函数对所有的消息进行处理。该函数内部将调用AfxCallwndProc函数。后者又将调用WindowProc函数,这是CWnd类的一个成员函数,应用程序所有类型的消息都会进入到这个函数中。WindowProc函数又将调用OnWndMsg函数,这个函数会对到来的消息进行一个类型判断,如果是标准消息,就利用消息映射机制来查找是哪个类响应了当前这个消息,并调用相应的消息映射函数,完成对消息的处理:如果是命令消息,它就会交由OnCommand这个函数来处理,在这个函数中将完成命令消息的路由;如果是通告消息,那么它将交由OnNotify这个函数来处理,该函数将完成通告消息的路由。二者最后都会调用OnCmdMsg函数。

以Menu程序为例,菜单命令消息路由的具体过程:
◼ 当点击某个菜单项时,最先接收到这个菜单命令消息的是框架类。
◼ 框架类将把接收到的这个消息交给它的子窗口,即视类,由视类首先进行处理。
◾◾◾ 视类首先根据命令消息映射机制查找自身是否对此消息进行了响应,如果响应了就调用相应响应函数对这个消息进行处理,消息路由过程结束。
◾◾◾ 如果视类没有对此命令消息做出响应,就交由文档类。文档类同样查找自身是否对这个菜单命令进行了响应,如果响应了,就由文档类的命令消息响应函数进行处理,路由过程结束。
◾◾◾ 如果文档类也未做出响应,就把这个命令消息交还给视类。视类在此把该消息交还给框架类。
◼ 框架类查看自己是否对这个命令消息进行了响应,如果它也没有做出响应,就把这个莱单命令消息交给应用程序类,由后者来进行处理。

三、基本菜单操作

3.1 标记菜单

运行Menu程序后,发现程序视图子菜单下的状态栏菜单项前面带有一个对号(√),称此类型的菜单为标记菜单

现在在视图子菜单下的应用程序外观菜单项上添加一个标记。由于主菜单属于框架窗口,所以需要在框架类窗口创建完成之后再去访问菜单对象。在框架类(CMainFrame类)OnCreate函数的最后添加实现这个功能的代码。

✨1) 为获取视图子菜单下的应用程序外观菜单项,首先获取程序的菜单项,在框架窗口中获得指向菜单栏的指针。
        通过CWnd的成员函数:GetMenu函数实现。声明如下:

CMenu*  GetMenu()  const;

        函数返回一个指向CMenu类对象的指针。CMenu类是一个MFC类,提供了一些与菜单操作有关的成员函数,如菜单的创建,更新和销毁等,还可以获取一个菜单的子菜单。通过GetSubMenu成员函数实现。函数声明如下:

CMenu* GetSubMenu(int nPos) const;

        函数参数nPos,该参数指定了子菜单的索引号。此函数也返回一个指向CMenu对象的指针,但是这个函数返回值所指向的对象与上面CWnd类的GetMenu函数返回值所指向的对象是不一样的。GetMenu函数返回的是指向程序菜单栏对象的指针,而CMenu类的GetSubMenu成员函数返回的是由参数nPos指定的子菜单的指针
在这里插入图片描述
✨✨ 2) 设置标记菜单,需要使用CMenu类的
CheckMenuItem函数。功能是**为菜单项添加一个标记,或者移除菜单项的标记。**声明如下:

UINT CheckMenuItem(UINT nIDCheckItem, UINT nCheck);
  • nIDCheckItem:指定需要处理的菜单项,取值由第二个参数决定。
  • nCheck:指定怎样设置菜单项,以及如何定位该菜单项的位置。取值可以是:
    MF_CHECKED:设置菜单项的复选标记。
    MF_UNCHECKED:移走菜单项的复选标记。
    MF_BYPOSITION:根据菜单项位置访问菜单项,即第一个参数指定的是菜单项的索引号。
    MF_BYCOMMAND:根据菜单项的命令访问菜单项,即第一个参数指定的是菜单项的ID。

✨✨✨2.1)通过索引进行标记。


Menu中,视图菜单的索引为1,应用程序外观菜单项索引也为2。CheckMenuItem函数的第2个参数设置为:MF_BYPOSITION|MF_CHECKED

        在框架类的OnCreate函数设置子菜单标记:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ......
    
	//获得框架类的菜单
	CMenu* topMenu = GetMenu();
	//获得菜单的子菜单
	CMenu* subMenu = topMenu->GetSubMenu(1);
	//MF_CHECKED:设置菜单项的复选标记;
	//MF_BYPOSITION:根据菜单项位置访问菜单项,即第一个参数指定的是菜单项的索引号。
	subMenu->CheckMenuItem(2,MF_BYPOSITION|MF_CHECKED ); 

	return 0;
}

运行程序,发现程序编译通过,但是运行出现异常。发现是由于topMenu并未获得菜单指针,GetMenu函数没有返回菜单栏指针,通过跟踪GetMenu的返回值:topMenu的值:错误没有找到符号。


解决方案

  • 法一:重建一个标准风格的单文档应用程序。
    上面创建的单文档项目是基于Visual Studio的应用程序,更改项目样式为MFC standard类型,将项目类型设置为“MFC标准类型”。

    运行程序,应用程序界面如下:
    在这里插入图片描述
    由于标准状态下的视图子菜单发生变化,现在对文件的子菜单打开菜单项进行标记。文件的索引为0、打开的索引为1。CheckMenuItem函数的第二个参数应该为:MF_BYPOSITION|MF_CHECKED

    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
    	......
    	
    	//获得框架类的菜单
    	CMenu* menu = GetMenu();
    	//获得菜单的子菜单
    	CMenu* subMenu = menu->GetSubMenu(0);
    	//MF_CHECKED:设置菜单项的复选标记;
    	//MF_BYPOSITION:根据菜单项位置访问菜单项,即第一个参数指定的是菜单项的索引号。
    	subMenu->CheckMenuItem(1, MF_BYPOSITION | MF_CHECKED);
    	return 0;
    }
    
    


    ✨✨✨2.2 ) 通过菜单标识访问菜单项。

    打开菜单编辑器,查看菜单项的属性对话框,得到文件的子菜单保存的标识为:ID_FILE_SAVE。此时CheckMenuItem函数第2个参数:MF_BYCOMMAND | MF_CHECKED

    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
    	......
    	
    	//获得框架类的菜单
    	CMenu* menu = GetMenu();
    	//获得菜单的子菜单
    	CMenu* subMenu = menu->GetSubMenu(0);
    	//MF_CHECKED:设置菜单项的复选标记;
    	//MF_BYPOSITION:根据菜单项位置访问菜单项,即第一个参数指定的是菜单项的索引号。
    	subMenu->CheckMenuItem(1, MF_BYPOSITION | MF_CHECKED);
    	subMenu->CheckMenuItem(ID_FILE_SAVE, MF_BYCOMMAND | MF_CHECKED);
    	//或直接写:
        //GetMenu()->subMenu(0)->CheckMenuItem(1, MF_BYPOSITION | MF_CHECKED);
        //GetMenu()->subMenu(0)->CheckMenuItem(ID_FILE_SAVE, MF_BYCOMMAND | MF_CHECKED);
    	return 0;
    }
    

  • 法二:源程序上更改。
    在CMainFrame类中找到CMFCMenuBar m_wndMenuBar这个成员变量,将跟它相关的代码注释掉。

    执行程序:

    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
        ......
    
        GetMenu()->GetSubMenu(1)->CheckMenuItem(2, MF_BYPOSITION | MF_CHECKED);
    	return 0;
    }
    

    成功。生成如下的界面:

3.2 默认菜单项

有些应用程序的子菜单下有一个菜单项是以粗体形式显示的,以这种形式显示的就是该子菜单的默认菜单项。为了实现这种菜单项,可以利用CMenu类的SetDefaulitem成员函数来完成。这个函数的声明形式如下所示:

BOOL SetDefaultItem(UINT iItem, BOOL fByPos=FALSE);

这个函数有两个参数,并且第一个参数的取值也是由第二个参数决定的。

  • ultem:可以是新的默认菜单项的标识或位置索引,如果值为-1,则表明没有默认菜单项。
  • fByPos:是BOOL类型,如果它的值为FALSE,则表明第一个参数是菜单项标识,否则是菜单项位置索引。

由此可见,这个函数也为我们提供了两种访问菜单项的方式。

✨✨1)位置索引。

  • 标准Standard型单文档应用程序。
    设置文件子菜单下的打开为该子菜单的默认菜单项。子菜单索引为1;此时SetDefaulitem函数第二个参数设置为TURE。
    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
    	......
    	//设置标记菜单
        GetMenu()->subMenu(0)->CheckMenuItem(1, MF_BYPOSITION | MF_CHECKED);
        //设置默认子菜单
        GetMenu()->GetSubMenu(0)->SetDefaultItem(1, TRUE);
       
    	return 0;
    }
    
  • 原Virtual Studio型单文档应用程序同样不受影响。
    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
        ......
    
        GetMenu()->GetSubMenu(1)->CheckMenuItem(2, MF_BYPOSITION | MF_CHECKED);
        GetMenu()->GetSubMenu(0)->SetDefaultItem(2, TRUE);
    	return 0;
    }
    

✨✨2)菜单项标识的方式。

📢📢📢以下基于标准Standard型单文档应用程序。

设置文件子菜单下的打印为该子菜单的默认菜单项。打印的标识符ID为:ID_FILE_PRINT。此时SetDefaulitem函数第二个参数设置为FALSE。
、

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	......
	
	subMenu->CheckMenuItem(1, MF_BYPOSITION | MF_CHECKED);
	//设置默认子菜单
	GetMenu()->GetSubMenu(0)->SetDefaultItem(1, TRUE);
	GetMenu()->GetSubMenu(0)->SetDefaultItem(ID_FILE_PRINT, FALSE);
	return 0;
}

📢注意


🔴 另存为打印菜单项之间有一个很长的分隔栏,这个分隔栏在子菜单中是占据索引位置的,所以打印菜单项的索引应该是5,读者一定要应该注意这一点。
在这里插入图片描述
🔴 上面程序中文件子菜单下的打印打开这两个菜单项都设置为默认菜单项。Build并运行Menu程序,发现打开菜单项没有以粗体显示,只有打印菜单项以粗体显示了。这说明一个子菜单只能有一默认菜单项,且最后设置的默认菜单会最终生效。


🔲🔲🔲下面全部在Virtual Studio型单文档应用程序中进行。

3.3 图形标记菜单

实现图形标记菜单项,可以利用CMenu类的SetMenultemBitmaps函数,这个函数的作用是将指定的位图与菜单项关联起来。该函数的声明如下所示:

BOOL SetMenurtemBitmaps ( UINT nPosition, UINT  nFlags, const CBitmap* pBmpUnchecked, const cBitmap* pBmpChecked);
  • nPosition 和 nFlags:此参数由第二个参数得值决定。如果第二个参数的值是MF_BYCOMMAND,则表明第一个参数的值是菜单项标识;如果第二个参数的值是MF_BYPOSITION,则表明第一个参数的值是菜单项位置索引
  • pBmpUnchecked :CBitmap类型的指针,用于设置与菜单项关联的两幅位图,指定当取消菜单项选中状态时的位图。
  • pBmpChecked:CBitmap类型的指针,用于设置与菜单项关联的两幅位图,指定当选中菜单项选中状态时显示的位图。

✨ 1) 准备图形。

新建一个位图资源,先建立一个默认大小的白色背景的位图,位图的ID为:IDB_BITMAP1

✨✨ 2) 在CMainFrame类的OnCreate函数中完成图形标记菜单的实现。由于位图要作为图形标记菜单的内容,所以位图对象要设置为CMainFrame的成员变量。所以为CMainFrame添加一个CBitmap类型的成员变量:m_bitmap

✨✨✨ 3)在CMainFrame的OnCreate函数中添加实现图像标记菜单的具体代码。首先通过位图的ID加载位图,初始化图形标记菜单将使用的位图对象。然后将程序视图下的工具栏和停靠窗口菜单项实现为图形标记菜单项,并且将该菜单项的选中和取消选中状态都设置为同一副位图。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   
    GetMenu()->GetSubMenu(1)->CheckMenuItem(2, MF_BYPOSITION | MF_CHECKED);
	GetMenu()->GetSubMenu(0)->SetDefaultItem(2, TRUE);

	m_bitmap.LoadBitmap(IDB_BITMAP1);
	GetMenu()->GetSubMenu(1)->SetMenuItemBitmaps(0, MF_BYPOSITION, &m_bitmap, &m_bitmap);;
	return 0;
}

在这里插入图片描述
通过GetSystemMetrics函数可以得到图形标记菜单上显示的位图的尺寸。这个函数的声明如下:

int GetSystemMetrics ( int nIndex );

参数用来指定希望获取哪部分系统信息。当该参数的值为SM_XMENUCHECK或SM_CYMENUCHECK时,这个函数将获取标记菜单项上标记图形的默认尺寸,前者是获得标记图形的宽度,后者将获得标记图形的高度。以这两个参数值调用GetSystemMetrics函数两次,从而得到标记菜单项上位图的尺寸。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   
    GetMenu()->GetSubMenu(1)->CheckMenuItem(2, MF_BYPOSITION | MF_CHECKED);
	GetMenu()->GetSubMenu(0)->SetDefaultItem(2, TRUE);

    CString str;
	str.Format(_T("x=%d,y=%d"), GetSystemMetrics(SM_CXMENUCHECK), GetSystemMetrics(SM_CYMENUCHECK));
	MessageBox(str);
	m_bitmap.LoadBitmap(IDB_BITMAP1);
	GetMenu()->GetSubMenu(1)->SetMenuItemBitmaps(0, MF_BYPOSITION, &m_bitmap, &m_bitmap);;
	return 0;
}


获得位图作为图形标记菜单项上显示的位图的宽度和高度后,可以调整位图的尺寸大小进行设置。窗口有显示位图尺寸的区域。


🔲🔲🔲由于在之前的操作中,误删除了文件菜单项,所以重新建立一个Virtual Studio型的单文档程序。并实现上述使用的Virtual Studio单文档程序的所有功能。

3.4 禁用菜单项

若禁用菜单项,以屏蔽菜单实现的功能,可以利用CMenu类的成员函数: EnableMenultem来完成。该函数的作用是设置菜单项的状态:能够使用、禁用或变灰显示。声明形式如下:

UINT EnableMenurtem( UINT nIDEnableltem, UINT nEnable );

EnableMenultem函数第一个参数的含义也是由第二个参数决定的。后者可以是MF-DISABLED, MF-ENABLED或MFGRAYED与MFBYCOMMAND或MF-BYPOSITION的组合。
     ◼ MF_BYCOMMAND:指定第一个参数是菜单项的标识ID。
     ◼ MF_BYPOSITION:指定第一个参数是菜单项的位置索引。
     ◼ MF_DISABLED:禁用菜单项,用户不能选择该菜单项,但该菜单项并没有变成灰色。但当用户选择此菜单时,程序没有反应。
     ◼ MF_ENABLED:使菜单项可用,用户可以选择这个菜单项。
     ◼ MF_GRAYED:禁用菜单项,用户不能选择该菜单项。并且该菜单项变成灰色的显示形式

📢📢📢 通常把MF_GRAYED和MF_DISABLED这两个标志放在一起使用。但是,这么做开不是必须的。

EnableMenultem函数说明


一旦在CMainFrame类的构造函数中把成员变量m_bAutoMenuEnable设置为FALSE后,就不需要对ON_UPDATECOMMANDUION_COMMAND消息进行响应处理了, CMenu类的EnableMenultem函数将能够正常工作。

MFC为菜单提供了一种命令更新的机制。程序在运行时,根据此机制去判断哪个菜单可以使用,哪个菜单不能够使用,然后显示其相应的状态。默认情况下,所有菜单项的更新都是由MFC的命令更新机制完成的。如果想更改菜单项的状态,那就必须先把m_bAutoMenuEnable变量设置为FALSE,自己对菜单项的状态更新才能起作用。

✨1)在程序Menu的CMainFrame类构造函数中把m_bAutoMenuEnable这个变量初始化为FALSE。

CMainFrame::CMainFrame() noexcept
{
	// TODO: 在此添加成员初始化代码
	m_bAutoMenuEnable = FALSE;
	theApp.m_nAppLook = theApp.GetInt(_T("ApplicationLook"), ID_VIEW_APPLOOK_VS_2008);
}

✨✨2)禁用文件菜单的子菜单项的功能。默认打开菜单项是可以选择文件的。禁用后,使其不再弹出打开对话框。

✨✨✨2.1) 使用索引。禁用打开菜单项。
打开对话框的索引是1,配合MF_BYPOSITION。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   
    GetMenu()->GetSubMenu(2)->CheckMenuItem(2, MF_BYPOSITION | MF_CHECKED);
	GetMenu()->GetSubMenu(1)->SetDefaultItem(2, TRUE);

	CString str;
	str.Format(_T("x=%d,y=%d"), GetSystemMetrics(SM_CXMENUCHECK), GetSystemMetrics(SM_CYMENUCHECK));
	MessageBox(str);
	m_bitmap.LoadBitmap(IDB_BITMAP1);
	GetMenu()->GetSubMenu(2)->SetMenuItemBitmaps(0, MF_BYPOSITION, &m_bitmap, &m_bitmap);;
	
	GetMenu()->GetSubMenu(0)->EnableMenuItem(1, MF_BYPOSITION | MF_DISABLED);
	//GetMenu()->GetSubMenu(0)->EnableMenuItem(1, MF_BYPOSITION | MF_DISABLED|MF_GRAYED);
	return 0;
}


✨✨✨2.2) 使用菜单项标识符,禁用保存菜单项。
保存菜单项的ID为:ID_FILE_SAVE,配合MF_BYCOMMAND使用。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   
    GetMenu()->GetSubMenu(2)->CheckMenuItem(2, MF_BYPOSITION | MF_CHECKED);
	GetMenu()->GetSubMenu(1)->SetDefaultItem(2, TRUE);

	CString str;
	str.Format(_T("x=%d,y=%d"), GetSystemMetrics(SM_CXMENUCHECK), GetSystemMetrics(SM_CYMENUCHECK));
	MessageBox(str);
	m_bitmap.LoadBitmap(IDB_BITMAP1);
	GetMenu()->GetSubMenu(2)->SetMenuItemBitmaps(0, MF_BYPOSITION, &m_bitmap, &m_bitmap);;
	
	GetMenu()->GetSubMenu(0)->EnableMenuItem(1, MF_BYPOSITION | MF_DISABLED);
	//GetMenu()->GetSubMenu(0)->EnableMenuItem(1, MF_BYPOSITION | MF_DISABLED|MF_GRAYED);
	GetMenu()->GetSubMenu(0)->EnableMenuItem(ID_FILE_SAVE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
	return 0;
}

在这里插入图片描述

3.5 移除和转载菜单

若要移除一个菜单的话,可利用CWnd类提供的SetMenu成员函数实现,该函数声明:

BOOL SetMenu(CMenu*  pMenu);

参数指向一个新菜单对象。参数值若为NULL,则当前菜单被移除。同样在框架类的OnCreate函数中操作。

✨1) 移除菜单操作。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   
   SetMenu(NULL);
   return 0;
}

✨✨2) 加载菜单并显示。
通过菜单的标识符ID加载MAIN菜单。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   SetMenu(NULL);
   
   CMenu menu;
   menu.LoadMenuW(IDR_MAINFRAME);
   SetMenu(&menu);
   
   return 0;
}

运行程序,菜单成功显示出来。但是程序出现了异常。

🟢原因:由于OnCreate中定义的CMenu对象menu是局部对象。
🟢解决方法:
①:将CMenu对象定义为CMainFrame类的一个成员变量。
②:CMenu对象仍然为局部对象,调用SetMenu将其设置为窗口菜单后,立即调用CMenu类的另一个成员函数Detach,以便将菜单句柄与菜单对象分离。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   SetMenu(NULL);
   
   CMenu menu;
   menu.LoadMenuW(IDR_MAINFRAME);
   SetMenu(&menu);
   menu.Detach();
   
   return 0;
}

原理:SetMenu函数会把窗口的菜单设置为其参数指定的新菜单,导致窗口重绘,以反映菜单的这种变化,同时也将该菜单对象的所有权交由给窗口对象。而随后的Detach函数会把菜单句柄与这个菜单对象分离。当这个局部菜单对象的生命周期结束时,它不会去销毁一个它不再具有拥有权的菜单。这个菜单在窗口销毁时会自动销毁。

3.6 MFC菜单命令更新机制

MFC编程中,菜单项状态的维护依赖于CN_UPDATE_COMMAND_UI消息。可以通过类向导在消息映射中添加ON_UPDATE_COMMAND_UI宏来捕获CN_UPDATE_COMMAND_UI消息。

✨✨使编辑菜单下的子菜单剪切菜单项变为可以使用的状态。为剪切菜单添加UPDATE_COMMAND_UI消息处理函数。

会发现CMainFrame类的消息映射中添加一个ON_UPDATE_COMMAND_UI宏。此宏用于捕获CN_UPDATE_COMMAND_UI消息。

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx)
	ON_WM_CREATE()
	ON_COMMAND(ID_VIEW_CUSTOMIZE, &CMainFrame::OnViewCustomize)
	ON_REGISTERED_MESSAGE(AFX_WM_CREATETOOLBAR, &CMainFrame::OnToolbarCreateNew)
	ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook)
	ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnUpdateApplicationLook)
	ON_WM_SETTINGCHANGE()
	
	ON_UPDATE_COMMAND_UI(ID_EDIT_CUT, &CMainFrame::OnUpdateEditCut)
END_MESSAGE_MAP()

程序框架捕获到 CN_UPDATE_COMMAND_UI 消息后,最终会交由该消息的响应函数处理。

void CMainFrame::OnUpdateEditCut(CCmdUI* pCmdUI)
{
	// TODO: 在此添加命令更新用户界面处理程序代码
}

OnUpdateEditCut 这个函数有一个CCmdUI指针类型的参数。利用这个CCmdUI类,可以决定一个菜单项是否可以使用、是否有标记,还可以改变菜单项的文本。

添加一个UPDATA_COMMAND_UI消息响应函数,MFC后台:

        操作系统发出WM_INITMENUPOPUP消息,然后由程序窗口的基类(如CFrameWnd)接管。它会创建一个CCmdUI对象,并与程序的第一个菜单项相关联,调用该对象的一个成员函数DoUpdate()。这个函数发出CN_UPDATE_COMMAND_UI消息,这条消息带有一个指向CCmdUI对象的指针。系统会判断是否存在一个ON_UPDATE_COMMAND_UI宏去捕获这个菜单项消息。如果找到这样一个宏,就调用相应的消息响应函数进行处理,在这个函数中,可以利用传递过来的CCmdUI对象去调用相应的函数,使该菜单项可以使用,或禁用该菜单项。当更新完第一个菜单项后,同一个CCmdUI对象就设置为与第二个菜单项相关联,依此顺序进行,直到完成所有菜单项的处理。这就是MFC采用的命令更新机制

        利用MFC提供的命令更新机制,我们只需要捕获UPDATE_COMMAND_UI 消息,在该消息的响应函数中调用CCmdUI对象的相应函数,例如Enable、 SetCheck或SetText函数,就可以分别实现使菜单项可用或禁用、设置标记菜单,或设置菜单项的文本这些功能。

CCmdUI类的Enable函数的声明形式如下所示:

virtual void Enable (BOOL bOn = TRUE );

当参数值为TRUE时,使菜单项可用;当参数值为FALSE时,禁用菜单项。

🔴 当前剪切菜单项是默认不可用状态:

🟢 设置为可用状态,在响应函数中实现:

void CMainFrame::OnUpdateEditCut(CCmdUI* pCmdUI)
{
	// TODO: 在此添加命令更新用户界面处理程序代码
	pCmdUI->Enable();
}

或者利用索引也可以实现此功能。CCmdUI类还有一个成员变量:m_nIndex,保存了当前菜单项的位置索引。

void CMainFrame::OnUpdateEditCut(CCmdUI* pCmdUI)
{
	// TODO: 在此添加命令更新用户界面处理程序代码
	//计算菜单项索引时,一定要把分隔栏菜单项计算在内
	if (2 == pCmdUI->m_nIndex) {
		pCmdUI->Enable();
	}
}

用索引法,由于菜单项和工具按钮的位置索引计算方式不同,工具栏上的剪切按钮仍然为灰色。

发现工具栏上剪切工具按钮也可以使用了。打开资源视图选项卡,双击Toolbar分支下的工具栏标识:IDR_MAINFRAME,查看剪刀图标的ID:ID_EDIT_CUT,与菜单项的标识符相同。:

📢📢📢 如果要把工具栏上的一个工具按钮与菜单栏中的某个菜单项相关联,只要将它们的ID设置为同一个标识就可以了

禁用文件的子菜单另存为


点击另存为发现已经无反应。点击工具栏的保存图标,发现仍然可以使用。是因为工具栏的保存图标的标识符与另存为的标识符不同。

📢📢📢 在程序中设置某个菜单项的状态步骤:
1)首先为这个菜单项添加UPDATE_COMMAND_UI消息响应函数
2) 然后在这个函数中进行状态的设置即可。

3.7 快捷菜单

单击鼠标右键显示快捷菜单(也称为上下文菜单,或右键菜单)这一功能。

四、动态菜单操作

4.1 在己有菜单项目后面添加新的菜单项目

通过代码动态添加菜单项目,可以利用CMenu类提供的成员函数:AppendMenu函数完成。函数作用是把一个新菜单项目添加到一个指定的菜单项目的末尾。声明如下:

BOOL AppendMenu (UINT nFlags, UINT_PTR nIDNewItem = 0, LPCTSTR lpszNewItem=NULL );

◼ nFlags:指定新添加的菜单项目的状态信息,有多种这样的状态标志。
◼ nIDNewItem:取值取决于第一个参数。如果第一个参数的值是MF_POPUP,那么nIDNewltem就是一个顶层菜单的句柄;否则就是要添加的新菜单项的命令ID。如果第一个参数的值是MF_SEPARATOR,那么nIDNewltem的值将被忽略。
◼ lpszNewItem:取值同样取决于第一个参数。如果第一个参数的值是MF_STRING,则lpszNewltem 就是指向要添加的这个新菜单项目的文本的指针。如果第一个参数的值是MF_OWNERDRAW,则IpszNewItem 就是指向该菜单项目的一个附加数据的指针。如果第一个参数的值是MF_SEPARATOR,则IpszNewltem的值将被忽略。

🔲 菜单标志 🔲


首先创建一个菜单对象。利用CMenu类的成员函数 CreatePopupMenu实现。函数作用: 创建一个弹出菜单,然后将其与一个CMenu对象相关联。在框架类的OnCreate函数中实现动态添加菜单。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   
   	//先定了一个CMenu对象: menu
	CMenu menu;
	//使用GetMenu函数得到程序框架窗口菜单
	menu.CreateMenu();
	//并利用该指针调用AppendMenu函数,以添加一个新的弹出菜单。这个新的弹出菜单显示文本为: WaitFoF。
	//因为CMenu对象的m_hMenu, 成员变量是菜单句柄, 其类型为HMENU, 但这里函数要求的是UINT类型, 所以必须进行强制转换。
	//添加的是一个弹出菜单, 相当于是程序框架窗口菜单的一个子菜单
	GetMenu()->AppendMenuW(MF_POPUP, (UINT)menu.m_hMenu, _T("WaitFoF"));

   
   return 0;
}

当我们用鼠标单击此子菜单时,立即就会弹出一个提示程序出现异常的窗口。

原因就是因为我们在程序中把menu对象定义成局部变量了,至于解决这个问题的办法,前面已经介绍了。有两种:

  • 1)一种是把menu对象修改为框架类的成员变量;
  • 2)另一种是在完成菜单的添加之后立即调用菜单对象的Detach函数,将菜单句柄与菜单对象之间的关联解开。
    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
       ......
       
       	//先定了一个CMenu对象: menu
    	CMenu menu;
    	//使用GetMenu函数得到程序框架窗口菜单
    	menu.CreateMenu();
    	//并利用该指针调用AppendMenu函数,以添加一个新的弹出菜单。这个新的弹出菜单显示文本为: WaitFoF。
    	//因为CMenu对象的m_hMenu, 成员变量是菜单句柄, 其类型为HMENU, 但这里函数要求的是UINT类型, 所以必须进行强制转换。
    	//添加的是一个弹出菜单, 相当于是程序框架窗口菜单的一个子菜单
    	GetMenu()->AppendMenuW(MF_POPUP, (UINT)menu.m_hMenu, _T("WaitFoF"));
        menu.Detach();
        
        return 0;
    }
    

4.2 己有菜单项目之间插入一个新的菜单项目

包括在两个子菜单之间插入一个子菜单,以及在两个菜单项之间插入一个新的菜单项。这可以利用CMenu类的InsertMenu成员函数来实现。这个函数的一种声明具有以下形式:

BOOL InsertMenu (UINT nPosition, UINT nFlags, UINT_PTR nIDNewItem = 0LPCTSTR IpszNewItem = NULL);

参数nFlags,nIDNewltem 和IpszNewltem 与 AppendMenu函数中相应参数具有相同的意义。
◼ nFlags:除了具有AppendMenu函数中那些标志以外,还可以利用或运算MF_BYCOMMANDMF_BYPOSITION标志相组合。
◼ nPosition:指定的是新菜单项目插入的位置。它的取值取决于第二个参数nFlags。
        ◼◾▪ 当nFlags参数指定的是MF_BYCOMMAND标志时,第1个参数指定的是一个菜单命令标识表示新菜单项将在这个标识所表示的菜单项之前插入
        ◼◾▪ 如果nFlags参数指定的是MF_BYPOSITION标志,新菜单项目将在第1个参数指定的位置所表示的菜单项目之前插入第一个参数这时所表示的就是这个新菜单项目插入后所在的位置

✨ 1)在两个菜单项之间插入一个新的菜单项。
帮助菜单项和WaitFoF菜单项之间插入一个以个插入菜单菜单项,插入的位置应该为**“4”**。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	 ......
	 
	 //先定了一个CMenu对象: menu
	CMenu menu;
	//使用GetMenu函数得到程序框架窗口菜单
	menu.CreateMenu();
	//并利用该指针调用AppendMenu函数,以添加一个新的弹出菜单。这个新的弹出菜单显示文本为: WaitFoF。
	//因为CMenu对象的m_hMenu, 成员变量是菜单句柄, 其类型为HMENU, 但这里函数要求的是UINT类型, 所以必须进行强制转换。
	//添加的是一个弹出菜单, 相当于是程序框架窗口菜单的一个子菜单
	GetMenu()->AppendMenuW(MF_POPUP, (UINT)menu.m_hMenu, _T("WaitFoF"));
	GetMenu()->InsertMenu(4,MF_POPUP|MF_BYPOSITION,(UINT)menu.m_hMenu, TEXT("插入菜单"));
	menu.Detach();
	    
	return 0;
}


✨✨ 2)在两个子菜单之间插入一个子菜单

✨✨✨2.1)在插入菜单菜单项下增加4个子菜单,子菜单的ID号随意设置成111~114。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	 ......
	 
	 //先定了一个CMenu对象: menu
	CMenu menu;
	//使用GetMenu函数得到程序框架窗口菜单
	menu.CreateMenu();
	//并利用该指针调用AppendMenu函数,以添加一个新的弹出菜单。这个新的弹出菜单显示文本为: WaitFoF。
	//因为CMenu对象的m_hMenu, 成员变量是菜单句柄, 其类型为HMENU, 但这里函数要求的是UINT类型, 所以必须进行强制转换。
	//添加的是一个弹出菜单, 相当于是程序框架窗口菜单的一个子菜单
	//GetMenu()->AppendMenuW(MF_POPUP, (UINT)menu.m_hMenu, _T("WaitFoF"));
	GetMenu()->InsertMenu(4,MF_POPUP|MF_BYPOSITION,(UINT)menu.m_hMenu, TEXT("插入菜单"));
	menu.AppendMenuW(MF_STRING, 111, _T("插入子菜单1"));
	menu.AppendMenuW(MF_STRING, 112, _T("插入子菜单2"));
	menu.AppendMenuW(MF_STRING, 113, _T("插入子菜单3"));
	menu.AppendMenuW(MF_STRING, 114, _T("插入子菜单4"));
	menu.Detach();
	   
	return 0;
}

发现新增加的插入菜单增加了4个子菜单。

✨✨✨2.2)在文件子菜单下添加一个菜单项Wait_FoF

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	 ......

    //先定了一个CMenu对象: menu
	CMenu menu;
	//使用GetMenu函数得到程序框架窗口菜单
	menu.CreateMenu();
	//并利用该指针调用AppendMenu函数,以添加一个新的弹出菜单。这个新的弹出菜单显示文本为: WaitFoF。
	//因为CMenu对象的m_hMenu, 成员变量是菜单句柄, 其类型为HMENU, 但这里函数要求的是UINT类型, 所以必须进行强制转换。
	//添加的是一个弹出菜单, 相当于是程序框架窗口菜单的一个子菜单
	GetMenu()->AppendMenuW(MF_POPUP, (UINT)menu.m_hMenu, _T("WaitFoF"));
	GetMenu()->InsertMenu(4,MF_POPUP|MF_BYPOSITION,(UINT)menu.m_hMenu, TEXT("插入菜单"));
	/*menu.AppendMenuW(MF_STRING, 111, _T("插入子菜单1"));
	menu.AppendMenuW(MF_STRING, 112, _T("插入子菜单2"));
	menu.AppendMenuW(MF_STRING, 113, _T("插入子菜单3"));
	menu.AppendMenuW(MF_STRING, 114, _T("插入子菜单4"));
	*/
	menu.Detach();
	
	GetMenu()->GetSubMenu(0)->AppendMenuW(MF_STRING, 110, _T("Wait_FoF"));
	  
	return 0;
} 


✨✨✨2.3)在编辑的子菜单剪切复制之间添加子菜单ψ(`∇´)ψ

✨✨✨✨2.3.1)使用索引实现。
搭配MF_BYPOSITION使用,新菜单项目将在第1个参数指定的位置所表示的菜单项目之前插入。编辑的索引为1,剪切的索引为2,复制的索引为3。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	 ......
	 //先定了一个CMenu对象: menu
	CMenu menu;
	//使用GetMenu函数得到程序框架窗口菜单
	menu.CreateMenu();
	//并利用该指针调用AppendMenu函数,以添加一个新的弹出菜单。这个新的弹出菜单显示文本为: WaitFoF。
	//因为CMenu对象的m_hMenu, 成员变量是菜单句柄, 其类型为HMENU, 但这里函数要求的是UINT类型, 所以必须进行强制转换。
	//添加的是一个弹出菜单, 相当于是程序框架窗口菜单的一个子菜单
	GetMenu()->InsertMenu(4,MF_POPUP|MF_BYPOSITION,(UINT)menu.m_hMenu, TEXT("插入菜单"));
	GetMenu()->AppendMenuW(MF_POPUP, (UINT)menu.m_hMenu, _T("WaitFoF"));
	/*menu.AppendMenuW(MF_STRING, 111, _T("插入子菜单1"));
	menu.AppendMenuW(MF_STRING, 112, _T("插入子菜单2"));
	menu.AppendMenuW(MF_STRING, 113, _T("插入子菜单3"));
	menu.AppendMenuW(MF_STRING, 114, _T("插入子菜单4"));
	*/
	menu.Detach();
	GetMenu()->GetSubMenu(0)->AppendMenuW(MF_STRING, 110, _T("Wait_FoF"));
	
	GetMenu()->GetSubMenu(1)->InsertMenu(3, MF_STRING | MF_BYPOSITION, 111, TEXT("ψ(`∇´)ψ"));
   
	return 0;
}

✨✨✨✨2.3.2)使用标识符表示。
搭配MF_BYCOMMAND使用,新菜单项将在这个标识所表示的菜单项之前插入。复制的标识符ID:ID_EDIT_COPY

GetMenu()->GetSubMenu(1)->InsertMenu(ID_EDIT_COPY, MF_STRING | MF_BYCOMMAND, 111, TEXT("ψ(`∇´)ψ"));	

在这里插入图片描述

4.3 删除菜单

CMenu提供了一个DeleteMenu成员函数,这个函数可以删除一个菜单项目,包括子菜单,以及子菜单下的菜单项主要取决于调用这个函数的对象。如果该对象是程序的菜单栏对象,那么删除的就是指定的子菜单;如果该对象是一个子菜单对象,那么删除的就是该子菜单下的一个菜单项。该函数具有以下形式的声明:

BOOL DeleteMenu( UINT nPosition, UINT nFlags );

DeleteMenu函数的两个参数与前面讲述的CMenu类的其他几个函数的同名参数具有相同的意义。

✨1)删除菜单栏的菜单项。
例如删除帮助子菜单,那么就可以在CMainFrame类的OnCreate函数最后,return之前添加:

GetMenu()->DeleteMenu(3, MF_BYPOSITION);


✨✨2)删除一个菜单项的子菜单。
例如删除文件菜单项的子菜单打印浏览
        ① 首先需要得到该菜单项所在的子菜单
        ② 然后既可以按照菜单项的标识,也可以根据它的位置索引来删除它。
✨✨✨2.1)菜单项的标识
搭配MF_BYCOMMAND,打印浏览的菜单项的标识为:ID_FILE_PRINT_PREVIEW。

GetMenu()->GetSubMenu(0)->DeleteMenu(ID_FILE_PRINT_PREVIEW, MF_BYCOMMAND) ;

✨✨✨2.2)菜单索引
搭配MF_BYPOSITION,打印浏览的菜单项的索引为6。

GetMenu()->GetSubMenu(0)->DeleteMenu(6, MF_BYPOSITION) ;

4.4 动态添加的菜单项的命令响应

对于动态添加的菜单项,在程序运行之前,它们的标识是不知道的,资源视图的Menu的分支IDR_MAINFRAME中找不到动态创建的菜单项。

🟢🟢 以插入菜单的子菜单插入菜单2为例。进行动态菜单的命令响应。
调整程序的CMainFrame类的OnCreate函数,仅添加以下自定义的代码。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	 ......

//先定了一个CMenu对象: menu
	CMenu menu;
	//使用GetMenu函数得到程序框架窗口菜单
	menu.CreateMenu();
	//并利用该指针调用AppendMenu函数,以添加一个新的弹出菜单。这个新的弹出菜单显示文本为: WaitFoF。
	//因为CMenu对象的m_hMenu, 成员变量是菜单句柄, 其类型为HMENU, 但这里函数要求的是UINT类型, 所以必须进行强制转换。
	//添加的是一个弹出菜单, 相当于是程序框架窗口菜单的一个子菜单
    GetMenu()->InsertMenu(4,MF_POPUP|MF_BYPOSITION,(UINT)menu.m_hMenu, TEXT("插入菜单"));    
	menu.AppendMenuW(MF_STRING, 111, _T("插入子菜单1"));
	menu.AppendMenuW(MF_STRING, 112, _T("插入子菜单2"));
	menu.AppendMenuW(MF_STRING, 113, _T("插入子菜单3"));
	menu.AppendMenuW(MF_STRING, 114, _T("插入子菜单4"));
	menu.Detach();
  
	return 0;
}


1)首先,为这个菜单项创建一个菜单资源ID。
在Menu工程的头文件目录中打开Resource.h文件,这个文件中定义了程序当前使用的一些资源的ID。手工在其中为菜单项插入菜单2添加一个ID:112,即 #define IDMHELLO 111

Menu程序就有了一个名为IDM_INSERTMENU2的ID,在程序中可以为插入菜单2菜单项使用这个ID。
则更改OnCreate中的菜单项插入菜单2创建的代码:

menu.AppendMenuW(MF_STRING, IDM_INSERTMENU2, _T("插入子菜单2"));

然后为这个菜单添加命令消息响应函数。遵循MFC的消息映射机制,需要添加3处代码实现命令消息的响应。
⭕1)在响应此菜单项命令的程序类的头文件中添加响应函数原型,添加的位置最好在声明消息映射宏DECLARE_MESSAGE_MAP()之前。也可以直接在头文件最后声明。

此例添加在MainFrm.h中:

// 生成的消息映射函数
protected:
	afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
	afx_msg void OnViewCustomize();
	afx_msg LRESULT OnToolbarCreateNew(WPARAM wp, LPARAM lp);
	afx_msg void OnApplicationLook(UINT id);
	afx_msg void OnUpdateApplicationLook(CCmdUI* pCmdUI);
	afx_msg void OnSettingChange(UINT uFlags, LPCTSTR lpszSection);

	afx_msg void OnInsertMenu2();
	DECLARE_MESSAGE_MAP()

📢📢📢 可以遵循MFC命名习惯,在菜单项名称的前面加上On来命名该菜单命令响应函数。这只是一种习惯,并不是规定函数名一定要这样来命名。

⭕⭕2)在响应这个菜单项命令的程序类的源文件中的消息映射表中添加消息映射。添加位置应该是在BEGIN_MESSAGE_MAPEND_MESSAGE_MAP宏之间,在两个AFX_MSG_MAP注释宏之后,并且菜单命令消息的映射宏是ON_COMMAND。本例在CMainFrame的源文件中添加如下代码:

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx)
	ON_WM_CREATE()
	ON_COMMAND(ID_VIEW_CUSTOMIZE, &CMainFrame::OnViewCustomize)
	ON_REGISTERED_MESSAGE(AFX_WM_CREATETOOLBAR, &CMainFrame::OnToolbarCreateNew)
	ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook)
	ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnUpdateApplicationLook)
	ON_WM_SETTINGCHANGE()
	ON_UPDATE_COMMAND_UI(ID_EDIT_CUT, &CMainFrame::OnUpdateEditCut)
	ON_UPDATE_COMMAND_UI(ID_FILE_SAVE_AS, &CMainFrame::OnUpdateFileSaveAs)
	
	ON_COMMAND(IDM_INSERTMENU2, &CMainFrame::OnInsertmenu2)
END_MESSAGE_MAP()

在这里插入图片描述
⭕⭕⭕3)实现菜单命令消息响应函数的定义体。在CMainFrame源文件的最底端实现:

void CMainFrame::OnInsertMenu2() {
	MessageBox(TEXT("增加菜单2"));
}


🟢🟢🟢 也可以使用类向导为其创建命令响应函数。

五、电话本实例

新建MenuTest程序。

此外,还需要在CMainFrame类中找到CMFCMenuBar m_wndMenuBar这个成员变量,将跟它相关的代码注释掉。否则操作菜单会报错。

实现如下功能:
1)在应用程序的窗口中,输入一行文字,这行文字的格式是:人名 电话号码
2)在这行文字输入完成之后按下回车键,就会在程序的菜单栏上的帮助菜单之后动态生成一个子菜单电话薄,并且刚才输入的人名将作为电话薄的一个子菜单项来显示。
3)继续上述过程,在程序窗口中输入下一行文字,按下回车键后,并不需要在菜单栏新添一个菜单项,而是直接在已添加的电话薄菜单项下添加子菜单。这个新子菜单的文本就是新输入的人名。
4)当单击这个动态生成的电话薄中的某个子菜单项时,程序就会把相应的人名和电话号码显示在程序窗口上。

5.1 动态添加子菜单

✨ 1)为了在窗口中显示键盘输入的文字内容,视类需要捕获WM_CHAR消息。利用类向导完成消息响应函数的添加。

✨✨2)程序在运行时,只是在第一次输入一行文字后按下回车键时,在菜单栏上添加一个动态菜单项电话薄。之后只向这个菜单项添加子菜单。需要为视类添加一个成员变量,用来指示当前是第几次按下回车键。因此:
🟣🟣 2.1)为视类添加一个int类型的私有成员变量: m_nIndex,并在视类构造函数中将其初始化为-1。

🔴🔴2.2) 还需要为视类增加一个CMenu类型的成员变量m_menu,用于为菜单栏创建新的菜单项使用。

🟡🟡2.3)然后在WM_CHAR消息响应函数中需要进行判断,只有是第一次按下回车键时,才为程序添加一个新的菜单项。

由于之前我们都是在CMainFrame类中的OnCreate函数中调用GetMenu函数获取程序的菜单栏指针对象的。要在视类中获取属于框架类的菜单栏对象,首先需要利用GetParent函数获得视类的父窗口,即框架类窗口对象,然后调用框架类窗口对象的GetMenu函数就可以获得程序的菜单栏对象的指针。

void CMenuTestView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	//若输出的是换行符
	if (0x0d == nChar) {
		//且换行符是第一次输入
		if (0 == ++m_nIndex) {
			m_menu.CreatePopupMenu();
			GetParent()->GetMenu()->AppendMenuW(MF_POPUP, (UINT)m_menu.m_hMenu, TEXT("菜单薄"));	   
		}
	}
	CView::OnChar(nChar, nRepCnt, nFlags);
}

运行MenuTest程序,由于这时我们还没有为MenuTest程序添加显示输入字符的代码。程序窗口中不会显示输入的字符。随便敲几个字符,然后按下回车键,会发现MenuTest程序的菜单栏上并没有添加电话薄菜单项,但当我们把鼠标移动到应该显示这个菜单项的位置时,或者程序窗口的尺寸发生变化之后,这个电话薄菜单项就会出现。

🔳🔳原因:先前在CMainFrame类的OnCreate函数中进行的菜单操作会立即显示是因为CMainFrame类的OnCreate函数的作用是:实现窗口的创建。也就是说,在调用OnCreate函数时,程序的窗口还未创建和显示,所以在这个函数中对窗口上菜单所作的修改会立即随着OnCreate函数的调用在程序界面上呈现出来。但在窗口创建并显示完成之后,再去修改程序菜单的内容时,需要对菜单栏进行一次重绘操作才能显现修改的结果。

📋📋解决:CWnd类提供了一个DrawMenuBar成员函数用来完成菜单栏的重绘操作。菜单属于框架类窗口,所以要框架类窗口去重绘菜单栏。即现在视类中获得父窗口——框架类窗口。

5.2 显示输入的字符

根据前面的知识可以知道,为了显示输入的字符,把输入的字符都保存在一个字符串中,然后在窗口中显示这个字符串。

✨ 1)定义视类字符串成员变量为:m_strLine,并在构造函数中初始化为空。

✨✨ 2)把当前输入的字符添加到m_strLine中,在利用CDC类的TextOut函数在窗口(0,0)位置处输出。

void CMenuTestView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	//若输出的是换行符
	if (0x0d == nChar) {
		//且换行符是第一次输入
		if (0 == ++m_nIndex) {
			m_menu.CreatePopupMenu();
			GetParent()->GetMenu()->AppendMenuW(MF_POPUP, (UINT)m_menu.m_hMenu, TEXT("菜单薄"));
			GetParent()->DrawMenuBar();
		}
		
	}
	else
	{
		m_strLine += (wchar_t)nChar;
		dc.TextOutW(0, 0, m_strLine);
	}

	CView::OnChar(nChar, nRepCnt, nFlags);
}


运行上面的程序,发现当换行后,在次输出的字符是接着前面一行的文字输出的。
🔳🔳 原因:是由于m_strLine中保存了不断添加的所有字符。
📋📋 解决:按下回车键后,将m_strLine内容清空。

void CMenuTestView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	//若输出的是换行符
	if (0x0d == nChar) {
		//且换行符是第一次输入
		if (0 == ++m_nIndex) {
			m_menu.CreatePopupMenu();
			GetParent()->GetMenu()->AppendMenuW(MF_POPUP, (UINT)m_menu.m_hMenu, TEXT("菜单薄"));
			GetParent()->DrawMenuBar();
		}
		m_strLine.Empty();
	}
	else
	{
		m_strLine += (wchar_t)nChar;
		dc.TextOutW(0, 0, m_strLine);
	}

	CView::OnChar(nChar, nRepCnt, nFlags);
}


再次测试发现又出现一个问题:再次输入的文字是在上次输入的文字之上显示的。希望将上次显示的内容清除掉,再显示当前输入的文字。有多种方法可以实现窗口上文字的擦除,可以利用窗口重绘这种方法来实现。CWnd类有一个名为Invalidate的成员函数,该函数的作用是让窗口的整个客户区无效。这样,当下一条WM_PAINT消息发生时,窗口就会被更新。函数的声明如下:

void Invalidate( BOOL bErase =TRUE);

这个函数有一个BOOL类型的参数,该参数的默认值是TRUE。如果该参数的值是TRUE,窗口重绘时就会把窗口的背景擦除掉;否则,保留窗口的背景。

📋📋 解决:给Invalidate函数传递一个TRUE值,让视类窗口重绘并擦除窗口的背景。这样,在显示新一行输入字符串时,窗口上已显示的上一次输入的文字消失。

void CMenuTestView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	//若输出的是换行符
	if (0x0d == nChar) {
		//且换行符是第一次输入
		if (0 == ++m_nIndex) {
			m_menu.CreatePopupMenu();
			GetParent()->GetMenu()->AppendMenuW(MF_POPUP, (UINT)m_menu.m_hMenu, TEXT("菜单薄"));
			GetParent()->DrawMenuBar();
		}
		m_strLine.Empty();
	}
	else
	{
		m_strLine += (wchar_t)nChar;
		dc.TextOutW(0, 0, m_strLine);
	}

	CView::OnChar(nChar, nRepCnt, nFlags);
}

5.3 添加子菜单

接下来需要实现在输入人名、空格、电话号码,并当按下回车键后,把输入的人名作为子菜单项的文本添加到电话薄菜单下这一功能。

✨1)因为我们把当前输入的内容全部保存到m_strLine这个变量中,并且人名和电话号码之间是以空格分隔的,所以首先需要从m_strLine变量中分离出人名字符串。

CString类提供了一个Find成员函数,这个函数在字符串中可以查找一个字符,或者一个字符串,返回匹配结果的第一个字符在该字符串中的位置索引

📋📋 方法:
① 在m_strLine中查找空格字符,得到它的位置索引。
② 然后利用前面我们已经介绍的CString类的另一个成员函数: Left 把人名字符串截取出来,并将该字符串作为子菜单名称添加到电话薄菜单项下。

void CMenuTestView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	//若输出的是换行符
	if (0x0d == nChar) {
		//且换行符是第一次输入
		if (0 == ++m_nIndex) {
			m_menu.CreatePopupMenu();
			GetParent()->GetMenu()->AppendMenuW(MF_POPUP, (UINT)m_menu.m_hMenu, TEXT("菜单薄"));
			GetParent()->DrawMenuBar();
		}
		//得到空格第一次出现的位置
		int pos = m_strLine.Find(' ');
		//返回指定位置前的子字符串
		CString name = m_strLine.Left(pos);
		m_menu.AppendMenuW(MF_STRING, 111, name);
		m_strLine.Empty();
		Invalidate();
		
	}
	else
	{
		m_strLine += (wchar_t)nChar;
		dc.TextOutW(0, 0, m_strLine);
	}

	CView::OnChar(nChar, nRepCnt, nFlags);
}


✨✨2)设置动态增加的子菜单的标识符ID。

程序不能每次添加新菜单项时都使用这个菜单ID值。在Resource.h头文件中,设置一个标识符:#define IDM_PHONE1 32771。前面已经定义了一个变量:m_nIndex,当每次按下回车键增加菜单项时,它的值就增1。这样就可以利用这个变量加上IDM_PHONE1的值来确定当前的菜单ID。当第一次按下回车键后,m_nlndex的值是0,加上IDM_PHONE1后得到IDM_PHONE1,即第一个菜单项的ID;当第二次按下回车键后,m_nIndex的值增加为1,再加上IDM_PHONE1 (它的值为32771)后变成32772,也就是第二个菜单项的ID。因此利用这种方法,随着文字的输入,回车键的按下,m_nIndex的数值不断增加,得到的菜单项ID号也在不断变化。

再回到上述所示CMenuTestView的OnChar函数,找到动态添加菜单项的代码,即调用AppendMenu函数的那行代码,把其中的菜单ID参数改成IDM_PHONE1,即修改成下面这行代码:

m_menu.AppendMenu(MF_STRING,IDM_PHONE1+m_nIndex,m_strLine.Left(m_strLine.Find ('')));

5.4 动态点击子菜单显示具体电话信息

实现当单击这些菜单项时,应在程序窗口中显示对应的字符串,即:人名 电话号码。

✨1)保存所有字符串信息。

程序应该将所有输入的字符串都保存起来:
① 我们可以定义一个字符串数组来保存用户输入的所有字符串,但是因为不知道用户会输入多少个字符串,所以数组的大小无法确定。

② 对于这种动态增加的数组,可以通过链表来实现,但是这种方法很麻烦,也比较复杂。

③ MFC提供了一些非常有用的集合类。这些集合类类似于数组的功能,但可以很方便地动态增加和删除元素。可以利用一个名为CStringArray的集合类,这个集合类支持CString对象的数组。

🔲◼◾▪ 为了增加一个字符串元素,可以利用该集合类的Add成员函数,该函数的声明:

int Add( LPCTSTR newElement );

该函数的参数是一个指向常量字符串的指针(LPCTSTR类型)。类CString的成员函数重载了LPCTSTR操作符。所以当向CStringArray对象中添加元素时,可以直接给Add函数的参数传递一个CString类型的变量,编译器会自动完成转换。

🔲◼◾▪ 当需要返回一个集合元素时,可以利用集合类的GetAt成员函数。声明如下:

cstring GetAt( int nIndex ) const;

🟣🟣 1.1)为视类添加一个公有的CStringArray类型的成员变量:m_strArray,用于保存所有需要输出的字符串。

🟢🟢 1.2)然后在OnChar函数中,在按下回车键后,并在m_strLine变量清空之前,把当前输入的一行文字增加到这个集合类变量中:

void CMenuTestView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	//若输出的是换行符
	if (0x0d == nChar) {
		//且换行符是第一次输入
		if (0 == ++m_nIndex) {
			m_menu.CreatePopupMenu();
			GetParent()->GetMenu()->AppendMenuW(MF_POPUP, (UINT)m_menu.m_hMenu, TEXT("菜单薄"));
			GetParent()->DrawMenuBar();
		}
		//得到空格第一次出现的位置
		int pos = m_strLine.Find(' ');
		//返回指定位置前的子字符串
		CString name = m_strLine.Left(pos);
		m_menu.AppendMenuW(MF_STRING, 111, name);

		m_strArray.Add(m_strLine);

		m_strLine.Empty();
		Invalidate();
		
	}
	else
	{
		m_strLine += (wchar_t)nChar;
		dc.TextOutW(0, 0, m_strLine);
	}

	CView::OnChar(nChar, nRepCnt, nFlags);
}

✨✨ 2)动态响应命令函数
当单击动态添加的人名菜单项时,程序要在窗口中显示对应的字符串:人名 电话号码。这就需要首先对动态添加的菜单项进行命令捕获。

假设电话本最多有4个人的信息。现在资源头文件(Resource.h)中动态添加4个动态子菜单的标识符ID。

#define IDM_PHONE1                      32771
#define IDM_PHONE2                      32772
#define IDM_PHONE3                      32773
#define IDM_PHONE4                      32774

利用类向导为四个子菜单添加四个命令响应函数。

BEGIN_MESSAGE_MAP和END_MESSAGE_MAP宏之间就会生成四个COMMAND函数。

BEGIN_MESSAGE_MAP(CMenuTestView, CView)
	// 标准打印命令
	ON_COMMAND(ID_FILE_PRINT, &CView::OnFilePrint)
	ON_COMMAND(ID_FILE_PRINT_DIRECT, &CView::OnFilePrint)
	ON_COMMAND(ID_FILE_PRINT_PREVIEW, &CMenuTestView::OnFilePrintPreview)
	ON_WM_CONTEXTMENU()
	ON_WM_RBUTTONUP()
	ON_WM_CHAR()
	ON_COMMAND(IDM_PHONE1, &CMenuTestView::OnPhone1)
	ON_COMMAND(IDM_PHONE2, &CMenuTestView::OnPhone2)
	ON_COMMAND(IDM_PHONE3, &CMenuTestView::OnPhone3)
	ON_COMMAND(IDM_PHONE4, &CMenuTestView::OnPhone4)
END_MESSAGE_MAP()

对命令响应函数进行编辑。输出对应的电话本信息内容。

void CMenuTestView::OnPhone1()
{
	// TODO: 在此添加命令处理程序代码
	CClientDC dc(this);
	dc.TextOutW(0, 0, m_strArray.GetAt(0));
}


void CMenuTestView::OnPhone2()
{
	// TODO: 在此添加命令处理程序代码
	CClientDC dc(this);
	dc.TextOutW(0, 0, m_strArray.GetAt(1));
}


void CMenuTestView::OnPhone3()
{
	// TODO: 在此添加命令处理程序代码
	CClientDC dc(this);
	dc.TextOutW(0, 0, m_strArray.GetAt(2));
}


void CMenuTestView::OnPhone4()
{
	// TODO: 在此添加命令处理程序代码
	CClientDC dc(this);
	dc.TextOutW(0, 0, m_strArray.GetAt(3));
}

✨✨ 3)框架类窗口截获菜单命令消息

上面动态添加的菜单项命令由视类捕获,现在改成由框架类来捕获。根据前面“菜单命令的路由”一节中的内容,知道当框架类窗口接收到一个消息时,它首先会把消息交给其子窗口,即视类窗口去处理

🔳🔳 如何让框架类窗口首先捕获菜单命令并响应?
📋📋 菜单命令是交由OnCommand函数来处理的,在这个函数中将完成命令消息的路由。OnCommand函数是一个虚函数,可以在框架类中重写它,然后截获那些动态添加的菜单项的命令消息,让它们不再继续向下路由,把本该交由视类去响应的命令消息由框架类来处理。这个函数声明:

virtual BOOL OnCommand(WPARAM wParam, LPARAM 1Param );

🟣🟣 2.1)CMainFrame类添加一个OnCommand虚函数。

OnCommand函数代码如下:

BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam)
{
	// TODO: 在此添加专用代码和/或调用基类
	
	return CFrameWndEx::OnCommand(wParam, lParam);
}

这时程序运行时,当菜单命令由程序框架类(CFrameWnd类)的OnWndMsg函数交由OnCommand函数后,因为其子类: CMainFrame类重写了这个OnCommand函数,菜单命令消息就会先到子类的OnCommand函数中报到。后者最后将调用基类(即程序框架类: CFrameWnd)的OnCommand函数进行消息的路由。OnCommand函数是对所有的命令消息进行路由处理的,包括菜单、工具按钮,以及加速键的命令消息。

🟢🟢 2.2)在OnCommand函数中需要对到达的消息加以判断,检查该消息是否是我们需要的。

OnCommand这个函数带有两个参数,其中第一个参数的类型是WPARAM,这是一个4字节的无符号整型。在参数wParam低端的两个字节中放置的是发送当前消息的菜单项、工具按钮,或加速键的命令ID。因此就可以利用LOWORD这个宏取得当前消息的命令ID,然后判断其是否是程序中动态添加的菜单项,即当前消息的命令ID是否在这些菜单项ID范围之内。如果是,就处理;否则,就把消息交由基类继续路由

本例中要处理的子菜单项命令范围的下限就是刚才定义的IDM_PHONE1,但是并不知道在程序中会动态增加多少菜单项。为了让程序具有较好的可扩充性,在程序中就不应该用具体的数字来指定这个范围。因为已经把每次输入的字符串都保存到m_strArray这个集合类变量中了。集合类的成员函数GetSize可以获得集合变量中元素的个数,也就是输入的字符串个数

但是m_strArray是视类CMenuTestView的成员变量,在框架类CMainFrame中如何去调用视类的成员变量呢?
      首先需要获得视类对象,然后才能访问该对象的公有成员。可以利用CMainFrame类提供的GetActiveView成员函数,获取与框架相关联的当前视类的指针。这个函数的声明如下:

cview* GetActiveview() const;

      我们可以看到这个函数返回一个CView类型的指针,而程序需要的是CMenuTestView类型的指针,因此需要进行类型转换。有了这个视类指针,就可以调用其public类型的成员变量了。这就是先前把m_strArray定义为public类的原因。

BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam)
{
	// TODO: 在此添加专用代码和/或调用基类
	int MenuCmdID = LOWORD(wParam);
	CMenuTestView* pView = (CMenuTestView*)GetActiveView();
	if (MenuCmdID >= IDM_PHONE1 && MenuCmdID < IDM_PHONE1 + pView->m_strArray.GetSize()){

	}
	return CFrameWndEx::OnCommand(wParam, lParam);
}


因为在框架类CMainFrame中用到了视类CMenuTestView类型,所以应该在框架类的源文件中包含视类的头文件。即点击修改建议第二条:

发现框架源文件包含了视类的头文件。

运行程序,发现仍出现错误:

单击第一个错误信息,定位到了视类的头文件中。

错误原因并不是在“*”号之前加“;”。而是程序不认识CMenuTestDoc这个类。C++程序在编译时,只有源文件参与编译,在CMainFrame类的源文件前部加入了包含MenuTestview.h文件的代码。因此在编译CMainFrame类的源文件时,当遇到这行语句,就会展开MenuTestView.h文件的内容,但该文件中引用了尚未定义的CMenuTestDoc类。

📋📋 解决:

  1. 法一:可以把视类源文件中包含文档类的定义语句移到视类的头文件中,并放置在视类定义之前。即剪切MenuTestView.cpp文件中的#include "MenuTestDoc.h"这行语句,并将其粘贴到MenuTestView.h文件的前部。
  2. 法二:可以把视类源文件中包含文档类的定义语句移到视类的头文件中,并放置在视类定义之前。

本例中,先前已经对动态添加的菜单项为CMenuTestView类添加了命令响应函数,所以这里当框架类响应了这个消息后,视类仍会做出响应。为了只让框架类捕获到这些动态菜单命令消息,那么应该在框架类对这些消息做出响应之后,不要让它们再继续路由了。也就是框架类对这些消息处理完成之后返回一个TRUE值,这样这些消息就不会再交给基类CFrameWnd类的OnCommand函数,它们也就不会再继续路由了。

在CMainFrame类OnCommand函数体最后添加语句:return TRUE;

BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam)
{
	// TODO: 在此添加专用代码和/或调用基类
	//获得子菜单的ID号
	int MenuCmdID = LOWORD(wParam);
	//MessageBox(MenuCmdID+_T(" "));
	CMenuTestView* pView = (CMenuTestView *)GetActiveView();
	int menuNum = pView->m_strArray.GetSize();
	if (MenuCmdID >= IDM_PHONE1 && MenuCmdID < IDM_PHONE1 + menuNum){
		CClientDC dc(pView);
		dc.TextOutW(0, 0, pView->m_strArray.GetAt(MenuCmdID - IDM_PHONE1));
		return TRUE;
	}
	return CFrameWndEx::OnCommand(wParam, lParam);
}

根据前面的知识知道要进行绘图操作,首先就需要创建一个设备描述表。所以首先创建了一个CClientDC类对象:dc。但这时给这个类的构造函数传递的指针,并不是先前在视类中实现菜单命令响应函数时那样传递的是this指针。这里的this指针现在指向的是框架类:CMainFrame类。由于视类窗口一直位于框架类窗口之上,在程序窗口进行的绘图操作实际上是在视类窗口中进行的,所以这里应该创建的是与视类窗口相关联的设备描述表对象。

  • 0
    点赞
  • 0
    评论
  • 4
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:博客之星2020 设计师:CSDN官方博客 返回首页

打赏作者

WaitFoF

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值