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);
if(this->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);
}