图形界面 II: 主菜单元件 (第四章))·进阶篇
🧩

图形界面 II: 主菜单元件 (第四章))·进阶篇

(2/3)· 主菜单类封装、非活动控件拦截与事件通信,MQL5 界面库实战的硬骨头

实战向进阶 第 2/3 篇
不少交易者在自建 MQL5 面板时,把菜单项直接堆在图表上却没做活动状态隔离,结果点空白处也触发旧回调。主菜单不封非活动控件,多窗体下事件会串号,回测脚本里这坑更隐蔽。

◍ 菜单指针越界与窗口容器结构

在 MT5 自定义界面库里,按索引取主菜单或上下文菜单指针时,先拿 ArraySize 测数组长度。若 array_size<1 说明主菜单一个项都没挂,直接在终端日志打印报错提示,但函数不会中断,仍会继续跑后面的越界修正逻辑。 索引修正用了一行三元表达式:i=(index>=array_size)? array_size-1 : (index<0)? 0 : index。也就是索引超上限就钳到末位,低于 0 就钳到 0,正常区间才原样返回,这能避免 GetPointer(m_items[i]) 触发越界崩溃。 WndContainer.mqh 里 CWndContainer 类用 WindowElements 结构存 CMenuBar* 数组,并暴露 MenuBarsTotal(const int window_index) 接口统计指定窗口的主菜单数量。其内部先判 window_index>=ArraySize(m_wnd),越界就 Print(PREVENTING_OUT_OF_RANGE),这对多图表 EA 面板排查对象丢失有帮助。 外汇与贵金属 MT5 界面开发属高风险定制,GUI 逻辑错误可能让 EA 面板失效但不影响下单,建议开 MT5 把这段越界钳制抄进自己的 CMenuBar 派生类验证边界行为。

MQL5 / C++
class="type">int array_size=::ArraySize(m_items);
class=class="str">"cmt">//--- 如果主菜单不包含任何菜单项,就报错
if(array_size<class="num">1)
  {
  ::Print(__FUNCTION__," > 将调用此方法, "
  "主菜单至少要包含一个菜单项!");
  }
class=class="str">"cmt">//---如果超出范围就做调整
class="type">int i=(index>=array_size)? array_size-class="num">1 : (index<class="num">0)? class="num">0 : index;
class=class="str">"cmt">//--- 返回指针
class="kw">return(::GetPointer(m_items[i]));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据索引返回上下文菜单的指针                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CContextMenu *CMenuBar::ContextMenuPointerByIndex(class="kw">const class="type">int index)
  {
  class="type">int array_size=::ArraySize(m_contextmenus);
class=class="str">"cmt">//--- 如果主菜单不包含任何菜单项,就报错
  if(array_size<class="num">1)
    {
    ::Print(__FUNCTION__," > 将调用此方法, "
    "主菜单至少要包含一个菜单项!");
    }
class=class="str">"cmt">//---如果超出范围就做调整
  class="type">int i=(index>=array_size)? array_size-class="num">1 : (index<class="num">0)? class="num">0 : index;
class=class="str">"cmt">//--- 返回指针
  class="kw">return(::GetPointer(m_contextmenus[i]));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                       WndContainer.mqh                          |
class=class="str">"cmt">//|          Copyright class="num">2015, MetaQuotes Software Corp.              |
class=class="str">"cmt">//|                     [MQL5官方文档]                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "MenuBar.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于保存所有界面对象的类                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWndContainer
  {
class="kw">protected:
  class=class="str">"cmt">//--- 元件数组结构
  class="kw">struct WindowElements
    {
    class=class="str">"cmt">//--- 主菜单数组
    CMenuBar        *m_menu_bars[];
    };
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 主菜单数量
  class="type">int               MenuBarsTotal(class="kw">const class="type">int window_index);
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 把主菜单元件指针保存到库中
  class="type">bool              AddMenuBarElements(class="kw">const class="type">int window_index,CElement &object);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据指定的窗口索引返回主菜单的数量                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CWndContainer::MenuBarsTotal(class="kw">const class="type">int window_index)
  {
  if(window_index>=::ArraySize(m_wnd))
    {
    ::Print(PREVENTING_OUT_OF_RANGE);

菜单条指针落库时的数组扩容细节

在自定义 GUI 容器里,主菜单(CMenuBar)不是直接塞进通用元件数组,而是拆成菜单项逐个挂接。AddMenuBarElements 先拿 ClassName 判型,非 CMenuBar 直接返回 false,避免误把普通控件当菜单处理。 判型通过后,用 ItemsTotal() 取菜单项总数,再走 for 循环。每轮先读 m_elements 当前长度,ArrayResize 加 1,然后把 ItemPointerByIndex(i) 得到的 CMenuItem 指针写进末尾,并顺手调 AddToObjectsArray 把对象指针也进通用库。 循环结束才把整个 CMenuBar 指针通过 AddToRefArray 存进 m_menu_bars 私有数组,函数返回 true。上层 AddToElementsArray 收到 true 就直接 return,不再走通用添加分支。 验证时可开 MT5 新建个带 CMenuBar 的面板,在 AddMenuBarElements 里 Print(::ArraySize(m_wnd[window_index].m_elements)),能看到每加一个菜单项数组长度精确 +1,外汇与贵金属 GUI 开发属高风险定制,参数误用可能导致面板不渲染。

MQL5 / C++
  class="kw">return(WRONG_VALUE);
   }
class=class="str">"cmt">//---
   class="kw">return(::ArraySize(m_wnd[window_index].m_menu_bars));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把指针加到元件数组                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndContainer::AddToElementsArray(class="kw">const class="type">int window_index,CElement &object)
  {
class=class="str">"cmt">//--- 如果库中没有包含控件表单
class=class="str">"cmt">//--- 如果需求是为了不存在的表单
class=class="str">"cmt">//--- 添加到通用元件数组中
class=class="str">"cmt">//--- 把元件对象加到通用对象数组中
class=class="str">"cmt">//--- 保存所有表单中最后一个元件的id
class=class="str">"cmt">//--- 元件id计数器加1
class=class="str">"cmt">//--- 在库中保存上下文菜单对象的指针
class=class="str">"cmt">//--- 把主菜单对象的指针存储到库中
   if(AddMenuBarElements(window_index,object))
      class="kw">return;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把主菜单对象的指针保存到库中                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWndContainer::AddMenuBarElements(class="kw">const class="type">int window_index,CElement &object)
  {
class=class="str">"cmt">//--- 如果不是主菜单就退出
   if(object.ClassName()!="CMenuBar")
      class="kw">return(false);
class=class="str">"cmt">//--- 取得主菜单的指针
   CMenuBar *mb=::GetPointer(object);
class=class="str">"cmt">//--- 在库中保存它的对象指针
   class="type">int items_total=mb.ItemsTotal();
   for(class="type">int i=class="num">0; i<items_total; i++)
     {
     class=class="str">"cmt">//--- 加大元件数组
      class="type">int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+class="num">1);
     class=class="str">"cmt">//--- 取得菜单项的指针
      CMenuItem *mi=mb.ItemPointerByIndex(i);
     class=class="str">"cmt">//--- 在数组中保存指针
      m_wnd[window_index].m_elements[size]=mi;
     class=class="str">"cmt">//--- 把菜单项中的对象指针加到通用数组中
      AddToObjectsArray(window_index,mi);
     }
class=class="str">"cmt">//--- 把指针加到私有数组中
   AddToRefArray(mb,m_wnd[window_index].m_menu_bars);
   class="kw">return(true);
  }

「把三项的菜单条挂进面板」

这一步只验证主菜单元件本身:在 CProgram 里塞一个 CMenuBar,写死三个菜单项,宽度分别取 50、55、53 像素,文字是 File / View / Help,其余属性全走默认。 动手前先挪一遍表单上其他控件的坐标,给菜单条腾出 Y 方向 20 像素的偏移(MENUBAR_GAP_Y),否则会叠在标题栏上。然后把 CreateMenuBar() 接进创建图形界面的主方法,EA 编译拖进图表就能看到效果。 实测现象:菜单条跟着表单一起移动,鼠标掠过菜单项时背景色会变。下面这段是类内声明加两个方法的骨架,注意 AddItem 的循环只传了宽和文字,没碰字体和点击事件。 别把默认坐标当万能:MENUBAR_GAP_Y 写 20、菜单项 Y 偏移 45 和 75 是这套布局的硬约束,你改了表单高度不跟着调,菜单就会漂到控件底下。

MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
  class=class="str">"cmt">//--- 主菜单
  CMenuBar            m_menubar;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//---
class="macro">#define MENUBAR_GAP_X(class="num">1)
class="macro">#define MENUBAR_GAP_Y(class="num">20)
  class="type">bool                CreateMenuBar(class="type">void);
  class=class="str">"cmt">//---
class="macro">#define MENU_ITEM1_GAP_X(class="num">6)
class="macro">#define MENU_ITEM1_GAP_Y(class="num">45)
  class=class="str">"cmt">//---
class="macro">#define SEP_LINE_GAP_X(class="num">6)
class="macro">#define SEP_LINE_GAP_Y(class="num">75)
  class=class="str">"cmt">//---
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建交易面板                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateTradePanel(class="type">void)
  {
class=class="str">"cmt">//--- 创建控件表单
class=class="str">"cmt">//--- 创建控件:
class=class="str">"cmt">//    主菜单
  if(!CreateMenuBar())
      class="kw">return(false);
class=class="str">"cmt">//--- 菜单项
class=class="str">"cmt">//--- 分隔线
class=class="str">"cmt">//--- 重绘图表
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建主菜单                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateMenuBar(class="type">void)
  {
class=class="str">"cmt">//--- 主菜单中有三个菜单项
class="macro">#define MENUBAR_TOTAL class="num">3
class=class="str">"cmt">//--- 保存窗口指针
  m_menubar.WindowPointer(m_window);
class=class="str">"cmt">//--- 坐标
  class="type">int x=m_window.X()+MENUBAR_GAP_X;
  class="type">int y=m_window.Y()+MENUBAR_GAP_Y;
class=class="str">"cmt">//--- 含有每个菜单项独特属性的数组
  class="type">int     width[MENUBAR_TOTAL] ={class="num">50,class="num">55,class="num">53};
  class="type">class="kw">string  text[MENUBAR_TOTAL]  ={"File","View","Help"};
class=class="str">"cmt">//--- 把菜单项加到主菜单
  for(class="type">int i=class="num">0; i<MENUBAR_TOTAL; i++)
      m_menubar.AddItem(width[i],text[i]);
class=class="str">"cmt">//--- 创建控件
  if(!m_menubar.CreateMenuBar(m_chart_id,m_subwin,x,y))
      class="kw">return(false);
class=class="str">"cmt">//--- 把对象添加到对象组的通用数组中
  CWndContainer::AddToElementsArray(class="num">0,m_menubar);
  class="kw">return(true);
  }

◍ 激活控件时锁住整块表单

做一个像 MT5 原生终端那样的下拉或右键菜单,核心不是画出来,而是「画出来以后别让底下东西乱动」。你开任何一个上下文菜单或下拉列表,会注意到鼠标划过主图表按钮、输入框时它们颜色都不跳了——这就是终端把所属表单临时屏蔽了,避免被浮层挡住的元件还对光标响应。 我们在 CWindow 里加一个 m_is_locked 布尔栏位,构造时置 false,表示默认不锁。再配一对 IsLocked() 读写方法,定时器里只要 m_is_locked 为 true 就跳过 ChangeObjectsColor(),表单和子元件颜色全冻结。 但光锁表单会出 bug:上下文菜单自己也是表单子元件,锁了之后菜单项 hover 也不变色。所以在 CContextMenu 里单独写一套改色方法,并在计时器调用;同时 CMenuItem::OnEventTimer 要先过 !m_wnd.IsLocked() 才改色。Show() 里置锁,Hide() 不能直接解锁——多个菜单同开时只关了一个不该全解,得在 CheckHideContextMenus 和 ReceiveMessageFromMenuItem 里统一发关闭信号后再解。 编译后挂 EA 到图表验证:右键开菜单,背后按钮颜色静止;菜单内项 hover 仍高亮,即行为正确。外汇与贵金属 GUI 脚本测试请先在模拟盘,浮层锁死可能导致实盘点击失效,属高风险操作环节。

MQL5 / C++
class CWindow : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 屏蔽窗口的状态
   class="type">bool              m_is_locked;
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 屏蔽窗口的状态
   class="type">bool              IsLocked(class="type">void)                                                class="kw">const { class="kw">return(m_is_locked);               }
   class="type">void              IsLocked(class="kw">const class="type">bool flag)                                      { m_is_locked=flag;                    }
   class=class="str">"cmt">//---
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWindow::CWindow(class="type">void) : m_is_locked(false)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器                                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::OnEventTimer(class="type">void)
  {
class=class="str">"cmt">//--- 如果窗口
   if(!m_is_locked)
     {
      class=class="str">"cmt">//--- 修改表单对象的颜色
      ChangeObjectsColor();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器                                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::OnEventTimer(class="type">void)
  {
class=class="str">"cmt">//--- 如果窗口是可用的
   if(!m_wnd.IsLocked())
     {
      class=class="str">"cmt">//--- 如果是禁用的上下文菜单
      if(!m_context_menu_state)
        class=class="str">"cmt">//--- 修改表单对象的颜色
        ChangeObjectsColor();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

右键菜单的锁表单与悬停染色逻辑

在 MT5 自定义面板里做上下文菜单,最容易被忽略的是表单锁定。CContextMenu::Show() 里调用 m_wnd.IsLocked(true) 会把底层窗口锁住,防止菜单弹出时用户误触其他控件;对应地,CheckHideContextMenus() 和 ReceiveMessageFromMenuItem() 在隐藏菜单时都必须补一句 m_wnd.IsLocked(false),否则界面会卡在不可交互状态。 菜单自动收起的判断放在 CheckHideContextMenus():当光标同时脱离菜单矩形和前驱节点范围,就解锁表单并广播隐藏信号。这里隐含一个细节——若某个菜单项自身还挂了子菜单,必须循环遍历 m_items 确认没有下级被激活,才能真关掉。 悬停高亮不靠事件驱动,而是用定时器轮询。OnEventTimer() 每跳一次就调 ChangeObjectsColor(),后者先判 m_context_menu_state,再对 ItemsTotal() 个项逐个跑 m_items[i].ChangeObjectsColor()。在 30 fps 的图表环境下,这种轮询延迟通常低于 33 ms,肉眼基本无感。 外汇与贵金属图表挂这类面板时,注意锁表单期间 EA 仍在跑,价格跳动不会停,高频刷新叠加菜单轮询可能抬升 CPU 占用,建议在低价量时段先开 MT5 试一轮。

MQL5 / C++
class=class="str">"cmt">//| 显示一个上下文菜单                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::Show(class="type">void)
  {
class=class="str">"cmt">//--- 如果元件已经可见, 退出
class=class="str">"cmt">//--- 显示上下文菜单的对象
class=class="str">"cmt">//--- 显示菜单项
class=class="str">"cmt">//--- 给可见元件设置状态
class=class="str">"cmt">//--- 上下文菜单的状态
class=class="str">"cmt">//--- 在前一节点注册状态
class=class="str">"cmt">//--- 屏蔽表单
   m_wnd.IsLocked(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查关闭所有上下文菜单的条件                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::CheckHideContextMenus(class="type">void)
  {
class=class="str">"cmt">//--- 如果光标在上下文菜单范围之外或者前一节点范围之外
class=class="str">"cmt">//--- 如果光标在这些元件范围之外, 则 ...
class=class="str">"cmt">//    ... 需要检查是否之后有上下文菜单被激活
class=class="str">"cmt">//--- 为此在这个上下文菜单项中作循环 ...
class=class="str">"cmt">//    ... 以检查是否有菜单项包含上下文菜单
class=class="str">"cmt">//--- 解锁表单
   m_wnd.IsLocked(false);
class=class="str">"cmt">//--- 发送隐藏所有上下文菜单的信号
  }
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匹配
class=class="str">"cmt">//--- 隐藏上下文菜单
class=class="str">"cmt">//--- 解锁表单
   m_wnd.IsLocked(false);
  }
class CContextMenu : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 当鼠标光标掠过时改变菜单项的颜色
   class="type">void              ChangeObjectsColor(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::OnEventTimer(class="type">void)
  {
class=class="str">"cmt">//--- 当鼠标光标掠过时改变菜单项的颜色
   ChangeObjectsColor();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 当鼠标光标掠过时改变对象颜色                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::ChangeObjectsColor(class="type">void)
  {
class=class="str">"cmt">//--- 如果上下文菜单被禁用就退出
   if(!m_context_menu_state)
      class="kw">return;
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">//--- 修改菜单项的颜色
      m_items[i].ChangeObjectsColor();
     }
  }

「主菜单与上下文菜单的联动机制」

主菜单初始处于未激活态,鼠标掠过仅高亮,点击某项后才弹出对应上下文菜单并进入激活态;激活后光标掠过不同菜单项会自动切换上下文菜单,这与 Windows 程序行为一致。 挂接上下文菜单时,需把前节点指针传给 CContextMenu,并在 CMenuBar 中保存其指针。设置主菜单属性时,上下文菜单坐标按菜单项底部计算(FIX_BOTTOM),其余创建逻辑与此前相同。 点击处理在 CMenuBar::OnEvent 中拦截 CHARTEVENT_OBJECT_CLICK,转交 OnClickMenuItem。该方法先从对象名解析索引与 ID,若无上下文菜单指针则广播关闭信号,有指针且再次点击同一项时才关闭当前菜单。关闭动作最终由 CWndEvents::OnHideContextMenus 强制清场。 激活态下的悬停切换由 SwitchContextMenuByFocus(index) 完成:传入焦点项索引显示对应菜单、隐藏其余,若存在其他打开的菜单则发 ON_HIDE_BACK_CONTEXTMENUS,隐藏后必须重置菜单项颜色防止双高亮。编译载入 EA 后,首次点击出菜单、二次点击收起,验证通过即可。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建上下文菜单                                                          | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateMBContextMenu1(class="type">void)
  {
class=class="str">"cmt">//--- 上下文菜单中有三项
class=class="str">"cmt">//--- 保存窗口指针
   m_mb_contextmenu1.WindowPointer(m_window);
class=class="str">"cmt">//--- 保存前节点的指针
   m_mb_contextmenu1.PrevNodePointer(m_menubar.ItemPointerByIndex(class="num">0));
class=class="str">"cmt">//--- 把上下文菜单附加到指定的菜单项
   m_menubar.AddContextMenuPointer(class="num">0,m_mb_contextmenu1);
class=class="str">"cmt">//--- 项目名称数组
class=class="str">"cmt">//--- 可用模式的标签数组
class=class="str">"cmt">//--- 禁用模式的标签数组
class=class="str">"cmt">//--- 项目类型数组
class=class="str">"cmt">//--- 在创建之前设置属性
   m_mb_contextmenu1.FixSide(FIX_BOTTOM);
class=class="str">"cmt">//--- 在上下文菜单中加入菜单项
class=class="str">"cmt">//--- 第二个项目后的分隔线
class=class="str">"cmt">//--- 去除第二项的激活状态
class=class="str">"cmt">//--- 创建上下文菜单
   if(!m_mb_contextmenu1.CreateContextMenu(m_chart_id,m_subwin))
      class="kw">return(false);
class=class="str">"cmt">//--- 把对象添加到对象组的通用数组中
   CWndContainer::AddToElementsArray(class="num">0,m_mb_contextmenu1);
   class="kw">return(true);
   }
class CMenuBar : class="kw">public CElement
  {
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">//| 事件处理函数                                                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 
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)
  {
class=class="str">"cmt">//--- 处理鼠标左键点击主菜单项的事件
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickMenuItem(sparam))
         class="kw">return;
     }
   }
把界面状态巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件激活态与事件流,你专注决策逻辑而非反复手查源码。

常见问题

默认背景高 22 像素,菜单项在构造时按背景高自动算;挂表前也可用 CElement::YSize() 改,但背景宽随表单宽自动取,不必手填。
目前小布内置的是行情与控件状态诊断,自写库需先跑在终端,再把品种页状态对照看,暂不支持外部 mqh 直接注入渲染。
主菜单项索引配上下文菜单对象指针存 m_contextmenus[],解耦组合元件,避免每个项写死弹出逻辑,后续通信方法才好统一派发。
拦的是焦点不在该元件时的鼠标与重绘事件,防止隐藏窗体误收点击;库里靠状态位判断,多菜单共存时必开。
除正常点击回传,还要测附加上下文后索引错位、阻断态下事件零触发,完整跨篇讨论见《图形界面 II 主菜单元件·基础篇》。