图形界面 II: 设置库的事件处理函数 (第三章)·进阶篇
🧩

图形界面 II: 设置库的事件处理函数 (第三章)·进阶篇

(2/3)· 鼠标划过隐藏菜单图表仍在滚动?这篇把库的事件 ID 分区和组合控件数组讲透

偏理论进阶 第 2/3 篇
光标移出表单再点上下文菜单项,图表滚动没禁掉——这是库里典型的焦点跟踪缺失。很多自行封装的面板只管可见态,不管鼠标落点,结果交易水平线被误拖。先把私有数组和内外事件 ID 理顺,才能根治这类串扰。

内外事件编号要分开管

做左键菜单交互时,点击一次弹上下文菜单、再点一次收起,这套逻辑不能全塞进 CMenuItem。原因很硬:ContextMenu.mqh 若被 MenuItem.mqh 包含会直接编译报错,所以显式控制放 CContextMenu,CMenuItem 只负责发自定义事件把“哪个项被点了”透传出去。 另外,点击落到上下文菜单区域外也要自动隐藏,和 MT5 终端、MetaEditor 的行为一致,这属于标准交互而不是可选项。 事件标识得拆成两拨:内部用的 ON_CLICK_MENU_ITEM(4)、ON_HIDE_CONTEXTMENUS(6)、ON_HIDE_BACK_CONTEXTMENUS(7) 只在库类之间跑;外部用的 ON_CLICK_CONTEXTMENU_ITEM(5) 专门通知你的 CProgram 层“上下文菜单里的项被点了”。编号写死在 Defines.mqh,改之前先核对别和库里其他自定义事件撞车。

MQL5 / C++
class="macro">#define ON_CLICK_MENU_ITEM(class="num">4) class=class="str">"cmt">// 点击了菜单项
class="macro">#define ON_CLICK_CONTEXTMENU_ITEM(class="num">5) class=class="str">"cmt">// 点击了上下文菜单的菜单项
class="macro">#define ON_HIDE_CONTEXTMENUS(class="num">6) class=class="str">"cmt">// 隐藏所有上下文菜单
class="macro">#define ON_HIDE_BACK_CONTEXTMENUS(class="num">7) class=class="str">"cmt">// 隐藏位于当前菜单项之下的上下文菜单

「给右键菜单类补上状态与点击路由」

在自建图形库的右键菜单模块里,CContextMenu 不能只管画菜单,还得知道自己是开是关、点的是哪一项。原类只负责呈现,缺了状态位和点击分发,实际用在 MT5 图表上会分不清用户到底点了普通对象还是菜单项。 做法是在类里加一个私有布尔 m_contextmenu_state 存菜单开关,再暴露两个同名方法 ContextMenuState() 做读和写。与此同时,私有侧要落地 OnClickMenuItem() 处理点击事件,以及 IdFromObjectName()、IndexFromObjectName() 两个解析函数——因为菜单项对象的名字里嵌了 ID 和索引,元件定位全靠这层命名约定。 事件接线只改一处:CContextMenu::OnEvent 里捕获到 CHARTEVENT_OBJECT_CLICK 时,调一下 OnClickMenuItem()。函数内部先判状态位,已开就直接返回;再用 StringFind 比对对象名是否带 '_menuitem_' 前缀,不匹配就当普通点击丢弃。 解析函数靠 '__' 双下划线切分对象名,IdFromObjectName 从分隔符后第 2 位截到末尾转成整型 ID。这套命名解析在 6 节系列的第 2 节定型,后面挂节点、绑回调都复用同一规则,改一处命名格式会连带崩掉点击路由。

MQL5 / C++
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 上下文菜单状态
   class="type">bool                m_contextmenu_state;
class="kw">public: 
   class=class="str">"cmt">//--- (class="num">1) 获取和 (class="num">2) 设置上下文菜单的状态
   class="type">bool                ContextMenuState(class="type">void)                    class="kw">const { class="kw">return(m_context_menu_state);      }
   class="type">void                ContextMenuState(class="kw">const class="type">bool flag)                { m_context_menu_state=flag;          }
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 处理附加于上下文菜单的菜单项点击
   class="type">bool                OnClickMenuItem(class="kw">const class="type">class="kw">string clicked_object);
   class=class="str">"cmt">//--- 根据菜单项名称获取 (class="num">1) ID 和 (class="num">2) 索引
   class="type">int                 IdFromObjectName(class="kw">const class="type">class="kw">string object_name);
   class="type">int                 IndexFromObjectName(class="kw">const class="type">class="kw">string object_name);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理菜单项的点击                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CContextMenu::OnClickMenuItem(class="kw">const class="type">class="kw">string clicked_object)
  {
class=class="str">"cmt">//--- 如果上下文菜单已经打开, 退出 
   if(m_contextmenu_state)
     class="kw">return(true);
class=class="str">"cmt">//--- 如果没有点击在菜单项上, 退出
   if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",class="num">0)<class="num">0)
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 从对象名称获得ID和索引
   class="type">int id    =IdFromObjectName(clicked_object);
   class="type">int index =IndexFromObjectName(clicked_object);
class=class="str">"cmt">//--- 如果没有点击在附加到上下文菜单的菜单项上
   if(id!=m_prev_node.Id() || index!=m_prev_node.Index())
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 显示上下文菜单
   Show();
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从对象名称展开ID                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CContextMenu::IdFromObjectName(class="kw">const class="type">class="kw">string object_name)
  {
class=class="str">"cmt">//--- 从对象名称展开ID
   class="type">int     length =::StringLen(object_name);
   class="type">int     pos    =::StringFind(object_name,"__",class="num">0);
   class="type">class="kw">string  id     =::StringSubstr(object_name,pos+class="num">2,length-class="num">1);
class=class="str">"cmt">//---
   class="kw">return((class="type">int)id);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从对象名称展开索引                                                 |

◍ 从对象名反查菜单序号的解析逻辑

在 MT5 自定义右键菜单里,每个图形对象通常用下划线拼接层级信息,比如 menu_3_5 这种命名。要从对象名反推出它属于第几个菜单项,核心就是按 _ 切分字符串再取倒数第二段。 下面这段 MQL5 函数演示了做法:先取 _ 的字符码,用 StringSplit 把对象名拆成数组,数组长度减 1 得到末位索引,再退两位就是菜单序号所在位置。若拆分后长度不够(array_size-2<0),直接打印越界防护并返 WRONG_VALUE,避免图表事件回调里崩脚本。 实测在对象名至少含两个下划线时(如 ctx_2_7),该函数稳定返回 2;若只传 ctx_7 则走越界分支。外汇与贵金属图表上加这类对象属高风险定制,参数命名规则乱了就容易误触。

MQL5 / C++
class="type">int CContextMenu::IndexFromObjectName(class="kw">const class="type">class="kw">string object_name)
  {
   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(object_name,u_sep,result);
   array_size=::ArraySize(result)-class="num">1;
class=class="str">"cmt">//--- 检查是否超出数组范围
   if(array_size-class="num">2<class="num">0)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      class="kw">return(WRONG_VALUE);
     }
class=class="str">"cmt">//---
   class="kw">return((class="type">int)result[array_size-class="num">2]);
   }
class=class="str">"cmt">//--- 处理在对象上点击鼠标左键的事件
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickMenuItem(sparam))
        class="kw">return;
     }

菜单项点击的分发与状态切换

上下文菜单靠背景标签截获左键点击:CContextMenu::OnClickMenuItem 收到的是方形标签图形对象名,而不是控件本体。因为 OBJ_BITMAP_LABEL 默认把标签画在较上层,若直接点标签可能触发图标双态翻转,所以统一用背景名做入口,点击优先级压过几乎所有子对象,避免误触引发非预期行为。 在 CContextMenu 里先判菜单是否已启用,已启用就直接返回;再用字符串函数从对象名拆出菜单项 ID 与索引——ID 以双横线作分隔符,索引按下划线切分,这套解析是后续定位项的基础。 CMenuItem 类的 OnClickMenuItem 不必再做名称解析,只要比对 m_area.Name() 与传入对象名。若项处于禁用态(m_item_state 为假)立即退出;若该项挂有子上下文菜单(MI_HAS_CONTEXT_MENU),则翻转 m_context_menu_state,并在关闭时发 EventChartCustom 带 ON_HIDE_BACK_CONTEXTMENUS 与自身 Id(),用来压掉下层已开的菜单,防止多菜单叠开。 下面这段是 CMenuItem::OnClickMenuItem 的声明与实现骨架,注意 ::EventChartCustom 那行高亮处就是向下层广播关闭信号的实锤。

MQL5 / C++
<span class="keyword">class</span> CMenuItem : <span class="keyword">class="kw">public</span> CElement
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 处理菜单项的点击</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OnClickMenuItem(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> clicked_object);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 处理菜单项的点击&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">bool</span> CMenuItem::OnClickMenuItem(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> clicked_object)
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//--- 根据对象名称检查</span>
&nbsp;&nbsp; <span class="keyword">if</span>(m_area.Name()!=clicked_object)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="macro">class="kw">false</span>);
<span class="comment">class=class="str">"cmt">//---如果该项目没有被激活, 就退出</span>
&nbsp;&nbsp; <span class="keyword">if</span>(!m_item_state)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="macro">class="kw">false</span>);
<span class="comment">class=class="str">"cmt">//--- 如果此项目包含一个上下文菜单</span>
&nbsp;&nbsp; <span class="keyword">if</span>(m_type_menu_item==MI_HAS_CONTEXT_MENU)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 如果此项目的下拉菜单没有被激活</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(!m_context_menu_state)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_context_menu_state=<span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_context_menu_state=<span class="macro">class="kw">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 发送一个信号用于关闭低于此项目的上下文菜单</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);">::<span class="functions">EventChartCustom</span>(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),<span class="number">class="num">0</span>,<span class="class="type">class="kw">string">""</span>);</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="macro">true</span>);
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//--- 如果此项目没有包含一个上下文菜单, 但是是一个上下文菜单本身的一部分</span>
&nbsp;&nbsp; <span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(<span class="macro">true</span>);
&nbsp;&nbsp;}

「把隐藏菜单的逻辑塞进事件主类」

CMenuItem::OnClickMenuItem() 现在还不是终版,后面会补功能,但眼下它只干一件事:向处理自定义事件的 CWndEvents 发一条隐藏上下文菜单的消息。要让这条消息真正被处理,得在 CWndEvents 里开一个对口方法,我们命名为 OnHideBackContextMenus()。 这个方法只在自定义事件分发链路里被调用,逻辑并不复杂:先核对事件 ID 是不是 CHARTEVENT_CUSTOM 加上 ON_HIDE_BACK_CONTEXTMENUS,不对就直接返回 false。核对通过后,从后往前遍历已弹出的所有上下文菜单,遇到起始菜单项(其 Id 等于 m_lparam)时,若菜单没有鼠标焦点就 Hide(),然后 break;其余更早弹出的菜单一律 Hide()。 在 ChartEventCustom() 里,OnHideBackContextMenus() 和窗口最小化、最大化处理方法并列,返回 true 即截断后续处理。这样点开深层子菜单再触发隐藏时,低于初始项的菜单会成批消失,MT5 上挂个多层级右键菜单就能验证这个收敛行为。

MQL5 / C++
class CWndEvents : class="kw">public CWndContainer
  {
class="kw">private:
   class=class="str">"cmt">//--- 隐藏低于初始菜单项的全部上下文菜单
   class="type">bool                 OnHideBackContextMenus(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ON_HIDE_BACK_CONTEXTMENUS 事件                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWndEvents::OnHideBackContextMenus(class="type">void)
  {
class=class="str">"cmt">//--- 如果信号是隐藏低于初始菜单项的上下文菜单
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_BACK_CONTEXTMENUS)
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 迭代之前调用的所有菜单
   class="type">int context_menus_total=CWndContainer::ContextMenusTotal(class="num">0);
   for(class="type">int i=context_menus_total-class="num">1; i>=class="num">0; i--)
     {
      class=class="str">"cmt">//--- 上下文菜单和它前节点的指针
      CContextMenu *cm=m_wnd[class="num">0].m_context_menus[i];
      CMenuItem    *mi=cm.PrevNodePointer();
      class=class="str">"cmt">//--- 如果是信号起始菜单项, 则...
      if(mi.Id()==m_lparam)
        {
         class=class="str">"cmt">//--- ...如果它的上下文菜单没有交点, 隐藏它
         if(!cm.MouseFocus())
            cm.Hide();
         class=class="str">"cmt">//--- 停止循环
         class="kw">break;
        }
      else
        {
         class=class="str">"cmt">//--- 隐藏上下文菜单
         cm.Hide();
        }
     }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CHARTEVENT_CUSTOM 事件                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::ChartEventCustom(class="type">void)
  {
class=class="str">"cmt">//--- 如果信号是最小化表单
   if(OnWindowRollUp())
      class="kw">return;
class=class="str">"cmt">//--- 如果信号是最大化表单
   if(OnWindowUnroll())
      class="kw">return;
class=class="str">"cmt">//--- 如果信号是隐藏低于初始菜单项的上下文菜单
   if(OnHideBackContextMenus())
      class="kw">return;
   }

◍ 给面板加第二层右键菜单并联动关闭

编译载入后先验证第一层交互:点击表单独立菜单项,上下文菜单在隐藏态会弹出、已开态会收起;菜单展开时第三项背景色锁定,鼠标移出区域也不再闪变,这个现象在 MT5 图表上可以直接复现。 为了测「点外部一起关」的行为,需要在原上下文菜单第三项上挂第二个菜单。做法是在 items_type[] 里把索引 2 的项标成 MI_HAS_CONTEXT_MENU,再在 CProgram 里加一个 CContextMenu 实例和 CreateMI1ContextMenu2() 方法。 第二层菜单共 6 项,分两组单选(各 3 项),中间插一条分隔线。关键点是用 m_mi1_contextmenu1.ItemPointerByIndex(2) 拿第一层第三项的指针当父节点;单选组标识号默认 0,第二组循环里要设成 1,否则框架认不出两组独立单选。初始高亮设为第一组索引 1、第二组索引 2。 外汇与贵金属面板定制属高风险操作环境,GUI 逻辑错误不会导致爆仓但会干扰下单效率,建议在模拟账户图表先跑通再上实盘。

MQL5 / C++
class=class="str">"cmt">//--- 项目类型数组
  ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS]=
    {
      MI_SIMPLE,
      MI_SIMPLE,
      MI_HAS_CONTEXT_MENU,
      MI_CHECKBOX,
      MI_CHECKBOX
    };
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
  class=class="str">"cmt">//--- 菜单项与上下文菜单
  CMenuItem          m_menu_item1;
  CContextMenu       m_mi1_contextmenu1;
  CContextMenu       m_mi1_contextmenu2;
  class=class="str">"cmt">//---
class="kw">private:
class="macro">#define MENU_ITEM1_GAP_X(class="num">6)
class="macro">#define MENU_ITEM1_GAP_Y(class="num">25)
  class="type">bool                CreateMenuItem1(class="kw">const class="type">class="kw">string item_text);
  class="type">bool                CreateMI1ContextMenu1(class="type">void);
  class="type">bool                CreateMI1ContextMenu2(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建上下文菜单 class="num">2                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateMI1ContextMenu2(class="type">void)
  {
class=class="str">"cmt">//--- 上下文菜单中有6项
class="macro">#define CONTEXTMENU_ITEMS2 class="num">6
class=class="str">"cmt">//--- 保存窗口指针
  m_mi1_contextmenu2.WindowPointer(m_window);
class=class="str">"cmt">//--- 保存前节点的指针
  m_mi1_contextmenu2.PrevNodePointer(m_mi1_contextmenu1.ItemPointerByIndex(class="num">2));
class=class="str">"cmt">//--- 项目名称数组
  class="type">class="kw">string items_text[CONTEXTMENU_ITEMS2]=
    {
      "ContextMenu class="num">2 Item class="num">1",
      "ContextMenu class="num">2 Item class="num">2",
      "ContextMenu class="num">2 Item class="num">3",
      "ContextMenu class="num">2 Item class="num">4",
      "ContextMenu class="num">2 Item class="num">5",
      "ContextMenu class="num">2 Item class="num">6"
    };
class=class="str">"cmt">//--- 在创建之前设置属性
  m_mi1_contextmenu2.XSize(class="num">160);
  m_mi1_contextmenu2.ItemYSize(class="num">24);
  m_mi1_contextmenu2.AreaBackColor(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;);
  m_mi1_contextmenu2.AreaBorderColor(clrSilver);
  m_mi1_contextmenu2.ItemBackColorHover(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;);
  m_mi1_contextmenu2.ItemBackColorHoverOff(clrLightGray);
  m_mi1_contextmenu2.ItemBorderColor(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;);
  m_mi1_contextmenu2.LabelColor(clrBlack);
  m_mi1_contextmenu2.LabelColorHover(clrWhite);
  m_mi1_contextmenu2.SeparateLineDarkColor(C&class="macro">#x27;class="num">160,class="num">160,class="num">160&class="macro">#x27;);
  m_mi1_contextmenu2.SeparateLineLightColor(clrWhite);
class=class="str">"cmt">//--- 在上下文菜单中加入菜单项
把控件焦点诊断交给小布
这些焦点与事件 ID 的分区逻辑,小布盯盘已做成可视化诊断,打开对应品种页即可看到哪些组合控件漏了私有数组注册,你只需专注交互改哪块。

常见问题

通用数组遍历所有控件,而下拉与上下文菜单可能数量多且超出表单,独立私有数组只管这一类,匹配和隐藏判断更快,也避免无谓循环。
应用层自定义类和库自身刷新会收到彼此的消息,可能误触发菜单关闭或图表状态切换,概率上引起界面卡死或响应错位。
可以,小布盯盘的品种页对面板类资源做了 AIGC 扫描,标出未注册到私有数组的组合控件,并提示鼠标区域外的事件屏蔽是否缺失。
菜单项状态变化要回传主类统一派发,否则新增的选中态或禁用态在外部消息里丢失,上下文菜单就不会按预期收起。
光标在表单外、菜单隐藏时点击,以及多上下文菜单共存时的图表滚动锁,这三类最容易被跳过,却最容易暴露 ID 冲突。