加载中...
玩转主菜单
第1节:在Windows桌面打个叉
第2节:在窗口上跟踪输出鼠标坐标—Win32版
第3节:你好!wxWidgets
第4节:深入分析基于框架窗口的应用
第5节:玩转主菜单
第6节:简简单单,状态栏
课文封面
  • 学习wxMenuBar、wxMenu、wxMenuItem及彼此关系;
  • 学习如何创建菜单、普通菜单项、可选菜单项、单选菜单项、菜单项间的视觉分隔及逻辑分组;
  • 学习如何为菜单项挂接事件响应;
  • 学习两种常用的获取及使用菜单“选中”状态的方法。

1. 相关class

一个主菜单需要有一个菜单栏(wxMenuBar),一个菜单栏通常包含多个菜单(wxMenu),每个菜单之下,又可以包含多个菜单项(wxMenuItem)或子菜单(同样是wxMenu,本课不演示)。

三者关系示意如下:

菜单包含关系

菜单栏、菜单(包括子菜单)、菜单项除了视觉上的包含关系以外,同时也存在包含者是被包含者的 “Owner / 拥有” 关系,即:包含者被释放(delete)者时,将负责释放被包含的组件。也就是说,当 wxMenuBar 要被释放(delete)时,它会负责先释放其下的菜单,而后者又会负责释放其下的子菜单或菜单项。

那,菜单栏由谁负责呢?由它所在的窗口负责——这种父组件“拥有并负责释放”子组件的设计,不仅是wxWidgets 库的基础设计,也是其它多数GUI库的基础设计。

2. 视频

3. 创建新菜单

创建新菜单的常用方法是:通过 new wxMenu 得到一个菜单对象,然后通过 wxMenuBar 的 Append( … ) 方法加入:

wxMenu* optionsMenu = new wxMenu(_T("")); mbar->Append(optionsMenu, _("&Options"));

其中 mbar 是向导生成的框架构造函数中,wxMenuBar 的对象。

尽管 wxMenu 构造函数提供一个字符串入参,用以设置该菜单的标题,但通常此时仅设置为空,直到第二步,即调用 wxMenuBar 的 Append() 方法,才通过第二个入参真实设置所添加的菜单的标题。

4. 添加新菜单项

4.1 添加普通菜单项

无需显式 new wxMenuItem,而是通过菜单的 Append 方法添加,该方法三参数版各参数含义如下:

  • 入参1:待添加菜单的唯一ID。(指:在所属窗口类的范围唯一),比如本例中的 idMenuAboutAuthor
  • 入参2:菜单项标题。如果需使用包含汉字的标题,需配合使用 wxT("") 宏或 _T(""),并确保当前源文件使用 utf-8 编码;如考虑支持多国语言,则标题等默认内容应使用英文,并配合 _("")宏;另,标题中前面加 & 前缀的英文字母,将成为热键(通常在用户按下 Alt 时显示下划线)
  • 入参3:菜单项的功能提示。通常显示在状态栏。提示内容如涉及国际化,参看入参2说明。

示例:在 Help 菜单下添加 “About author” 菜单项,并设置 o 字母为快捷键。

helpMenu->Append(idMenuAboutAuthor, _("About auth&or"), _("Show info about this application's author"));

4.2 添加 Check 菜单项

添加方法改用 wxMenu 的 AppendCheckItem( … ),三个入参同 Append ( … ) 版本。

示例:在 Options 菜单下添加 “Show &motion info” 可选菜单项:

optionsMenu->AppendCheckItem(idMenuShowMotionInfo, _("Show &motion info"), _T("Show motion info or no"));

4.3 添加 Radio 菜单项

添加方法改用 wxMenu 的 AppendRadioItem( … ),三个入参同 Append ( … ) 版本。

RadioItem,即单选菜单项,通常成组出现,除非中间存在分割线(Seperator)或其它非单选菜单项,否则连续排列的所有单选项,将划归一组,用户每次仅能在一组当中选中一项。

示例:在 Options 菜单下,添加一组颜色相关的 单选菜单项:

optionsMenu->AppendRadioItem(idMenuBlueColor, _("&Blue text"), _("Set text blue")); optionsMenu->AppendRadioItem(idMenuRedColor, _("&Red text"), _("Set text red")); optionsMenu->AppendRadioItem(idMenuGreenColor, _("&Green text"), _("Set text green"));

4.4 添加菜单项分隔线 (Seperator)

在需要分隔的位置,调用菜单(wxMenu)对象的 AppendSeparator() 方法即可。

示例:

wxMenu* optionsMenu = new wxMenu(_T("")); mbar->Append(optionsMenu, _("&Options")); optionsMenu->AppendCheckItem(idMenuShowMotionInfo, _("Show &motion info"), _("Show motion info or no")); optionsMenu->AppendSeparator(); // 分隔线1,纯视频分隔效果 optionsMenu->AppendRadioItem(idMenuBlueColor, _("&Blue text"), _("Set text blue")); optionsMenu->AppendRadioItem(idMenuRedColor, _("&Red text"), _("Set text red")); optionsMenu->AppendRadioItem(idMenuGreenColor, _("&Green text"), _("Set text green")); optionsMenu->AppendSeparator(); // 分隔线2,同时逻辑分组上下两组单选菜单项 optionsMenu->AppendRadioItem(idMenuGreenColor + 100, _("Big text"), _("Set big text")); optionsMenu->AppendRadioItem(idMenuGreenColor + 101, _("Small text"), _("Set small text"));

5. 挂接菜单项点击事件

通过绑定一个方法(成员函数),以实现菜单项被点击(或热键等其它方式激活)后,调用该方法。此类方法原型为:

void OnMenuItem (wxCommandEvent& event);

重点在于事件类型: wxCommandEvent。该类提供 GetId() 方法以获得当前被触发(比如点击)的菜单项的唯一ID。

挂接(或称绑定)菜单项的点击事件及对应的响应函数,可在窗口的事件表中,通过 EVT_MENU 宏 实现,如:

BEGIN_EVENT_TABLE(Ls11Frame, wxFrame) ... EVT_MENU(idMenuQuit, Ls11Frame::OnQuit) EVT_MENU(idMenuAbout, Ls11Frame::OnAbout) EVT_MENU(idMenuAboutAuthor, Ls11Frame::OnAboutAuthor) EVT_MENU(idMenuBlueColor, Ls11Frame::OnTextColorSelected) EVT_MENU(idMenuRedColor, Ls11Frame::OnTextColorSelected) EVT_MENU(idMenuGreenColor, Ls11Frame::OnTextColorSelected) ... END_EVENT_TABLE()

6. 获取选中状态

普通菜单项(wxMenuItem)不存在状态,可选(Check)和单选(Radio)菜单项拥有“是否选中”的状态。

通常,在事件响应函数中,我们并不通过菜单项本身来获得这一状态(感觉有那么一点点不“面向对象:),而是借助菜单项所在的菜单栏,来获取(检索)这一状态。不过,在事件响应函数中,通常我们也没有“菜单栏”对象的变量,怎么办?

方法是:首先,通过 this->GetMenuBar() 先获得当前框架式窗口(即 this 代表的对象)的菜单栏。

注意,仅框架式窗口,即 wxFrame 及其派生类拥有 GetMenuBar() 方法,普通窗口类并不拥有。

然后,通过菜单栏的 IsChecked(菜单项ID) 以查询指定的菜单项的选中状态。

如果对一个普通的菜单项(非Check、非Radio)执行此操作,将永远得到 false。

6.1 可选菜单项状态获取惯用法

以下是在 OnPaint() 函数中主动获取特定 Check 菜单项是否选中,从而决定是否在窗口上显示当前鼠标位置的示例应用:

void Ls11Frame::OnPaint(wxPaintEvent& event) { // 示例:获取 CheckItem 菜单项的选中状态 bool showInfo = this->GetMenuBar()->IsChecked(idMenuShowMotionInfo); wxString txt; if (showInfo) { txt << wxT("鼠标位置:") << xPos << wxT(" - ") << yPos; } else { txt = wxT("你可以选中\"Show motion info\"来显示鼠标位置"); } ... }

6.2 单选菜单项状态获取惯用法

作为对比,Radio 菜单项成组出现,因此,如果仍然使用 wxMenuBar 的 IsChecked() 来检索,N 个选项的菜单项,就至少需要写 N - 1 个判断 (最后一个直接使用 else ),比如:

/* 示例,相对“麻烦”的写法 */ bool isBlue = false, isRed = false, isGreen = false; // 是否蓝色? isBlue = this->GetMenuBar()->IsChecked(idMenuBlueColor); ifthis->GetMenuBar()->IsChecked(idMenuBlueColor)) { isBlue = true; } else if (this->GetMenuBar()->IsChecked(idMenuRedColor)) { isRed = true; } else // 都不是,那就是绿色的 { isGreen = true; } ...

此时,为每个单选项挂接一个事件响应函数,从而在不同的函数内做出不的处理,是最简单也最常见的方法,不过,有些时候,我们还可以使用另一种惯用法:为同一组的所有单选菜单项,创建并挂接同一个事件响应函数,并于该函数中记录当前选中的是哪一项。

比如:

// 三个颜色菜单项点击时,均调用以下函数: void Ls11Frame::OnTextColorSelected(wxCommandEvent& event) { this->selectedColorMenuItemId = event.GetId(); // 记录选中的菜单ID }

有了状态数据之后,可通过 switch / case 等 流程结构 ,做出相应处理;或者,如果需求是通过 “点击不同菜单项”,从而得到不同的数据——比如本例中的不同颜色——则可以借助 数据结构 以得到更优雅的实现。本例中,我们使用的数据结构是字典(也称映射),具体类型是 C++ 标准库的 std::map 。

// 一个静态数组,以避免每次调用当前函数都需要重新生成 static std::map<int, wxColor const*> colors = { { idMenuBlueColor, wxBLUE }, { idMenuRedColor, wxRED }, { idMenuGreenColor, wxGREEN } }; wxPaintDC dc(this); // 菜单ID -> 颜色 if (auto c = colors[this->selectedColorMenuItemId]; c) { dc.SetTextForeground(*c); }