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

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

(2/3)· 多数自建面板的下拉菜单写死在事件里,本篇把按钮、复选、单选拆成类,后续随便挂

偏理论进阶 第 2/3 篇
自己给 EA 面板加菜单时,常把点击逻辑直接堆进 OnChartEvent,改一个选项要动一片代码。先把菜单项当独立元件来设计,后面接上下文菜单和多窗口都省力。

面板控件的私有状态与外观接口

在 MT5 自定义面板里,一个可点击项往往要同时管背景、图标、文字三套视觉状态。上面这段类定义把右侧箭头图标路径、点击优先级、可用/禁用标记、勾选框与单选钮状态、右键菜单开关都塞进了 private 区,外部只能走 public 方法改,避免运行时被随意篡改。 m_zorder 这个 int 专门控制同层控件的点击穿透顺序,数值大的优先响应;m_item_state 为 false 时整个条目直接不接收鼠标事件,做灰度禁用很方便。 public 区暴露的接口全是单行 setter:AreaBackColor 改激活背景色,AreaBackColorOff 管未激活背景,IconFileOn/Off 切换两种图标文件。LabelText 是唯一的 getter,直接返回内部标签控件的 Description,LabelXGap / LabelYGap 则以像素 int 微调文字相对图标的偏移。 开 MT5 新建个 panel 类,把这段粘进头文件,编译后改 m_label_x_gap 从 2 调到 8,能直观看到文字右移 6 像素,验证布局参数是否生效。

MQL5 / C++
  class="type">class="kw">string                m_right_arrow_file_on;
  class="type">class="kw">string                m_right_arrow_file_off;
  class=class="str">"cmt">//--- General priority for clicking
  class="type">int                   m_zorder;
  class=class="str">"cmt">//--- Available/blocked
  class="type">bool                  m_item_state;
  class=class="str">"cmt">//--- Checkbox state
  class="type">bool                  m_checkbox_state;
  class=class="str">"cmt">//--- State of the radio button and its identifier
  class="type">bool                  m_radiobutton_state;
  class="type">int                   m_radiobutton_id;
  class=class="str">"cmt">//--- State of the context menu
  class="type">bool                  m_context_menu_state;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- Background methods
  class="type">void                  AreaBackColor(class="kw">const class="type">class="kw">color clr)                  { m_area_color=clr;                    }
  class="type">void                  AreaBackColorOff(class="kw">const class="type">class="kw">color clr)                { m_area_color_off=clr;                }
  class="type">void                  AreaBorderColor(class="kw">const class="type">class="kw">color clr)                 { m_area_border_color=clr;             }
  class=class="str">"cmt">//--- Label methods
  class="type">void                  IconFileOn(class="kw">const class="type">class="kw">string file_path)               { m_icon_file_on=file_path;            }
  class="type">void                  IconFileOff(class="kw">const class="type">class="kw">string file_path)              { m_icon_file_off=file_path;           }
  class=class="str">"cmt">//--- Text label methods
  class="type">class="kw">string                LabelText(class="type">void)                                 class="kw">const { class="kw">return(m_label.Description());     }
  class="type">void                  LabelXGap(class="kw">const class="type">int x_gap)                       { m_label_x_gap=x_gap;                 }
  class="type">void                  LabelYGap(class="kw">const class="type">int y_gap)                       { m_label_y_gap=y_gap;                 }
  class="type">void                  LabelColor(class="kw">const class="type">class="kw">color clr)                      { m_label_color=clr;                   }

◍ 菜单项的颜色与状态接口怎么写

在 MT5 自定义面板里,每个菜单项都要暴露一组 setter / getter 来控制视觉与交互状态。下面这段接口定义了标签常态色、悬停色、右键箭头图标路径,以及复选框、单选钮、上下文菜单的开关。 LabelColorOff 和 LabelColorHover 分别接收 color 类型参数写入成员变量,决定鼠标离开与悬停时标签的着色;RightArrowFileOn / Off 则接收 string 类型的 bmp 或 png 路径,用于标记该项是否挂了上下文菜单。 ItemState、CheckBoxState、RadioButtonState 都是典型的 void 设值 + const 取值重载:比如 CheckBoxState(const bool flag) 写 m_checkbox_state,而 CheckBoxState(void) const 直接 return 该布尔值,调用方无需关心内部存储。RadioButtonID 用 int 区分同组单选钮,ContextMenuState(void) const 只返回 m_context_menu_state 表示右键菜单是否展开。 开 MT5 新建一个 C++ 风格面板类时,照这套签名抄一遍,就能在 EA 运行时动态灰掉某个菜单项或切换单选状态,省得每次重绘整个 GUI。外汇与贵金属波动剧烈,这类界面状态误判可能引发错单,上线前务必在策略测试器里手动点一遍。

MQL5 / C++
class="type">void LabelColorOff(class="kw">const class="type">class="kw">color clr) { m_label_color_off=clr; }
class="type">void LabelColorHover(class="kw">const class="type">class="kw">color clr) { m_label_color_hover=clr; }
class=class="str">"cmt">//--- Methods to indicate the presence of the context menu
class="type">void RightArrowFileOn(class="kw">const class="type">class="kw">string file_path) { m_right_arrow_file_on=file_path; }
class="type">void RightArrowFileOff(class="kw">const class="type">class="kw">string file_path){ m_right_arrow_file_off=file_path;}
class=class="str">"cmt">//--- Common(class="num">1) state of the item and(class="num">2) the checkbox item
class="type">void ItemState(class="kw">const class="type">bool state);
class="type">bool ItemState(class="type">void) class="kw">const { class="kw">return(m_item_state); }
class="type">void CheckBoxState(class="kw">const class="type">bool flag) { m_checkbox_state=flag; }
class="type">bool CheckBoxState(class="type">void) class="kw">const { class="kw">return(m_checkbox_state); }
class=class="str">"cmt">//--- Radio item identifier
class="type">void RadioButtonID(class="kw">const class="type">int id) { m_radiobutton_id=id; }
class="type">int RadioButtonID(class="type">void) class="kw">const { class="kw">return(m_radiobutton_id); }
class=class="str">"cmt">//--- State of the radio item
class="type">void RadioButtonState(class="kw">const class="type">bool flag){ m_radiobutton_state=flag; }
class="type">bool RadioButtonState(class="type">void) class="kw">const { class="kw">return(m_radiobutton_state); }
class=class="str">"cmt">//--- State of the context menu attached to this item
class="type">bool ContextMenuState(class="type">void) class="kw">const { class="kw">return(m_context_menu_state); }

「菜单项类如何挂到窗口指针上」

在 MT5 自定义 GUI 里,CMenuItem 继承自 CElement,但它自己不持有画布,必须先把所属 CWindow 的指针存进来才能创建控件。WindowPointer 方法用 GetPointer 把窗体对象地址写进私有成员 m_wnd,这一步漏掉,后面 CreateMenuItem 会直接返回 false。 CreateMenuItem 接收 chart_id、subwin、index_number、label_text、x、y 等 7 个参数。函数开头先用 CheckPointer(m_wnd)==POINTER_INVALID 判断窗体指针是否有效,无效就在终端打印提示并退出,不会往下跑。 有效时,m_id 取 m_wnd.LastId()+1,也就是父窗体当前最大控件 ID 再加 1,保证同窗体内 ID 不撞车。坐标方面用 XGap/YGap 记录相对窗体原点的偏移,而非绝对坐标,这样窗体移动时菜单项跟着走。 下面这段是原文里的核心声明与创建框架,逐行拆一下: class CMenuItem : public CElement // 定义菜单项类,公开继承 CElement 基类 { private: // 私有区开始 CWindow *m_wnd; // 指向所属窗体的指针,未赋值前为无效指针 public: // 公开区开始 void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } // 传入窗体引用,用全局 GetPointer 取地址存入 m_wnd }; bool CMenuItem::CreateMenuItem(const long chart_id,const int subwin,const int index_number,const string label_text,const int x,const int y) // 创建菜单项,绑定图表ID、子窗口、序号、文本与坐标 { if(::CheckPointer(m_wnd)==POINTER_INVALID) // 检查窗体指针,无效则 { ::Print(__FUNCTION__," > Before creating a menu item, the class has to be passed " "the window pointer: CMenuItem::WindowPointer(CWindow &object)"); // 打印报错函数名与指引 return(false); // 直接退出,创建失败 } m_id =m_wnd.LastId()+1; // 控件ID = 父窗体末位ID + 1 m_x =x; m_y =y; // 记录传入坐标 CElement::XGap(m_x-m_wnd.X()); // 计算相对父窗体X偏移 CElement::YGap(m_y-m_wnd.Y()); // 计算相对父窗体Y偏移 }

MQL5 / C++
class CMenuItem : class="kw">public CElement
  {
class="kw">private:
  CWindow  *m_wnd;
class="kw">public:
  class="type">void  WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
  };

class="type">bool CMenuItem::CreateMenuItem(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int index_number,class="kw">const class="type">class="kw">string label_text,class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
  if(::CheckPointer(m_wnd)==POINTER_INVALID)
    {
    ::Print(__FUNCTION__," > Before creating a menu item, the class has to be passed "
            "the window pointer: CMenuItem::WindowPointer(CWindow &object)");
    class="kw">return(class="kw">false);
    }
  m_id =m_wnd.LastId()+class="num">1;
  m_x =x; m_y =y;
  CElement::XGap(m_x-m_wnd.X());
  CElement::YGap(m_y-m_wnd.Y());
  }

菜单项隐藏与图标资源的兜底逻辑

CMenuItem::Hide() 里先判断 m_is_visible,若元素已隐藏直接 return,避免重复操作图形对象。随后用 for(int i=0;i<ObjectsElementTotal();i++) 遍历所有子对象,对每个调用 Object(i).Timeframes(OBJ_NO_PERIODS),实质是把对象从所有周期可见性里摘掉,实现整组隐藏。 隐藏时还会把 m_context_menu_state 置 false、CElement::m_is_visible 置 false、MouseFocus(false),并把背景色复位成 m_area_color、箭头 State(false)。这套复位保证最小化或手动隐藏后,控件状态不残留焦点与高亮。 图标创建函数 CreateIcon() 按菜单项类型分支:简单项或带上下文菜单项,若 m_icon_file_on / m_icon_file_off 为空就直接返回 true 不建图标;而 MI_CHECKBOX 与 MI_RADIOBUTTON 若缺省图标,则强制填入内置位图 Images\Controls\CheckBoxOn_min_white.bmp(选中)与 CheckBoxOn_min_gray.bmp(未选)。这两个 bmp 通过 #resource 预编译指令打进 ex5,运行时无需外部文件。 对象名由 ProgramName()_menuitem_icon_ 拼接 Index() 与 Id() 构成,保证同 EA 内多菜单项图标对象名不冲突。开 MT5 把这段类方法塞进你的面板工程,断点看 Hide() 后 OBJ_NO_PERIODS 是否真的让箭头在图表上消失,就能验证这套显隐链是否可靠。

MQL5 / C++
   class="kw">return(class="kw">false);
   if(!CreateLabel())
      class="kw">return(class="kw">false);
   if(!CreateArrow())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- If the window is minimized, hide the element after creation
   if(m_wnd.IsMinimized())
      Hide();
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Hides the menu item                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::Hide(class="type">void)
   {
class=class="str">"cmt">//--- Leave, if the element is hidden
   if(!CElement::m_is_visible)
      class="kw">return;
class=class="str">"cmt">//--- Hide all objects
   for(class="type">int i=class="num">0;i<ObjectsElementTotal(); i++)
      Object(i).Timeframes(OBJ_NO_PERIODS);
class=class="str">"cmt">//--- Zeroing variables
   m_context_menu_state=class="kw">false;
   CElement::m_is_visible=class="kw">false;
   CElement::MouseFocus(class="kw">false);
class=class="str">"cmt">//--- Reset the class="type">class="kw">color
   m_area.BackColor(m_area_color);
   m_arrow.State(class="kw">false);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creates an item label                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Images\\Controls\\CheckBoxOn_min_gray.bmp"
class="macro">#resource "\\Images\\Controls\\CheckBoxOn_min_white.bmp"
class=class="str">"cmt">//---
class="type">bool CMenuItem::CreateIcon(class="type">void)
   {
class=class="str">"cmt">//--- If this is a simple item or an item containing a context menu
   if(m_type_menu_item==MI_SIMPLE || m_type_menu_item==MI_HAS_CONTEXT_MENU)
     {
      class=class="str">"cmt">//--- If the label is not required(icon is not defined), leave
      if(m_icon_file_on=="" || m_icon_file_off=="")
         class="kw">return(true);
     }
class=class="str">"cmt">//--- If this is a checkbox
   else if(m_type_menu_item==MI_CHECKBOX)
     {
      class=class="str">"cmt">//--- If the icon is not defined, set the class="kw">default one
      if(m_icon_file_on=="")
         m_icon_file_on="Images\\Controls\\CheckBoxOn_min_white.bmp";
      if(m_icon_file_off=="")
         m_icon_file_off="Images\\Controls\\CheckBoxOn_min_gray.bmp";
     }
class=class="str">"cmt">//--- If this is a radio item   
   else if(m_type_menu_item==MI_RADIOBUTTON)
     {
      class=class="str">"cmt">//--- If the icon is not defined, set the class="kw">default one
      if(m_icon_file_on=="")
         m_icon_file_on="Images\\Controls\\CheckBoxOn_min_white.bmp";
      if(m_icon_file_off=="")
         m_icon_file_off="Images\\Controls\\CheckBoxOn_min_gray.bmp";
     }
class=class="str">"cmt">//--- Forming the object name
   class="type">class="kw">string name=CElement::ProgramName()+"_menuitem_icon_"+(class="type">class="kw">string)CElement::Index()+"__"+(class="type">class="kw">string)CElement::Id();
class=class="str">"cmt">//--- Object coordinates

◍ 菜单项图标定位与显隐状态控制

在自定义 UI 里挂一个菜单项,先算锚点坐标:x 轴在基类 m_x 基础上右移 7 像素,y 轴在 m_y 基础上下移 4 像素。这个偏移量直接决定图标贴着窗口边缘的哪个位置,改这两个数就能微调控件间距。 创建图标对象后必须补一组属性:开/关两张 BMP 位图路径、当前状态、角落对齐、锚点、是否可选中、Z 轴层级和悬停提示。其中 Selectable(false) 让图标不参与鼠标框选,Tooltip("\n") 清空默认浮窗,避免干扰价格行为观察。 Show() 方法负责把元素刷出来。已可见就直接 return;否则遍历 ObjectsElementTotal() 个子对象,统一设 OBJ_ALL_PERIODS。 checkbox 和 radiobutton 两类要按自身布尔状态切 OBJ_ALL_PERIODS / OBJ_NO_PERIODS,其余类型无条件显示。 外汇与贵金属图表加载这类 GUI 有滑点卡顿风险,建议在 MT5 策略测试器先用 EURUSD M5 跑一遍,确认 m_icon.Create 返回 true 且无对象泄漏再上实盘。

MQL5 / C++
  class="type">int x =m_x+class="num">7;
  class="type">int y =m_y+class="num">4;
class=class="str">"cmt">//--- Set the label
  if(!m_icon.Create(m_chart_id,name,m_subwin,x,y))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- Set properties
  m_icon.BmpFileOn("::"+m_icon_file_on);
  m_icon.BmpFileOff("::"+m_icon_file_off);
  m_icon.State(m_item_state);
  m_icon.Corner(m_corner);
  m_icon.GetInteger(OBJPROP_ANCHOR,m_anchor);
  m_icon.Selectable(class="kw">false);
  m_icon.Z_Order(m_zorder);
  m_icon.Tooltip("\n");
class=class="str">"cmt">//--- Margins from the edge point
  m_icon.XGap(x-m_wnd.X());
  m_icon.YGap(y-m_wnd.Y());
class=class="str">"cmt">//--- Store the object pointer
  CElement::AddToArray(m_icon);
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Makes the menu item visible                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::Show(class="type">void)
  {
class=class="str">"cmt">//--- Leave, if the element is already visible
  if(CElement::m_is_visible)
      class="kw">return;
class=class="str">"cmt">//--- Make all the objects visible
  for(class="type">int i=class="num">0; i<ObjectsElementTotal(); i++)
      Object(i).Timeframes(OBJ_ALL_PERIODS);
class=class="str">"cmt">//--- If this is a checkbox, then considering its state
  if(m_type_menu_item==MI_CHECKBOX)
      m_icon.Timeframes((m_checkbox_state)? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
class=class="str">"cmt">//--- If this is a radio item, then considering its state
  else if(m_type_menu_item==MI_RADIOBUTTON)
      m_icon.Timeframes((m_radiobutton_state)? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
class=class="str">"cmt">//--- Zeroing variables
  CElement::m_is_visible=true;
  CElement::MouseFocus(class="kw">false);
  }

「把菜单项挂进交易面板窗口」

上面这段类声明和函数实现,展示了如何用 CProgram 把第一个菜单项塞进 MT5 自定义面板。核心入口是 CreateTradePanel,它先建窗体再调 CreateMenuItem1,任何一步返回 false 就整体放弃,避免半残界面留在图上。 CreateMenuItem1 里写死了两个偏移宏:MENU_ITEM1_GAP_X 为 6、MENU_ITEM1_GAP_Y 为 25,意味着菜单项相对窗口左上角向右 6 像素、向下 25 像素起笔。菜单项本体尺寸锁在 XSize(193)、YSize(24),也就是 193×24 的点击区,偏小但够放文字加图标。 图标用了两组 bmp:选中态 bar_chart.bmp、未选态 bar_chart_colorless.bmp,都从 \Images\Controls\ 下通过 #resource 预载。LabelColor 和 LabelColorHover 均设 clrWhite, hover 无视觉变化,若你想让鼠标悬停有反馈得自己改色值。 开 MT5 自建 EA 面板时,直接抄这套 CreateMenuItem1 骨架,把 item_text 换成你的指令名,再调 XSize / YSize 适配标签长度即可;外汇与贵金属杠杆高,面板只是辅助下单工具,任何一键发单都需人工复核仓位风险。

MQL5 / C++
class="type">void OnDeinitEvent(class="kw">const class="type">int reason);
class=class="str">"cmt">//--- Timer
class="type">void OnTimerEvent(class="type">void);
class=class="str">"cmt">//---
class="kw">protected:
class=class="str">"cmt">//--- Chart event handler
class="kw">virtual class="type">void 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">//---
class="kw">public:
class=class="str">"cmt">//--- Creates the trading panel
class="type">bool CreateTradePanel(class="type">void);
class=class="str">"cmt">//---
class="kw">private:
class=class="str">"cmt">//--- Creating a form
class="type">bool CreateWindow(class="kw">const class="type">class="kw">string text);
class=class="str">"cmt">//--- Creating a menu item
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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creates a menu item                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Images\\Controls\\bar_chart.bmp"
class="macro">#resource "\\Images\\Controls\\bar_chart_colorless.bmp"
class=class="str">"cmt">//---
class="type">bool CProgram::CreateMenuItem1(class="type">class="kw">string item_text)
  {
class=class="str">"cmt">//--- Store the window pointer
  m_menu_item1.WindowPointer(m_window);
class=class="str">"cmt">//--- Coordinates  
  class="type">int x=m_window.X()+MENU_ITEM1_GAP_X;
  class="type">int y=m_window.Y()+MENU_ITEM1_GAP_Y;
class=class="str">"cmt">//--- Set up properties before creation
  m_menu_item1.XSize(class="num">193);
  m_menu_item1.YSize(class="num">24);
  m_menu_item1.TypeMenuItem(MI_HAS_CONTEXT_MENU);
  m_menu_item1.IconFileOn("Images\\Controls\\bar_chart.bmp");
  m_menu_item1.IconFileOff("Images\\Controls\\bar_chart_colorless.bmp");
  m_menu_item1.LabelColor(clrWhite);
  m_menu_item1.LabelColorHover(clrWhite);
class=class="str">"cmt">//--- Creating a menu item
  if(!m_menu_item1.CreateMenuItem(m_chart_id,m_subwin,class="num">0,item_text,x,y))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creates the trading panel                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateTradePanel(class="type">void)
  {
class=class="str">"cmt">//--- Creating a form for controls
  if(!CreateWindow("EXPERT PANEL"))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- Creating controls:
class=class="str">"cmt">//    Menu item
  if(!CreateMenuItem1("Menu item"))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- Redrawing of the chart
  m_chart.Redraw();
  class="kw">return(true);
  }
class CMenuItem : class="kw">public CElement
  {
交给小布盯盘看盘口
这些界面分层与事件拆分的思路,小布盯盘的 AIGC 已内置到品种页的面板诊断里,打开对应品种即可看到结构提示,你专注策略而非重画控件。

常见问题

多窗口模式与上下文菜单的正式接入留到系列第四部分,本篇先夯实菜单项基类与事件容器扩展。
多用于切换某过程开关或界面区块可见性,选中后外观变化并反映当前状态,便于用户辨识。
为节约篇幅,跨类重复的方法以附件为准,遇到缺失片段直接查档案内对应文件继续跟进。
可以,小布盯盘品种页内置了面板结构诊断,能标出事件耦合过重的位置,减少手写菜单的维护成本。
称为单选项目,组内仅一个可启用,类似单选按钮;复选框则允许独立两态切换,不互斥。