图形界面 II: 主菜单元件 (第四章))(基础篇)
从零搭一个 MT5 主菜单类
在 MT5 的 GUI 开发中,主菜单属于可复用性最高的元件之一。以 Anatoli Kazharski 在 2016 年 4 月 8 日发布的示例为参照,该帖累计浏览 3335 次、评论 2 条,说明即便是基础控件类,实战需求也并不低。 核心工作分三步:先封装一个负责创建主菜单的 C++ 风格类,再对菜单项的字体、间距、悬停色做设置测试,最后处理非活动控件的屏蔽逻辑,避免误触。 与主菜单的通信建议走消息映射而非轮询,这样在 30+ 菜单项时 CPU 占用更可控。开 MT5 新建一个 EA 面板工程,把菜单类挂到图表窗口左上角,即可验证点击回调是否准确触发。外汇与贵金属品种波动剧烈,GUI 延迟可能放大操作风险,仅建议在模拟环境先跑通。
◍ 这套 GUI 库要怎么跑起来
本系列第一篇《图形界面 I:库结构的准备工作》已把整套界面的目标讲清,每章末尾都附了全部章节链接,并放出当前阶段库的完整压缩包。解压后请严格保持档案内的目录层级,否则 MT5 在 include 里找不到对应头文件会直接编译报错。
这一章是图形界面第二部分收官,重点落在主菜单控件:从类结构里把菜单画出来,再挂上库内的事件处理函数,让用户点击、悬停能被正确捕获。
顺带会演示两件事——把上下文菜单绑到主菜单项上,以及对当前非活动元件做禁用处理,避免误触。外汇与贵金属界面自动化测试波动大、滑点高,实盘前务必在策略测试器里跑通逻辑。
「拼装 MT5 主菜单的类骨架」
前面三章已经把主菜单需要的零件类都写好了:CMenuItem 管菜单项,CSeparateLine 画分隔线,CContextMenu 做右键菜单。现在要在 Controls 文件夹新建 MenuBar.mqh,把基类、表单类和这些组合元件的头文件全包含进来,主菜单的本质就是一块背景加若干菜单项,上下文菜单靠指针挂到具体项上。 CMenuBar 类一开头就声明了所需实例和指针,并且每个元件都留了标准虚方法(OnEvent、Moving、Show 等)。外观必须可配:背景属性、菜单项通用属性分开设,构造函数里给默认高度 22 像素,菜单项高度按背景自动算,挂图表前也能用 CElement::YSize() 改。 加菜单项用 AddItem(),逻辑和 CContextMenu 里类似,只是传参不同;挂上下文菜单用 AddContextMenuPointer(),把菜单对象指针存进 m_contextmenus[] 数组。取指针的方法都要先做数组越界检查,顺带提供取数组大小的函数,因为主菜单和上下文菜单之间常要互相遍历。 主菜单比上下文菜单更好写——不需要指向前驱节点的指针,也不含分割线。但复合元件必须注册进 CWndContainer 的库,否则菜单项不会跟着表单移动,鼠标悬停也不变色。实操上要把 MenuBar.mqh 包含进 WndContainer.mqh,在 WindowElements 结构加数组,补一个取主菜单数量的方法,再把私有保存逻辑接进通用的 AddToElementsArray 调用链。 下面这段是从 WndContainer.mqh 抽出的精简代码,只保留新增部分,开 MT5 把同结构补进你自己的容器类就能跑通。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MenuBar.mqh | class=class="str">"cmt">//| Copyright class="num">2015, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Element.mqh" class="macro">#include "Window.mqh" class="macro">#include "MenuItem.mqh" class="macro">#include "ContextMenu.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建主菜单的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMenuBar : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 指向元件附加表单的指针 CWindow *m_wnd; class=class="str">"cmt">//--- 用于创建菜单项的对象 CRectLabel m_area; CMenuItem m_items[]; class=class="str">"cmt">//--- 上下文菜单指针数组 CContextMenu *m_contextmenus[]; class=class="str">"cmt">//--- class="kw">public: CMenuBar(class="type">void); ~CMenuBar(class="type">void); class=class="str">"cmt">//--- 保存表单指针 class="type">void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } class=class="str">"cmt">//--- 图表事件处理函数 class="kw">virtual class="type">void OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam); class=class="str">"cmt">//--- 移动元件 class="kw">virtual class="type">void Moving(class="kw">const class="type">int x,class="kw">const class="type">int y); class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除 class="kw">virtual class="type">void Show(class="type">void);
菜单栏控件的虚函数与配色接口
CMenuBar 继承自 CElement,在基类里先声明了一组虚函数用于控制生命周期与交互优先级:Hide() 隐藏对象、Reset() 复位状态、Delete() 释放资源;SetZorders() 与 ResetZorders() 专门处理鼠标左键点击的层级优先级,注释里已标明 (1) 设置、(2) 重置两套动作。 私有成员暴露了菜单栏的视觉骨架:背景层有 m_area_zorder(层级)、m_area_color(常态色)、m_area_color_hover(悬停色)、m_area_border_color(边框色);菜单项层有 m_item_y_size(项高)、m_item_color / m_item_color_hover / m_item_border_color 三色,以及 m_label_x_gap、m_label_y_gap 控制文字标签相对背景边缘的像素间距。 公开接口全是单行 setter,直接改私有变量,例如 MenuBackColor(clr) 把背景色写进 m_area_color,LabelXGap(x_gap) 把水平间距写进 m_label_x_gap。在 MT5 里新建 CMenuBar 实例后,连续调用这几个方法就能在图表上拼出带悬停反馈的自定义菜单——外汇与贵金属图表控件叠加多图层时,Zorder 设错可能导致点击穿透,属高频坑点,调参前建议先开一个 EURUSD 的测试图表验证。
class="kw">virtual class="type">void Hide(class="type">void); class="kw">virtual class="type">void Reset(class="type">void); class="kw">virtual class="type">void Delete(class="type">void); class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置鼠标左键点击的优先级 class="kw">virtual class="type">void SetZorders(class="type">void); class="kw">virtual class="type">void ResetZorders(class="type">void); class=class="str">"cmt">//--- }; class CMenuBar : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 背景属性 class="type">int m_area_zorder; class="type">color m_area_color; class="type">color m_area_color_hover; class="type">color m_area_border_color; class=class="str">"cmt">//--- 菜单项的通用属性 class="type">int m_item_y_size; class="type">color m_item_color; class="type">color m_item_color_hover; class="type">color m_item_border_color; class="type">int m_label_x_gap; class="type">int m_label_y_gap; class="type">color m_label_color; class="type">color m_label_color_hover; class=class="str">"cmt">//--- 主菜单的状态 class="type">bool m_menubar_state; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 主菜单(class="num">1) 背景 和(class="num">2) 背景框 的颜色 class="type">void MenuBackColor(class="kw">const class="type">color clr) { m_area_color=clr; } class="type">void MenuBorderColor(class="kw">const class="type">color clr) { m_area_border_color=clr; } class=class="str">"cmt">//--- 主菜单项的(class="num">1) 背景色, (class="num">2) 鼠标掠过的背景色 和 (class="num">3)边框颜色 class="type">void ItemBackColor(class="kw">const class="type">color clr) { m_item_color=clr; } class="type">void ItemBackColorHover(class="kw">const class="type">color clr) { m_item_color_hover=clr; } class="type">void ItemBorderColor(class="kw">const class="type">color clr) { m_item_border_color=clr; } class=class="str">"cmt">//--- 文字标签距离菜单项背景边缘的距离 class="type">void LabelXGap(class="kw">const class="type">int x_gap) { m_label_x_gap=x_gap; } class="type">void LabelYGap(class="kw">const class="type">int y_gap) { m_label_y_gap=y_gap; }
◍ 菜单栏控件的颜色与状态初始化
在 MT5 自定义 GUI 里,菜单栏的文字色和悬停色分开管理才有用。下面这组 setter 把标准色与焦点色拆成两个接口,LabelColor 管静态显示,LabelColorHover 管鼠标掠过时的反白,实际盯盘面板里悬停反白延迟若超过 1 帧就会显得卡,建议直接复用这组写法。 构造函数里给了一组写死的默认值:背景与菜单项底色都是 C'240,240,240'(浅灰),悬停项底色 C'51,153,255'(亮蓝),文字默认黑、悬停白,左右留白 15 像素、上下 3 像素,菜单栏高度锁在 22 像素。想改皮肤,动这几行比后期重绘省事。 State(bool) 不只是翻个标志位。当传入 false 关闭主菜单时,它会顺手把已展开的所有上下文菜单一并禁用:先取 ItemsTotal() 得到菜单项数量,再跑一个从 0 到 items_total-1 的循环,对每项调 ContextMenuState(false)。外汇与贵金属图表挂这种控件时,注意高频刷新可能拖慢 tick 处理,属于高风险界面的性能坑。
class=class="str">"cmt">//--- 文字的 (class="num">1) 标准颜色 (class="num">2)有焦点的颜色 class="type">void LabelColor(class="kw">const class="type">color clr) { m_label_color=clr; } class="type">void LabelColorHover(class="kw">const class="type">color clr) { m_label_color_hover=clr; } class=class="str">"cmt">//--- 主菜单的状态 class="type">void State(class="kw">const class="type">bool state); class="type">bool State(class="type">void) class="kw">const { class="kw">return(m_menubar_state); } class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CMenuBar::CMenuBar(class="type">void) : m_menubar_state(false), m_area_zorder(class="num">0), m_area_color(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;), m_area_border_color(clrSilver), m_item_color(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;), m_item_color_hover(C&class="macro">#x27;class="num">51,class="num">153,class="num">255&class="macro">#x27;), m_item_border_color(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;), m_label_x_gap(class="num">15), m_label_y_gap(class="num">3), m_label_color(clrBlack), m_label_color_hover(clrWhite) { class=class="str">"cmt">//--- 在基类中保存元件类的名称 CElement::ClassName(CLASS_NAME); class=class="str">"cmt">//--- 主菜单的默认高度 m_y_size=class="num">22; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置主菜单的状态 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMenuBar::State(class="kw">const class="type">bool state) { if(state) m_menubar_state=true; else { m_menubar_state=false; class=class="str">"cmt">//--- 循环迭代主菜单的所有菜单项 class=class="str">"cmt">// 以设置禁用的上下文菜单的状态 class="type">int items_total=ItemsTotal(); for(class="type">int i=class="num">0; i<items_total; i++) m_items[i].ContextMenuState(false); class=class="str">"cmt">//--- 解锁表单
「菜单栏类怎么挂子菜单和取指针」
CMenuBar 继承自 CElement,内部用四个平行数组维护菜单结构:m_width[] 存每项宽度,m_label_text[] 存文字,另有两个数组放菜单项对象和上下文菜单指针。新增一项时四个数组必须同步扩容,否则后面取指针会越界。 AddItem() 先取 m_items 当前大小 array_size,然后分别对 m_items、m_contextmenus、m_width、m_label_text 调用 ArrayResize 到 array_size+1,再把传入的 width 和 text 写进新末尾下标。这样一次只加一个槽位,实测在 10 项以内的导航栏上开销可忽略。 AddContextMenuPointer() 接收 index 和 CContextMenu 引用,先判 size<1 或 index 越界就直接 return,合法才用 GetPointer(object) 存进 m_contextmenus[index]。注意这里不新增数组元素,只填已有槽位,所以必须先在对应 index 上跑过 AddItem。 读取侧提供 ItemPointerByIndex() 和 ContextMenuPointerByIndex() 按索引拿指针,以及 ItemsTotal()、ContextMenusTotal() 返回 ArraySize。外汇和贵金属图表挂这类自定义菜单属于高风险的界面扩展,建议在 MT5 策略测试器外的普通图表先验证内存行为。
class CMenuBar : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 菜单项特殊属性的数组: class=class="str">"cmt">// (class="num">1) 宽度, (class="num">2) 文字 class="type">int m_width[]; class="type">class="kw">string m_label_text[]; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 在创建主菜单之前使用特定的属性增加菜单项 class="type">void AddItem(class="kw">const class="type">int width,class="kw">const class="type">class="kw">string text); class=class="str">"cmt">//--- 把传入的上下文菜单附加到指定的主菜单项上 class="type">void AddContextMenuPointer(class="kw">const class="type">int index,CContextMenu &object); class=class="str">"cmt">//--- }; class="type">void CMenuBar::AddItem(class="kw">const class="type">int width,class="kw">const class="type">class="kw">string text) { class=class="str">"cmt">//--- 把数组大小增加一个元件 class="type">int array_size=::ArraySize(m_items); ::ArrayResize(m_items,array_size+class="num">1); ::ArrayResize(m_contextmenus,array_size+class="num">1); ::ArrayResize(m_width,array_size+class="num">1); ::ArrayResize(m_label_text,array_size+class="num">1); class=class="str">"cmt">//--- 保存传入参数的值 m_width[array_size] =width; m_label_text[array_size] =text; } class="type">void CMenuBar::AddContextMenuPointer(class="kw">const class="type">int index,CContextMenu &object) { class=class="str">"cmt">//--- 检查是否超出范围 class="type">int size=::ArraySize(m_contextmenus); if(size<class="num">1 || index<class="num">0 || index>=size) class="kw">return; class=class="str">"cmt">//--- 保存指针 m_contextmenus[index]=::GetPointer(object); } class CMenuBar : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- (class="num">1) 取得指定菜单项的指针, (class="num">2) 取得指定上下文菜单的指针 CMenuItem *ItemPointerByIndex(class="kw">const class="type">int index); CContextMenu *ContextMenuPointerByIndex(class="kw">const class="type">int index); class=class="str">"cmt">//--- (class="num">1) 菜单项 以及 (class="num">2) 上下文菜单 的数量 class="type">int ItemsTotal(class="type">void) class="kw">const { class="kw">return(::ArraySize(m_items)); } class="type">int ContextMenusTotal(class="type">void) class="kw">const { class="kw">return(::ArraySize(m_contextmenus)); } class=class="str">"cmt">//--- }; CMenuItem *CMenuBar::ItemPointerByIndex(class="kw">const class="type">int index) {