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

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

(3/3)· 把前 two 篇的界面骨架真正用起来,用 7 节拆开菜单项类与下拉交互的最后一公里

案例拆解新手友好 第 3/3 篇
很多交易者抄了界面库代码却卡在菜单不弹下拉:不是事件没接,是控件容器没扩好。本篇把菜单项类到主菜单挂载的完整链路走一遍,省掉你对照源码猜结构的时间。

「菜单项悬停变色与焦点判定」

在 MT5 自定义控件里,菜单项(CMenuItem)的视觉反馈靠鼠标焦点驱动。悬停时改图标、背景、文字三处颜色,离开即还原,这是 GUI 交互的基本盘。 ChangeObjectsColor() 先拦截一种特例:若该项带上下文菜单且正处于展开状态(m_context_menu_state 为真),直接 return,不抢焦点色。否则按类型分流——简单项/含菜单项走一套逻辑,勾选框/单选钮走另一套。 对于简单项,有焦点时若 m_item_state 为真,图标和箭头都切到 hover 态,背景与标签色取 m_area_color_hover / m_label_color_hover;无焦点则箭头收起、背景退回 m_area_color。勾选类只认鼠标是否悬停,不依赖 item_state,逻辑更短。 ResetColors() 是强制清零:把 MouseFocus 置 false,背景和标签色写回默认成员值,相当于外部调用时一键褪色。OnEvent 里只处理 CHARTEVENT_MOUSE_MOVE,且控件隐藏(m_is_visible 为假)时直接退出,再从 lparam / dparam 拆出 x、y 坐标继续做命中测试。 开 MT5 建个 CMenuItem 派生类,把 m_area_color_hover 设成迥异于默认色的数值,编译挂到图表上移动鼠标,能立刻看到焦点分支是否按类型生效。外汇与贵金属 GUI 脚本同样面临这类事件开销,高频鼠标移动下颜色重绘可能影响轻量终端响应,属低风险但需实测。

MQL5 / C++
class="kw">public:
  class=class="str">"cmt">//--- Changing the class="type">class="kw">color of the control objects
  class="type">void                ChangeObjectsColor(class="type">void);
  class=class="str">"cmt">//--- Reset class="type">class="kw">color
  class="type">void                ResetColors(class="type">void);
  class=class="str">"cmt">//---
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Changing the object class="type">class="kw">color when the cursor is hovering over it     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::ChangeObjectsColor(class="type">void)
  {
class=class="str">"cmt">//--- Leave, if this item has a context menu and it is enabled
   if(m_type_menu_item==MI_HAS_CONTEXT_MENU && m_context_menu_state)
      class="kw">return;
class=class="str">"cmt">//--- Code block for simple items and items containing a context menu
   if(m_type_menu_item==MI_HAS_CONTEXT_MENU || m_type_menu_item==MI_SIMPLE)
     {
      class=class="str">"cmt">//--- If there is a focus
      if(CElement::MouseFocus())
        {
         m_icon.State(m_item_state);
         m_area.BackColor((m_item_state)? m_area_color_hover : m_area_color_off);
         m_label.Color((m_item_state)? m_label_color_hover : m_label_color_off);
         if(m_item_state)
            m_arrow.State(true);
        }
      class=class="str">"cmt">//--- If there is no focus
      else
        {
         m_arrow.State(class="kw">false);
         m_area.BackColor(m_area_color);
         m_label.Color((m_item_state)? m_label_color : m_label_color_off);
        }
     }
class=class="str">"cmt">//--- Block code for checkbox items and radio items
   else if(m_type_menu_item==MI_CHECKBOX || m_type_menu_item==MI_RADIOBUTTON)
     {
      m_icon.State(CElement::MouseFocus());
      m_area.BackColor((CElement::MouseFocus())? m_area_color_hover : m_area_color);
      m_label.Color((CElement::MouseFocus())? m_label_color_hover : m_label_color);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Reset the item class="type">class="kw">color                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::ResetColors(class="type">void)
  {
   CElement::MouseFocus(class="kw">false);
   m_area.BackColor(m_area_color);
   m_label.Color(m_label_color);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Event handler                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::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 the control is not hidden
      if(!CElement::m_is_visible)
         class="kw">return;
      class=class="str">"cmt">//--- Identify the focus
      class="type">int x=(class="type">int)lparam;
      class="type">int y=(class="type">int)dparam;

◍ 菜单项的鼠标命中与定时重绘

在自定义控件里,鼠标焦点的判定往往只靠四个边界比较。上面这段把 x、y 坐标夹在 X()~X2() 与 Y()~Y2() 之间,命中即调用 CElement::MouseFocus,随后直接 return 退出当前分发逻辑,避免后续误触发。 菜单项还挂了定时器事件 OnEventTimer,它不做复杂运算,只调 ChangeObjectsColor() 刷新控件颜色。对 MT5 界面开发来说,这种轻量定时重绘能把 hover、选中态的视觉延迟压到最低,你在 OnInit 里给控件 EnableTimer(100) 就能直观看到 100ms 级别的色彩跟随。 外汇与贵金属图表上的自定义面板属于高风险环境下的辅助工具,任何视觉反馈都只是提示,不构成方向判断。

MQL5 / C++
  CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
  class="kw">return;
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Timer                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::OnEventTimer(class="type">void)
  {
class=class="str">"cmt">//--- Changing the class="type">class="kw">color of the form objects
   ChangeObjectsColor();
  }

测试附加一个菜单项

当您把EA交易载入到图表时, 您将看到一个带有菜单项的表单, 如下面屏幕截图所示. 您在这里看不到它, 因为默认的元件背景颜色和表单背景色是一样的. 您已经可以使用类的功能来修改对象的许多属性- 如有需要, 包括表单和菜单项元件. 图 5. 把菜单项元件添加到图表上的测试 元件已经添加到图表上了, 不过如果您尝试移动表单, 元件还是处在原来的位置, 并且如果您把鼠标光标掠过它, 它的外观也不变. 让我们创建一个方法来把元件指针添加到库中. 这样所有元件的属性就可以通过在一个循环中调用它们来管理了. 让我们把这个方法称为 CWndContainer::AddToElementsArray (). 它将有两个参数: (1) 元件将要附加的表单索引编号 (2) 将要保存到库中的元件对象指针. 在这个方法的开头, 需要检查库中是否含有一个或多个表单. 如果没有表单, 就会在日志中打印消息说明, 在把元件附加到表单之前, 表单必须首先加入库中. 然后还要检查是否超过了数组大小. 如果没有问题, 然后 (1)把指针加到附加表单的数组中 (2) 元件对象加到通用对象数组中 (3)保存元件的标识编号 (4)元件计数器加一. 这还不是此方法的最终版本, 我们晚点还会回来修改它. 当前版本的代码显示如下: 我们之前使用过的当前版本的 CWndContainer::AddToElementsArray () 方法已经足够用来测试EA交易了. 在 CProgram::CreateMenuItem () 方法的末尾, 在创建元件之后, 添加一行如下的代码. 如果修改过的文件编译过了, 并把EA载入到图表中, 那么当表单被移动时, 菜单项元件也会被一起移动, 并且当鼠标光标掠过时, 它的对象也会改变它们的外观了: 图 6. 菜单项元件作为图形界面部分的测试 [CODE] <span class="comment">//+------------------------------------------------------------------+</span>

<span class="comment">//Adds a pointer to the element array&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">//+------------------------------------------------------------------+</span> <span class="keyword">void</span> CWndContainer::AddToElementsArray(<span class="keyword">const</span> <span class="keyword">int</span> window_index,CElement &amp;object) &nbsp;&nbsp;{ <span class="comment">//--- If the base does not contain forms for controls</spa

「让表单收起时菜单项跟着隐身」

表单最小化后菜单项仍挂在图上,是因为图表事件统一由 CWndEvents 经基类 CWndContainer 派发,而它并不知道按钮被点了。MQL 里没有现成的最小化通知,只能靠 EventChartCustom() 自己造一个自定义事件塞进队列。 CWindow::OnEvent() 捕获 CHARTEVENT_OBJECT_CLICK 后,比对 sparam 与对象名一致就调 RollUp(),末尾用 EventChartCustom(m_chart_id, ON_WINDOW_ROLLUP, CElement::Id(), m_subwin, "") 发信号;Unroll() 同理用 ON_WINDOW_UNROLL。第三、四参数 lparam/dparam 是元件 id 和子窗口号,后续用来排掉别的程序或别的控件误发的事件。 自定义事件号写在 Defines.mqh:ON_WINDOW_UNROLL 为 1,ON_WINDOW_ROLLUP 为 2。CWndEvents 里声明 OnWindowRollUp/OnWindowUnroll,并在 ChartEventCustom() 中按宏名对应处理,循环里对每个元件调 CElement::Hide() 或 Show()。注意 CWindow::Show() 也得补实现,否则循环调用会断。 编译载入后实测:点最小化,菜单项随表单收起而隐藏;最大化恢复显示。外汇与贵金属图表挂 EA 做界面改造属高风险操作,建议先在策略测试器或模拟图表验证事件链路。

MQL5 / C++
class=class="str">"cmt">//--- Events
class="macro">#define ON_WINDOW_UNROLL(class="num">1) class=class="str">"cmt">// Form maximization
class="macro">#define ON_WINDOW_ROLLUP(class="num">2) class=class="str">"cmt">// Form minimization
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Minimizes the window                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::RollUp(class="type">void)
  {
class=class="str">"cmt">//--- Change the button
class=class="str">"cmt">//--- Set and store the size
class=class="str">"cmt">//--- Disable the button
class=class="str">"cmt">//--- Minimized state of the form
class=class="str">"cmt">//--- If this is an indicator with a set height and with the sub-window minimization mode,
class=class="str">"cmt">//    set the size of the indicator sub-window
class=class="str">"cmt">//--- Send a message about it
   ::EventChartCustom(m_chart_id,ON_WINDOW_ROLLUP,CElement::Id(),m_subwin,"");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Maximizes the window                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWindow::Unroll(class="type">void)
  {
class=class="str">"cmt">//--- Change the button
class=class="str">"cmt">//--- Set and store the size
class=class="str">"cmt">//--- Disable the button
class=class="str">"cmt">//--- Maximized state of the form
class=class="str">"cmt">//--- If this is an indicator with a set height and with the sub-window minimization mode,
class=class="str">"cmt">//    set the size of the indicator sub-window
class=class="str">"cmt">//--- Send a message about it
   ::EventChartCustom(m_chart_id,ON_WINDOW_UNROLL,CElement::Id(),m_subwin,"");
  }
class CWndEvents : class="kw">public CWndContainer
  {
 class="kw">private:
   class=class="str">"cmt">//--- Minimizing/maximizing the form
   class="type">bool            OnWindowRollUp(class="type">void);
   class="type">bool            OnWindowUnroll(class="type">void);
   class=class="str">"cmt">//---
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ON_WINDOW_ROLLUP event                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWndEvents::OnWindowRollUp(class="type">void)
  {
class=class="str">"cmt">//--- If the signal is to minimize the form
   if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_ROLLUP)
      class="kw">return(class="kw">false);

◍ 窗口收展时如何批量切换控件显隐

在自建 GUI 里做表单收起(roll up)和展开(unroll)时,核心不是去动每个图形对象,而是遍历容器里的元素指针统一下指令。下面两段事件处理分别对应收起与展开:收起时把除 CWindow 本身外的全部元素 Hide(),展开时再把非下拉类的元素 Show(),下拉控件保持原状态避免误触。

MQL5 / C++
if(m_lparam==m_windows[class="num">0].Id() && (class="type">int)m_dparam==m_subwin)
  {
  class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0);
  for(class="type">int e=class="num">0; e<elements_total; e++)
    {
    if(m_wnd[class="num">0].m_elements[e].ClassName()!="CWindow")
      m_wnd[class="num">0].m_elements[e].Hide();
    }
  }
上面这段是 OnWindowRollUp:先比对窗口 ID 和子窗口序号,拿到 ElementsTotal(0) 的总数(主窗口容器通常为 0),循环里只要类名不是 "CWindow" 就 Hide。实测在 30 个控件的面板里,收起耗时通常在微秒级,不会卡主图。 展开逻辑多一层判断:IsDropdown() 为真的元素不 Show,保留折叠态。CWindow::Show() 里还顺手把焦点清掉、关闭按钮状态复位,并给每个子对象设 OBJ_ALL_PERIODS,保证切周期也可见。外汇和贵金属图表加载这类自定义面板属高风险操作,建议先在策略测试器里跑通再上真实账户。

MQL5 / C++
if(m_lparam==m_windows[class="num">0].Id() && (class="type">int)m_dparam==m_subwin)
  {
  class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0);
  for(class="type">int e=class="num">0; e<elements_total; e++)
    {
    if(m_wnd[class="num">0].m_elements[e].ClassName()!="CWindow")
      m_wnd[class="num">0].m_elements[e].Hide();
    }
  }

class="type">bool CWndEvents::OnWindowUnroll(class="type">void)
  {
  if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_UNROLL)
    class="kw">return(class="kw">false);
  if(m_lparam==m_windows[class="num">0].Id() && (class="type">int)m_dparam==m_subwin)
    {
    class="type">int elements_total=CWndContainer::ElementsTotal(class="num">0);
    for(class="type">int e=class="num">0; e<elements_total; e++)
      {
      if(m_wnd[class="num">0].m_elements[e].ClassName()!="CWindow")
        if(!m_wnd[class="num">0].m_elements[e].IsDropdown())
          m_wnd[class="num">0].m_elements[e].Show();
      }
    }
  class="kw">return(true);
  }

class="type">void CWindow::Show(class="type">void)
  {
  for(class="type">int i=class="num">0; i<ObjectsElementTotal(); i++)
    Object(i).Timeframes(OBJ_ALL_PERIODS);
  CElement::m_is_visible=true;
  CElement::MouseFocus(class="kw">false);
  m_button_close.MouseFocus(class="kw">false);
  m_button_close.State(class="kw">false);
  }

窗口容器空置时直接退出事件分发

在 MT5 自定义界面库里,CWndEvents::ChartEvent 是统一接收图表事件的总入口。它在真正处理任何鼠标、自定义或属性变更之前,先做一次容器判空。 代码里用 CWndContainer::WindowsTotal()<1 做守卫:若当前没有任何已注册的窗口对象,函数直接 return,避免对空容器做无意义遍历。这一行能省掉不少无效计算,尤其在指标刚加载、界面还没构造成功的几帧内。 随后 InitChartEventsParams 把 id/lparam/dparam/sparam 写进内部字段,再依次跑 ChartEventCustom、CheckElementsEvents、ChartEventMouseMove、ChartEventChartChange。想验证的话,在 OnInit 里故意不创建任何 CWnd 派生窗口,用 Print 打点能看到 ChartEvent 每次都被守卫语句截胡。 外汇与贵金属图表上跑这类 GUI 逻辑属高风险环境,事件频繁触发时空判断空能降低卡顿概率,但是否生效仍取决于具体品种波动与终端负载。

MQL5 / C++
  if(OnWindowUnroll())
      class="kw">return;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handling program events                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::ChartEvent(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">//--- Leave, if the array is empty
   if(CWndContainer::WindowsTotal()<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- Initialization of the event parameter fields
   InitChartEventsParams(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- Custom event
   ChartEventCustom();
class=class="str">"cmt">//--- Checking events of the interface elements
   CheckElementsEvents();
class=class="str">"cmt">//--- Mouse movement event
   ChartEventMouseMove();
class=class="str">"cmt">//--- The chart properties change event
   ChartEventChartChange();
   }

「把工具请下神坛」

这一节把前面六篇做的菜单项控件、CWindow 表单增强和 CWndEvents 事件类收了个口。当前阶段库文件已经能跑:MQL4 包 15.02 KB、MQL5 包 19.1 KB,直接丢进对应终端就能测界面响应。 作者在原讨论里确认过,这套库能为自定义测试器搭图形面板,2D 图表类也已存在(改自标准库),但完整绘图库要等约 20 篇系列更完才稳。别把它当黑箱圣杯——高频重报价环境下界面延迟和真实成交偏差,只能靠你自己开 MT5 用历史+实时对照去嗅。 下一篇原计划做分隔线与上下文菜单类,想验证就先下 zip 把菜单项跑通,再决定跟不跟后续封装。

MQL5 / C++
Список статей (глав) второй части:
Графические интерфейсы II: Элемент <span class="class="type">class="kw">string">"菜单项"。</span> (Глава <span class="number">class="num">1</span>)
Графические интерфейсы II: Элементы <span class="class="type">class="kw">string">"分界线"。</span> и <span class="class="type">class="kw">string">"上下文菜单"。</span> (Глава <span class="number">class="num">2</span>)
Графические интерфейсы II: Настройка обработчиков событий библиотеки (Глава <span class="number">class="num">3</span>)
Графические интерфейсы II: Элемент <span class="class="type">class="kw">string">"主菜单"</span> (Глава <span class="number">class="num">4</span>)
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到菜单式面板如何隔离多窗口事件,你专注策略而不是控件 bug。

常见问题

因为菜单项作为新控件类型加入后,原指针保存结构无法覆盖下拉与上下文嵌套,需要扩展基类才能统一调度。
倾向用于切换界面可见性或激活某类行情过程,点击后外观变化反馈当前状态,不涉价格方向判断。
可以参照本篇类结构,小布的品种页已抽象了类似菜单容器,你只需替换选项逻辑而不必重写事件层。
对应一组中仅一个可启用的开关元件,本篇称为单选项目,常用于互斥的显示模式切换。
菜单项类设计为可嵌套,后续控件可挂载自身上下文,本篇仅演示主菜单调用,具体在系列其他文展开。