图形界面 II: 主菜单元件 (第四章))(基础篇)
📘

图形界面 II: 主菜单元件 (第四章))(基础篇)

第 1/3 篇

从零搭一个 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 把同结构补进你自己的容器类就能跑通。

MQL5 / C++
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 的测试图表验证。

MQL5 / C++
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 处理,属于高风险界面的性能坑。

MQL5 / C++
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 策略测试器外的普通图表先验证内存行为。

MQL5 / C++
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)
  {

常见问题

先建菜单栏基类处理坐标与消息路由,再写子菜单项类存标签和回调,最后用容器类把两者挂一起,配色和虚函数留接口即可。
在对象构造末尾调颜色初始化函数统一设默认色,运行时状态切换只改画刷不重建对象,避免闪烁。
可以,把代码贴给小布,它会对照标准骨架标出未实现的虚函数和缺失的颜色状态初始化项。
多是容器没存返回值或坐标偏移算错,确认添加函数返回 CMenuItem* 并在父栏里用数组保住指针。
用 Set 函数封装更好,外部改色只调接口不碰内部画刷,换肤时循环调一次即可全栏更新。