图形界面 II: 主菜单元件 (第四章))·综合运用
🧩

图形界面 II: 主菜单元件 (第四章))·综合运用

(3/3)·从类构造到上下文菜单挂载,手把手拆完这套界面库最后一块拼图

案例拆解 第 3/3 篇
不少人在拼完按钮和文本框后就以为界面库完工,结果主菜单一挂上去,非活动控件还在抢点击、上下文菜单对不上项。这一章把菜单栏类、状态阻断和事件回路一次讲清,省得你后面改到怀疑人生。

主菜单点击与上下文菜单的联动收口

在自绘 UI 里,主菜单项被点击后第一道关卡是对象名前缀校验。代码用 StringFind 检查 clicked_object 是否包含 ProgramName()+"_menuitem_",若返回负值直接 return(false),这意味着任何非菜单对象(比如图表背景)的点击都不会误触发菜单逻辑。 如果命中菜单项,则从对象名反解出 id 与 index,再比对 CElement::Id()。只有 id 一致才继续,避免多实例窗体之间串号。接着判断 m_contextmenus[index] 指针是否有效:有效时,主菜单状态 m_menubar_state 取反于上下文菜单当前可见状态,并同步调用 m_wnd.IsLocked() 锁住表单;若此时主菜单被禁用,就发 ON_HIDE_CONTEXTMENUS 自定义事件把全部上下文菜单收掉。 OnHideContextMenus 负责真正执行隐藏:先取 ContextMenusTotal(0) 得到上下文菜单总数,循环调用 Hide();再取 MenuBarsTotal(0) 把所有主菜单 State(false) 置灰。注意这里 m_wnd[0] 是容器根窗体,下标写死 0 在单窗体场景没问题,多窗体时需改为动态索引否则会漏藏。 类里还留了两个私有/公有辅助:ChangeObjectsColor 在鼠标掠过时改色提升反馈,ActiveItemIndex 返回当前高亮项下标。开 MT5 把这段塞进你的面板基类,点几下菜单栏就能验证状态翻转是否符合预期,外汇与贵金属图表上跑这类 UI 需注意高波动时段重绘可能掉帧。

MQL5 / C++
class="type">bool CMenuBar::OnClickMenuItem(class="kw">const class="type">class="kw">string clicked_object)
  {
class=class="str">"cmt">//--- 如果没有点击在菜单项上, 退出
   if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",class="num">0)<class="num">0)
      class="kw">return(false);
class=class="str">"cmt">//--- 从对象名称获得ID和索引
   class="type">int id    =IdFromObjectName(clicked_object);
   class="type">int index =IndexFromObjectName(clicked_object);
class=class="str">"cmt">//--- 如果ID不匹配就退出
   if(id!=CElement::Id())
      class="kw">return(false);
class=class="str">"cmt">//--- 如果是一个上下文菜单项
   if(CheckPointer(m_contextmenus[index])!=POINTER_INVALID)
     {
      class=class="str">"cmt">//--- 主菜单的状态依赖于上下文菜单是否可见
      m_menubar_state=(m_contextmenus[index].ContextMenuState())? false : true;
      class=class="str">"cmt">//--- 设置表单的状态
      m_wnd.IsLocked(m_menubar_state);
      class=class="str">"cmt">//--- 如果主菜单被禁用
      if(!m_menubar_state)
        class=class="str">"cmt">//--- 发送一个用于隐藏所有上下文菜单的信号
        ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,class="num">0,class="num">0,"");
     }
class=class="str">"cmt">//--- 如果没有上下文菜单指针
   else
     {
      class=class="str">"cmt">//--- 发送一个用于隐藏所有上下文菜单的信号
      ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,class="num">0,class="num">0,"");
     }
class=class="str">"cmt">//---
   class="kw">return(true);
  }
class="type">bool CWndEvents::OnHideContextMenus(class="type">void)
  {
class=class="str">"cmt">//--- 如果是隐藏所有上下文菜单的信号
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS)
      class="kw">return(false);
class=class="str">"cmt">//--- 隐藏所有上下文菜单
   class="type">int cm_total=CWndContainer::ContextMenusTotal(class="num">0);
   for(class="type">int i=class="num">0; i<cm_total; i++)
      m_wnd[class="num">0].m_context_menus[i].Hide();
class=class="str">"cmt">//--- 禁用主菜单
   class="type">int menu_bars_total=CWndContainer::MenuBarsTotal(class="num">0);
   for(class="type">int i=class="num">0; i<menu_bars_total; i++)
      m_wnd[class="num">0].m_menu_bars[i].State(false);
class=class="str">"cmt">//---
   class="kw">return(true);
  }
class CMenuBar : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 当鼠标光标掠过时改变颜色
   class="type">void            ChangeObjectsColor(class="type">void);
class="kw">private:
   class=class="str">"cmt">//--- 返回主菜单的活动菜单项
   class="type">int             ActiveItemIndex(class="type">void);
  };

◍ 菜单栏悬停时的颜色与上下文联动

在 MT5 自定义控件里,菜单栏(CMenuBar)要在鼠标掠过主项时实时反馈,核心靠两个私有方法配合。ChangeObjectsColor 遍历 ItemsTotal() 返回的所有菜单项,逐个调用各自的颜色切换;ActiveItemIndex 则用 for 循环从 0 扫到 items_total-1,一旦 m_items[i].MouseFocus() 为真就记下 i 并 break,找不到时回传 WRONG_VALUE(-1)。 拿到活动项索引后,SwitchContextMenuByFocus 负责显隐上下文菜单。它先排除 CheckPointer 等于 POINTER_INVALID 的空指针项,只对 i==active_item_index 的项调 Show(),其余一律走隐藏分支。 隐藏分支里还有一层:被隐藏的上下文菜单自身可能挂了子菜单。代码会再嵌一个循环,用 cm.ItemsTotal() 拿到子项总数,对每个 CMenuItem 检查 TypeMenuItem() 是否等于 MI_HAS_CONTEXT_MENU,不是就 continue,是的话才进一步处理。这种两级指针校验能避免悬停切换时残留弹出层。 开 MT5 新建 EA 面板类时,直接把下面这段抄进 CMenuBar 实现,挂上 OnMouseMove 事件就能看到欧元、黄金图表上的菜单高亮随光标走;外汇与贵金属波动剧烈,这类 UI 只解决操作效率,不暗示任何方向概率。

MQL5 / C++
class="type">void CMenuBar::ChangeObjectsColor(class="type">void)
  {
   class="type">int items_total=ItemsTotal();
   for(class="type">int i=class="num">0; i<items_total; i++)
      m_items[i].ChangeObjectsColor();
  }

class="type">int CMenuBar::ActiveItemIndex(class="type">void)
  {
   class="type">int active_item_index=WRONG_VALUE;
   class="type">int items_total=ItemsTotal();
   for(class="type">int i=class="num">0; i<items_total; i++)
     {
      if(m_items[i].MouseFocus())
        {
         active_item_index=i;
         class="kw">break;
        }
     }
   class="kw">return(active_item_index);
  }

class="type">void CMenuBar::SwitchContextMenuByFocus(class="kw">const class="type">int active_item_index)
  {
   class="type">int items_total=ItemsTotal();
   for(class="type">int i=class="num">0; i<items_total; i++)
     {
      if(::CheckPointer(m_contextmenus[i])==POINTER_INVALID)
         class="kw">continue;
      if(i==active_item_index)
         m_contextmenus[i].Show();
      else
        {
         CContextMenu *cm=m_contextmenus[i];
         class="type">int cm_items_total=cm.ItemsTotal();
         for(class="type">int c=class="num">0; c<cm_items_total; c++)
           {
            CMenuItem *mi=cm.ItemPointerByIndex(c);
            if(::CheckPointer(mi)==POINTER_INVALID)
               class="kw">continue;
            if(mi.TypeMenuItem()!=MI_HAS_CONTEXT_MENU)
               class="kw">continue;
           }
        }
     }
  }

「菜单栏鼠标移动时的上下文联动」

在 MT5 自定义 GUI 里,主菜单栏的悬停切换依赖鼠标移动事件而非点击。CMenuBar::OnEvent 只在 id 等于 CHARTEVENT_MOUSE_MOVE 且 m_menubar_state 为真时介入,否则直接 return,避免无焦点时空转重绘。 先取 ActiveItemIndex(),若返回 WRONG_VALUE 也立即退出;这一步决定后续要不要改色和切上下文菜单。实测中若菜单项索引错位,WRONG_VALUE 会频繁触发,界面表现为悬停无反应。 确认有激活项后,ChangeObjectsColor() 重设焦点色,SwitchContextMenuByFocus() 按当前项索引显示对应下拉。隐藏旧菜单时用 EventChartCustom 发 ON_HIDE_BACK_CONTEXTMENUS 自定义事件,把同层其他上下文菜单收掉,再对 m_contextmenus[i] 调 Hide() 并 m_items[i].ResetColors() 清色。外汇与贵金属图表挂这类控件需注意:高频鼠标移动事件可能拖慢 tick 处理,建议在回测器外实盘前用 1 分钟图压测帧率。

MQL5 / C++
  class=class="str">"cmt">//--- 如果有上下文菜单,并且是激活状态
  if(mi.ContextMenuState())
    {
     class=class="str">"cmt">//--- 发送信号关闭本菜单项打开的所有上下文菜单
     ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),class="num">0,"");
     class="kw">break;
    }
   }
  class=class="str">"cmt">//--- 隐藏主菜单的上下文菜单
  m_contextmenus[i].Hide();
  class=class="str">"cmt">//--- 重置菜单项的颜色
  m_items[i].ResetColors();
  }
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuBar::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)
  {
  if(id==CHARTEVENT_MOUSE_MOVE)
    {
    class=class="str">"cmt">//--- 如果主菜单没有激活,退出
    if(!m_menubar_state)
      class="kw">return;
    class=class="str">"cmt">//--- 取得主菜单中激活的菜单项的索引
    class="type">int active_item_index=ActiveItemIndex();
    if(active_item_index==WRONG_VALUE)
      class="kw">return;
    class=class="str">"cmt">//--- 如果焦点改变,修改它的颜色
    ChangeObjectsColor();
    class=class="str">"cmt">//--- 根据主菜单激活的菜单项切换上下文菜单
    SwitchContextMenuByFocus(active_item_index);
    class="kw">return;
    }
  }

把独立菜单和嵌套上下文拼起来跑一遍

到这一步,前面几节搭的表单、独立菜单项、上下文菜单可以合在一起验证了。在表单上放几个独立菜单项,再补一个内部上下文菜单,把它挂到第三个上下文菜单的第二项下面,层级关系照原文截图那样排。 编译后把 EA 拖进图表,想复现图 5 的界面,直接载文章末尾的测试文件最快。末尾包里还带了一个同界面的测试指标,以及 MetaTrader 4 的对应版本,跨平台点一遍能确认菜单逻辑没写死在 MT5。 外汇和贵金属图表上挂这类 EA 界面属于高风险操作环境,菜单逻辑出错可能误触下单参数,验证时先用模拟账户跑。

◍ 把工具请下神坛

走到这一步,图形界面开发库里最绕的类结构和事件处理已经拆完,后面只剩控件拼装,难度曲线明显平缓。当前阶段库文件分两个包:MQL4 版 94.18 KB、MQL5 版 101.01 KB,都带示例 EA、指标和脚本,下载后直接丢进对应终端就能跑。 别把这套 GUI 库当成圣物,它只是帮你少写重复代码的轮子。真要验证价值,自己在 MT5 挂上 easyandfastgui_mql5 里的面板 EA,点几下菜单看上下文渲染是否完整——有用户反馈主菜单展开不全,这种坑只有实盘前亲手测出来才靠谱。 外汇和贵金属波动剧烈、杠杆高风险大,界面再顺手也不改仓位管理的底线。工具用熟了,剩下的时间该留给信号逻辑而不是拖控件。

把菜单状态诊断交给小布
这些控件激活态与事件回路的排查,小布盯盘的 AIGC 已内置了界面异常检测,打开对应品种页即可看到提示,你只管调逻辑。

常见问题

默认主菜单背景高 22 像素,菜单项高度按背景高自动推算,挂表前也可用 CElement::YSize() 改。
用指针存进数组可让同一菜单项动态绑不同上下文,避免重复建类,也方便后续换逻辑。
可以,小布内置的 AIGC 诊断会标出抢事件的隐藏控件,省去你逐行查消息映射。
倾向在表单消息分发前用状态标记过滤,让 CMenuBar 只响应激活态元件,减少误触。
要,档案内路径必须原样放,否则包含基类的相对引用会编译失败,详见基础篇结构说明。