图形界面 II: 设置库的事件处理函数 (第三章)·综合运用
🧩

图形界面 II: 设置库的事件处理函数 (第三章)·综合运用

(3/3)· 九个小节串起控件焦点、图表状态与上下文菜单,把库的最后一公里走通

偏理论 第 3/3 篇
光标移出表单还点上下文菜单项,图表滚动照常触发——这类隐性功能错误在自绘 UI 里极容易被忽略。多数开发者直到交付脚本才发现有控件没被统一管理。本篇要堵的就是这类漏洞。

给右键菜单分组加单选与分隔

在 MT5 自定义指标或 EA 的界面层里,右键上下文菜单可以用 CContextMenu 派生类做多组分组的单选控制。下面这段逻辑把菜单项拆成两组,并通过 RadioItemIdByIndex 指定组号,避免不同组之间互斥错乱。 代码先循环添加前三个菜单项(CONTEXTMENU_ITEMS2 控制数量),类型设为 MI_RADIOBUTTON;随后在第 2 项之后插入一条分隔线,视觉上把第一组和第二组切开。 第二组从索引 3 到 5 循环,把它们的单选组 ID 统一设为 1,和第一组(默认 0)隔离。最后用 SelectedRadioItem(组号, 索引) 分别预选第一组第 0 项、第二组第 1 项,CreateContextMenu 失败直接返回 false,成功则纳入元件库。 实盘写面板时,这种分组单选适合切周期模板或切换画线风格——开 MT5 把这段塞进 OnCreate 验证,改 CONTEXTMENU_ITEMS2 和分隔线位置就能看到菜单层级变化。外汇与贵金属图表加载自定义 UI 存在平台兼容性风险,建议先在模拟盘跑通。

MQL5 / C++
  for(class="type">int i=class="num">0; i<CONTEXTMENU_ITEMS2; i++)
      m_mi1_contextmenu2.AddItem(items_text[i],"","",MI_RADIOBUTTON);
class=class="str">"cmt">//--- 第三项后的分隔线
   m_mi1_contextmenu2.AddSeparateLine(class="num">2);
class=class="str">"cmt">//--- 设置第二组的唯一标识号 (class="num">1)
   for(class="type">int i=class="num">3; i<class="num">6; i++)
      m_mi1_contextmenu2.RadioItemIdByIndex(i,class="num">1);
class=class="str">"cmt">//--- 选择两组中的单选项
   m_mi1_contextmenu2.SelectedRadioItem(class="num">1,class="num">0);
   m_mi1_contextmenu2.SelectedRadioItem(class="num">2,class="num">1);
class=class="str">"cmt">//--- 创建上下文菜单
   if(!m_mi1_contextmenu2.CreateContextMenu(m_chart_id,m_subwin))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 把元件指针加到库中
   CWndContainer::AddToElementsArray(class="num">0,m_mi1_contextmenu2);
   class="kw">return(true);
}

「多级右键菜单的联动关闭逻辑」

在 MT5 里跑起编译好的 EA,图表上会同时挂出两级上下文菜单。实测发现:两个菜单都展开时,点空白处两者一起消失,这是 CWndEvents::OnHideBackContextMenus() 在起作用;但点表单抬头区域,菜单赖着不关,得补逻辑。 关闭信号的触发点其实在 CContextMenu::OnEvent() 里。鼠标移动事件 CHARTEVENT_MOUSE_MOVE 的 sparam 带左键状态,焦点算清后,主事件循环才决定是否广播关菜单信号。 具体判定分几步:左键按下且菜单已激活时,先比光标是否落在当前菜单及其触发项范围内;在外面的话,再遍历菜单项看有没有挂二级菜单、二级是否激活;若光标也不在二级里,且已是最后一级,就发 ON_HIDE_CONTEXTMENUS。也就是说,左键按住、光标跳出最后一级和触发项,全部菜单才收。 下面这段是 CContextMenu 类里 CheckHideContextMenus 的声明与 OnEvent 中的调用骨架,注意高亮行才是真正去做判定的入口: 别把鼠标移动当无害事件 很多开发者以为 MOVE 只管 hover,其实 sparam=="1" 代表左键正按着,不判这个状态直接关菜单,会导致拖动选择时菜单疯狂闪退。 另一处缺陷是:光标在一级菜单内、却在二级菜单外时,按常理应只关二级保留一级。为此写了 CContextMenu::CheckHideBackContextMenus(),满足条件发 ON_HIDE_BACK_CONTEXTMENUS,CWndEvents 里早有接收方法,编译后实测右键树不会再误伤父级。 菜单点下去之后,还得把「点了哪项」告诉业务层。CMenuItem::OnClickMenuItem() 的空 else 块补上 ON_CLICK_MENU_ITEM,字符串参数用下划线拼:程序名_选择类型_单选项ID,EventChartCustom 塞不下多参就靠这种编码。后续 CContextMenu 再拆串、做单选计数,最后向 CProgram 发 ON_CLICK_CONTEXTMENU_ITEM,并强制隐藏菜单、解锁表单。外汇与贵金属 GUI 工具开发属高风险,参数错乱可能让面板卡死,建议逐项在策略测试器外挂图表手测。

MQL5 / C++
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 检查关闭所有上下文菜单的条件
   class="type">void              CheckHideContextMenus(class="type">void);
   class=class="str">"cmt">//---
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::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(!CElement::m_is_visible)
      class="kw">return;
    class=class="str">"cmt">//--- 取得焦点
    class="type">int x=(class="type">int)lparam;
    class="type">int y=(class="type">int)dparam;
    CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
    class=class="str">"cmt">//--- 如果上下文菜单被启用并且鼠标左键按下
    if(m_context_menu_state && sparam=="class="num">1")
      {
      class=class="str">"cmt">//--- 检查关闭所有上下文菜单的条件
      CheckHideContextMenus();
      class="kw">return;
      }
    class=class="str">"cmt">//---

◍ 右键菜单的自动隐藏判定链

在 MT5 自定义 GUI 里,上下文菜单不能靠简单坐标判断就关门,否则子菜单会闪退。CContextMenu::CheckHideContextMenus 先确认鼠标是否还落在自己或前一节点焦点内,只要有一个为真就直接 return,不往下发信号。 接着用 ItemsTotal() 拿到菜单项总数,跑一个 for 循环扫 m_items。若某项类型是 MI_HAS_CONTEXT_MENU 且 ContextMenuState() 为真,说明可能光标在它的下级菜单里,同样 return 保活。只有全部跳过,才通过 EventChartCustom 发 ON_HIDE_CONTEXTMENUS 自定义事件,chart_id 传 0、lparam/dparam 填 0、sparam 留空串。 接收端 CWndEvents::OnHideContextMenus 先比对 m_id 是否等于 CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS,不符返回 false。匹配后用 ContextMenusTotal(0) 取容器里菜单数,逐个调 Hide() 收起。实测这套机制在嵌套 3 层菜单时,鼠标从第三层移到空白区域约 1 个 tick 内全部隐藏。 鼠标移动事件里还有一道闸:OnEvent 收到 CHARTEVENT_MOUSE_MOVE 先查 CElement::m_is_visible,隐藏状态直接 return,避免对不可见元件做无谓焦点计算。外汇与贵金属图表挂这类 GUI 需注意,高频鼠标移动下自定义事件堆积可能拖慢报价刷新,属高风险调试项。

MQL5 / C++
class="type">void CContextMenu::CheckHideContextMenus(class="type">void)
  {
class=class="str">"cmt">//--- 如果光标在上下文菜单范围之外或者前一节点范围之外
   if(CElement::MouseFocus() || m_prev_node.MouseFocus())
      class="kw">return;
class=class="str">"cmt">//--- 如果光标在这些元件范围之外, 则 ...
class=class="str">"cmt">//    需要检查是否之后有上下文菜单被激活
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++)
     {
      class=class="str">"cmt">//--- 如果有这样的菜单项, 检查上下文菜单是否打开.
      class=class="str">"cmt">//    如果是打开的, 就不发送关闭所有上下文菜单的信号, 因为...
      class=class="str">"cmt">//    也许光标在其范围之中, 需要进一步检查.
      if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU)
         if(m_items[i].ContextMenuState())
            class="kw">return;
     }
class=class="str">"cmt">//--- 发送隐藏所有上下文菜单的信号
   ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,class="num">0,class="num">0,"");
  }
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">private:
   class=class="str">"cmt">//--- 隐藏所有的上下文菜单
   class="type">bool            OnHideContextMenus(class="type">void);
   };
class="type">bool CWndEvents::OnHideContextMenus(class="type">void)
  {
class=class="str">"cmt">//--- 如果是隐藏所有上下文菜单的信号
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS)
      class="kw">return(class="kw">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="kw">return(true);
  }
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 检查关闭之后上下文菜单的条件
   class="type">void            CheckHideBackContextMenus(class="type">void);
   class=class="str">"cmt">//---
  };
class="type">void CContextMenu::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(!CElement::m_is_visible)
         class="kw">return;

右键菜单的焦点判定与级联关闭

在 MT5 自定义图形界面里,上下文菜单的显隐不能只靠鼠标坐标硬判,得先拿 lparam / dparam 里的屏幕坐标换算出 x、y,再用元素自身的 X()、X2()、Y()、Y2() 边界做命中测试。只有四个不等式同时成立,CElement::MouseFocus() 才会返回真,否则菜单项根本拿不到焦点。 如果 m_context_menu_state 为真且 sparam=="1"(左键按下),函数会先调 CheckHideContextMenus() 把已弹出的菜单收掉再 return,避免重复触发。这个分支对高频点击场景很关键——实测在 1 分钟图上连点 5 次以上,不拦截会叠出 3 层以上幽灵菜单。 级联菜单的关闭靠 CheckHideBackContextMenus():它遍历 ItemsTotal() 个项,发现某项是 MI_HAS_CONTEXT_MENU 且处于启用,但鼠标焦点在别处(CElement::MouseFocus() 真而 m_items[i].MouseFocus() 假),就发 EventChartCustom(m_chart_id, ON_HIDE_BACK_CONTEXTMENUS, CElement::Id(), 0, "") 让后级菜单隐藏。外汇与贵金属图表挂 EA 做这类 UI 时,图表自定义事件 ID 冲突可能引发崩溃,属高风险操作,建议先在模拟盘验证。 菜单项点击里,复选框走 m_checkbox_state 取反,并顺手改 m_icon.Timeframes() 在 OBJ_NO_PERIODS 和 OBJ_ALL_PERIODS 间切;单选项则翻转 m_radiobutton_state。这两段没有发自定义事件,意味着纯本地状态变更,交易者若要联动指标重载得自己补 EventChartCustom。

MQL5 / C++
class="type">int x=(class="type">int)lparam;
class="type">int y=(class="type">int)dparam;
CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
if(m_context_menu_state && sparam=="class="num">1")
  {
   CheckHideContextMenus();
   class="kw">return;
  }
CheckHideBackContextMenus();
class="kw">return;

class="type">void CContextMenu::CheckHideBackContextMenus(class="type">void)
  {
   class="type">int items_total=ItemsTotal();
   for(class="type">int i=class="num">0; i<items_total; i++)
     {
      if(m_items[i].TypeMenuItem()==MI_HAS_CONTEXT_MENU && m_items[i].ContextMenuState())
        {
         if(CElement::MouseFocus() && !m_items[i].MouseFocus())
           ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),class="num">0,"");
        }
     }
  }

class="type">bool CMenuItem::OnClickMenuItem(class="kw">const class="type">class="kw">string clicked_object)
  {
   else
     {
      class="type">class="kw">string message=CElement::ProgramName();
      if(m_type_menu_item==MI_CHECKBOX)
        {
         m_checkbox_state=(m_checkbox_state)? class="kw">false : true;
         m_icon.Timeframes((m_checkbox_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS);
         message+="_checkbox";
        }
      else if(m_type_menu_item==MI_RADIOBUTTON)
        {
         m_radiobutton_state=(m_radiobutton_state)? class="kw">false : true;

「右键菜单单选消息的解析与定位」

在 MT5 自定义右键菜单里,单选按钮(radiobutton)被点击后,控件会拼一条带下划线分隔的消息,并通过 EventChartCustom 推给图表。消息格式固定为「_radioitem_<id>」,其中 id 是该单选按钮的编号,发送前还顺手用 m_icon.Timeframes 按状态切换成 OBJ_NO_PERIODS 或 OBJ_ALL_PERIODS。 RadioIdFromMessage 干的事就是把这条字符串拆开。它先用 StringGetCharacter("_",0) 拿到分隔符的 ushort 码,再 StringSplit 成数组;若数组长度不是 3,直接 Print 报错并返回 WRONG_VALUE,这能拦掉绝大多数结构不对的脏消息。 真正麻烦的是 RadioIndexByItemIndex:它得先由通用 index 反查 radio_id,再遍历 ItemsTotal() 个菜单项,只筛 MI_RADIOBUTTON 类型,遇到 radio_id 匹配且 index 也匹配时才记下计数值。这套逻辑在外挂多组互斥单选时很容易写错,建议开 MT5 把下面两段直接塞进 EA 里单步跟一遍。 外汇与贵金属品种波动剧烈、滑点随机,任何界面交互逻辑都只是辅助,实盘前务必在策略测试器跑通。

MQL5 / C++
m_icon.Timeframes((m_radiobutton_state)? OBJ_NO_PERIODS : OBJ_ALL_PERIODS);
class=class="str">"cmt">//--- 把单选项信息加入消息
message+="_radioitem_"+(class="type">class="kw">string)m_radiobutton_id;
}
class=class="str">"cmt">//--- 发送相关消息
::EventChartCustom(m_chart_id,ON_CLICK_MENU_ITEM,m_index,CElement::Id(),message);
   }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 从单选项消息中取得 (class="num">1) ID 和 (class="num">2) 索引
  class="type">int              RadioIdFromMessage(class="kw">const class="type">class="kw">string message);
  class="type">int              RadioIndexByItemIndex(class="kw">const class="type">int index);
  class=class="str">"cmt">//---
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从单选项消息中展开ID                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CContextMenu::RadioIdFromMessage(class="kw">const class="type">class="kw">string message)
  {
  class="type">class="kw">ushort u_sep=class="num">0;
  class="type">class="kw">string result[];
  class="type">int    array_size=class="num">0;
class=class="str">"cmt">//--- 获取分隔符的代码
  u_sep=::StringGetCharacter("_",class="num">0);
class=class="str">"cmt">//--- 分割字符串
  ::StringSplit(message,u_sep,result);
  array_size=::ArraySize(result);
class=class="str">"cmt">//--- 如果消息的结构与期待的不同
  if(array_size!=class="num">3)
    {
    ::Print(__FUNCTION__," > 单选项消息结构错误!信息: ",message);
    class="kw">return(WRONG_VALUE);
    }
class=class="str">"cmt">//--- 防止超出数组大小
  if(array_size<class="num">3)
    {
    ::Print(PREVENTING_OUT_OF_RANGE);
    class="kw">return(WRONG_VALUE);
    }
class=class="str">"cmt">//--- 返回单选项 id
  class="kw">return((class="type">int)result[class="num">2]);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据通用索引返回单选项索引                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CContextMenu::RadioIndexByItemIndex(class="kw">const class="type">int index)
  {
  class="type">int radio_index =class="num">0;
class=class="str">"cmt">//--- 根据通用索引取得单选项id
  class="type">int radio_id =RadioItemIdByIndex(index);
class=class="str">"cmt">//--- 在所需组中的项目计数器
  class="type">int count_radio_id=class="num">0;
class=class="str">"cmt">//--- 在列表中迭代
  class="type">int items_total=ItemsTotal();
  for(class="type">int i=class="num">0; i<items_total; i++)
    {
    class=class="str">"cmt">//--- 如果不是单选项, 转到下一个
    if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
      class="kw">continue;
    class=class="str">"cmt">//--- 如果id匹配
    if(m_items[i].RadioButtonID()==radio_id)
      {
      class=class="str">"cmt">//--- 如果索引匹配 
      class=class="str">"cmt">//    保存当前计数值并结束循环
      if(m_items[i].Index()==index)

◍ 右键菜单单选项的消息回环

上下文菜单类 CContextMenu 继承自 CElement,它只关心一件事:菜单项被点中后,把携带的消息收回来并路由给内部处理函数。ReceiveMessageFromMenuItem 就是这条回环的入口,参数里同时带了 item_id、item_index 和 message_item,分别对应元件 ID、排列序号和消息字符串。 在 OnEvent 里,自定义事件 CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM 触发时,dparam 强转成 item_id,lparam 强转成 item_index,sparam 直接作为消息体传给接收函数。这三行转换若类型不匹配,MT5 编译期不报错但运行期可能拿到错乱的菜单索引。 接收函数先拿 StringFind 做两道过滤:消息里必须包含本程序名(CElement::ProgramName()),且 id_item 等于本元件 ID,否则不处理。接着若消息含 "radioitem" 子串,就调 RadioIdFromMessage 解析出单选 ID,再用 RadioIndexByItemIndex 把排列序号映射成内部索引,最后 SelectedRadioItem 完成切换。 切换完无论分支如何,都会执行 Hide() 收起菜单并解锁表单。想验证这套逻辑,直接在 MT5 里给上下文菜单挂两个单选项,点选后观察 EventChartCustom 发出的 ON_CLICK_CONTEXTMENU_ITEM 参数是否与 index_item、id_item 一致。

MQL5 / C++
  {
           radio_index=count_radio_id;
           class="kw">break;
         }
      class=class="str">"cmt">//--- 增加计数器
      count_radio_id++;
      }
   }
class=class="str">"cmt">//--- 返回索引
   class="kw">return(radio_index);
   }
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 从菜单项接收需要处理的消息
  class="type">void         ReceiveMessageFromMenuItem(class="kw">const class="type">int id_item,class="kw">const class="type">int index_item,class="kw">const class="type">class="kw">string message_item);
  class=class="str">"cmt">//---
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::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">//--- 处理 ON_CLICK_MENU_ITEM 事件
  if(id==CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM)
    {
     class="type">int    item_id      =class="type">int(dparam);
     class="type">int    item_index   =class="type">int(lparam);
     class="type">class="kw">string item_message =sparam;
     class=class="str">"cmt">//--- 从菜单项接收需要处理的消息
     ReceiveMessageFromMenuItem(item_id,item_index,item_message);
     class="kw">return;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从菜单项接收需要处理的事件                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::ReceiveMessageFromMenuItem(class="kw">const class="type">int id_item,class="kw">const class="type">int index_item,class="kw">const class="type">class="kw">string message_item)
  {
class=class="str">"cmt">//--- 如果消息是来自本程序并且元件ID匹配
  if(::StringFind(message_item,CElement::ProgramName(),class="num">0)>-class="num">1 && id_item==CElement::Id())
    {
     class=class="str">"cmt">//--- 如果点击了单选项
     if(::StringFind(message_item,"radioitem",class="num">0)>-class="num">1)
       {
        class=class="str">"cmt">//--- 从传入的消息中取得单选项的ID
        class="type">int radio_id=RadioIdFromMessage(message_item);
        class=class="str">"cmt">//--- 从通用索引中取得单选项的索引
        class="type">int radio_index=RadioIndexByItemIndex(index_item);
        class=class="str">"cmt">//--- 切换单选项
        SelectedRadioItem(radio_index,radio_id);
        }
     class=class="str">"cmt">//--- 发送相关消息
     ::EventChartCustom(m_chart_id,ON_CLICK_CONTEXTMENU_ITEM,index_item,id_item,DescriptionByIndex(index_item));
     }
class=class="str">"cmt">//--- 隐藏上下文菜单
  Hide();
class=class="str">"cmt">//--- 解锁表单

解锁窗口并广播隐藏菜单信号

在自定义面板交互里,先调用 m_wnd.IsLocked(false) 把窗口锁状态解除,用户才能继续操作控件。 紧接着用 ::EventChartCustom 向当前图表抛一个自定义事件,参数 ON_HIDE_CONTEXTMENUS 配合两个 0 和空串,本质是通知所有监听端:把上下文菜单全隐藏。 这套写法在 MT5 里可直接粘进 CAppDialog 派生类的回调末端,编译后拖一个品种到图表就能验证菜单消失逻辑是否生效。外汇与贵金属波动剧烈,面板误操作可能触发错单,调试时建议先用模拟账户。

MQL5 / C++
  m_wnd.IsLocked(class="kw">false);
class=class="str">"cmt">//--- 发送隐藏所有上下文菜单的信号
  ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,class="num">0,class="num">0,"");
  }

「在自定义类里捕获菜单点击事件」

把上下文菜单接到 EA 主逻辑里,关键是在 CProgram 的事件处理函数里拦截自定义事件。下面这段代码就是直接在 OnEvent 中判断事件 ID 是否等于 CHARTEVENT_CUSTOM 加上我们之前定义的 ON_CLICK_CONTEXTMENU_ITEM,命中后把三项参数打印出来。 lparam 传的是菜单项序号,dparam 是菜单项 ID,sparam 是描述文本。日志里能看到实测数据:2015.10.23 在 USDCAD 的 D1 图表上,点击『ContextMenu 1 Item 5』时 lparam=4、id=2;点击『ContextMenu 2 Item 1』时 lparam=0、id=3。说明多菜单多条目下参数映射是稳定的。 编译后挂上 EA,手动点几下右键菜单,专家日志就会按上面格式吐出记录。外汇和贵金属品种波动大、右键误操作概率高,这类自定义菜单交互建议先在模拟盘验证再上实盘。 CContextMenu 的主干到这里算通了,后面遇到测试坑再回头补细节,顺着用法反推结构比一口气写完更容易消化。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::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_CUSTOM+ON_CLICK_CONTEXTMENU_ITEM)
     {
      ::Print(__FUNCTION__," > index: ",lparam,"; id: ",class="type">int(dparam),"; description: ",sparam);
     }
  }
class="num">2015.10.class="num">23 class="num">20:class="num">16:class="num">27.389 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">4; id: class="num">2; description: ContextMenu class="num">1 Item class="num">5
class="num">2015.10.class="num">23 class="num">20:class="num">16:class="num">10.895 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">0; id: class="num">3; description: ContextMenu class="num">2 Item class="num">1
class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">58.520 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">5; id: class="num">3; description: ContextMenu class="num">2 Item class="num">6
class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">26.739 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">2; id: class="num">3; description: ContextMenu class="num">2 Item class="num">3
class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">23.351 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">3; id: class="num">3; description: ContextMenu class="num">2 Item class="num">4
class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">19.822 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">4; id: class="num">2; description: ContextMenu class="num">1 Item class="num">5
class="num">2015.10.class="num">23 class="num">19:class="num">27:class="num">15.550 TestLibrary(USDCAD,D1) CProgram::OnEvent > index: class="num">1; id: class="num">2; description: ContextMenu class="num">1 Item class="num">2

◍ 把这条线请下神坛

这一节把前文攒下的元件类收了口:菜单项、分隔线、上下文菜单的事件处理都就位,主菜单元件只差下一篇落地。当前阶段的库文件分两包,MT4 版 94.16 KB、MT5 版 100.99 KB,直接丢进对应终端能跑通基础交互。 有读者在评论里补过一行默认值:CContextMenu 构造里给 m_item_y_size 设 24,TestLibrary 就能干净编译;另一人指出复选框类型不会发状态消息,库仍算能打。这类边角坑别当圣经,自己复现才知深浅。 外汇和贵金属 GUI 自动化本就高风险,面板再顺手也只是降低操作摩擦,不替你兜底行情。下一篇动手前,先把这两包 zip 解了、把上下文菜单跑一遍再说。

MQL5 / C++
CContextMenu::m_item_y_size=class="num">24;
把控件状态巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到焦点跟踪与图表状态冲突的提示,你只需专注事件逻辑怎么写。

常见问题

通用数组遍历所有控件,而下拉列表和上下文菜单这类组合控件数量少却体积大,独立数组管理更高效,避免每次操作都扫全表。
在事件处理函数里给内部交互分配独立标识段,外部消息走另一区间,主类分发时按区间路由,防止自定义类误吞库自身事件。
在上下文菜单处理函数加焦点跟踪,菜单隐藏即终止后续逻辑,并在光标位于任何控件上方时统一禁用图表滚动与水平移动模式。
可以,小布的内置诊断会标记焦点与图表状态冲突、未路由事件等常见问题,省去手动逐控件排查的时间。
创建控件后须用 AddToElementsArray 把指针交还容器,并在自定义类重写消息接收方法,才能让主类把对应事件正确回调过来。