图形界面 II: 分隔线和上下文菜单元件 (第二章)·进阶篇
📘

图形界面 II: 分隔线和上下文菜单元件 (第二章)·进阶篇

第 2/3 篇

把分隔线挂进表单跑通测试

前面一篇已经把菜单项塞进表单,分隔线走的是同一套附加逻辑:把它当独立界面元件处理即可。库里若还没有这个元件类,第一步是在 WndContainer.mqh 里把对应文件 include 进来。 应用层的自定义类(示例里是 CProgram)必须继承自 CWndContainer 再经 CWndEvents 派生,include 完元件文件后,就能直接 new 出元件实例并调它的方法。 随后在图形界面程序的创建流程里调用元件的构建方法。编译完拖进图表,正常的话界面会多出一条分隔线(原文图 2 所示),这一步验证了 CSeparateLine 类本身已可用。 到此 CSeparateLine 的开发收口,后面要做上下文菜单时,这类基础元件就已经齐活。

「组合控件:上下文菜单类的骨架」

在界面库里,上下文菜单被归为复杂控件,它不像 CWindow、CMenuItem、CSeparateLine 那样只由基本对象拼成,而是内嵌了其他元件,所有元件基类都是 CElement。先在 Controls 目录建 ContextMenu.mqh,把 Element、Window、MenuItem、SeparateLine 几个头文件包含进来,才能复用已有零件。 CContextMenu 类要持有表单指针、标准虚方法,以及背景用的 OBJ_RECTANGLE_LABEL 对象(借 CRectLabel 实现)。菜单项和分隔线数量在构造时未知,所以必须用动态数组声明 CMenuItem 和 CSeparateLine 的实例集合,这是组合控件和简单元件最实在的区别。 菜单项必须能反向访问它的前驱节点,否则级联菜单管理会乱套。CContextMenu 和 CMenuItem 都要保存一个 CMenuItem* 指针,既用来串起附加关系,也用来在创建时校验调用顺序对不对。 对外提供 CContextMenu::AddItem() 接收文字、可用/禁用图标路径、类型四个参数,每次调用把内部数组扩容一个;分隔线则走 AddSeparateLine() 并记下标到 m_sep_line_index[]。取项方法里做了边界钳制:索引超上限取末项,小于零取首项,避免越界崩 GUI。 单选组靠组编号加组内相对索引区分,默认组号是 0,不处理就永远只有一组单选。复选框状态有独立读写方法。创建分三步私有方法——背景、菜单项、分隔线,再由公有 CreateContextMenu() 统一调。背景高度不写死,由 CreateArea() 按(菜单项数 + 分隔线数×9像素)算,因为一条分隔线占 9 像素高。 CreateItems() 里循环排坐标,比对 m_sep_line_index[] 决定在哪插分隔线,同时给每项存前节点指针。CreateContextMenu() 默认坐标零,相对前节点自动算;若传了至少一个坐标就取消自动。附加模式枚举进 Enums.mqh,默认按前节点右侧排,从属主菜单项时按底部排。 创建完必须调 Hide() 把背景、分隔线、各项全隐藏,等点击再弹。Delete() 除了删图对象,还要清空所有构建数组,否则切换品种或周期时菜单项会越叠越多——测试时注释掉那几行就能复现 bug。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ContextMenu.mqh |
class=class="str">"cmt">//|                     Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Element.mqh"
class="macro">#include "Window.mqh"
class="macro">#include "MenuItem.mqh"
class="macro">#include "SeparateLine.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于创建上下文菜单的类                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 指向元件附加表单的指针

◍ 右键菜单类的私有成员与构造细节

在自定义右键菜单类里,私有区先挂了窗口指针与背景绘制对象:CWindow* m_wnd 负责回指所属窗口,CRectLabel m_area 承担菜单背景矩形。菜单项与分隔线用动态数组 CMenuItem m_items[] 和 CSeparateLine m_sep_line[] 管理,这意味着增删菜单项只需调整数组长度,不用重写布局逻辑。 背景外观全部收在私有变量中:m_area_zorder 控层级,m_area_color 与 m_area_border_color 定底色和边框,m_area_color_hover 管鼠标悬停高亮,m_area_color_array[] 则允许按项索引切换颜色。实际调试时改这几个值,菜单观感立刻变。 构造函数只做两件实事:CElement::ClassName(CLASS_NAME) 把类名登记进基类,CElement::m_is_dropdown=true 标记自己属于下拉元件。析构函数为空,资源释放交给 Reset() 与 Delete() 在别处处理。外汇与贵金属图表上挂这类 GUI 高风险,实盘前务必在策略测试器里先跑通事件响应。

MQL5 / C++
  CWindow              *m_wnd;
  class=class="str">"cmt">//--- 用于创建菜单项的对象
  CRectLabel            m_area;
  CMenuItem             m_items[];
  CSeparateLine         m_sep_line[];
  class=class="str">"cmt">//---
class="kw">public:
                      CContextMenu(class="type">void);
                     ~CContextMenu(class="type">void);
  class=class="str">"cmt">//--- 保存传来的指针
  class="type">void                WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
  class=class="str">"cmt">//--- 图表事件处理函数
  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">virtual class="type">void        OnEventTimer(class="type">void);
  class=class="str">"cmt">//--- 移动元件
  class="kw">virtual class="type">void        Moving(class="kw">const class="type">int x,class="kw">const class="type">int y);
  class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除
  class="kw">virtual class="type">void        Show(class="type">void);
  class="kw">virtual class="type">void        Hide(class="type">void);
  class="kw">virtual class="type">void        Reset(class="type">void);
  class="kw">virtual class="type">void        Delete(class="type">void);
  class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置鼠标左键点击的优先级
  class="kw">virtual class="type">void        SetZorders(class="type">void);
  class="kw">virtual class="type">void        ResetZorders(class="type">void);
  };
CContextMenu::CContextMenu(class="type">void)
  {
  CElement::ClassName(CLASS_NAME);
  CElement::m_is_dropdown=true;
  }
CContextMenu::~CContextMenu(class="type">void)
  {
  }
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 背景属性
  class="type">int                 m_area_zorder;
  class="type">color               m_area_color;
  class="type">color               m_area_border_color;
  class="type">color               m_area_color_hover;
  class="type">color               m_area_color_array[];
  class=class="str">"cmt">//--- 菜单项属性

上下文菜单的外观字段与方法

在 MT5 自定义控件库里,右键上下文菜单的视觉状态由一组私有成员加公有 setter 控制。下面这段声明覆盖了菜单项尺寸、背景、边框、悬停态以及分隔线双色,是改皮肤前必须认全的变量。 int m_item_y_size; color m_item_back_color; color m_item_border_color; color m_item_back_color_hover; color m_item_back_color_hover_off; color m_label_color; color m_label_color_hover; string m_right_arrow_file_on; string m_right_arrow_file_off; //--- 分隔线属性 color m_sepline_dark_color; color m_sepline_light_color; 以上每层颜色都对应一种交互态:hover 是鼠标掠过可用项,hover_off 是掠过禁用项,sepline 用 dark/light 双色模拟立体分隔。 公开方法里 ItemsTotal() 直接返回 m_items 数组大小,也就是菜单项数量;MenuBackColor() 和 MenuBorderColor() 只改菜单整块区域的底色与描边。ItemYSize(20) 这类调用决定单项高度,默认不设时多数库给的是 20 像素,过密列表可降到 16。 void ItemsTotal(void) const { return(::ArraySize(m_items)); } void MenuBackColor(const color clr) { m_area_color=clr; } void MenuBorderColor(const color clr) { m_area_border_color=clr; } void ItemYSize(const int y_size) { m_item_y_size=y_size; } void ItemBackColor(const color clr) { m_item_back_color=clr; } void ItemBorderColor(const color clr) { m_item_border_color=clr; } void ItemBackColorHover(const color clr) { m_item_back_color_hover=clr; } void ItemBackColorHoverOff(const color clr) { m_item_back_color_hover_off=clr; } void LabelColor(const color clr) { m_label_color=clr; } 别把悬停色和选中色混为一谈 m_item_back_color_hover 只在鼠标移动上去但未点击时生效,真选中后的态往往由别处回调重绘。调色时先单独改 hover 看高亮是否跟手,再处理 hover_off,否则禁用项也会显得可点。 开 MT5 新建个 EA 把上面成员塞进你的 CContextMenu 类,编译后右键调出菜单,逐项改 color 参数就能直观比对。外汇与贵金属图表上挂这类自定义控件需注意,过度重绘可能拖慢 tick 响应,属高风险环境下的性能权衡。

MQL5 / C++
class="type">int                m_item_y_size;
class="type">color               m_item_back_color;
class="type">color               m_item_border_color;
class="type">color               m_item_back_color_hover;
class="type">color               m_item_back_color_hover_off;
class="type">color               m_label_color;
class="type">color               m_label_color_hover;
class="type">class="kw">string              m_right_arrow_file_on;
class="type">class="kw">string              m_right_arrow_file_off;
class=class="str">"cmt">//--- 分隔线属性
class="type">color               m_sepline_dark_color;
class="type">color               m_sepline_light_color;
class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 菜单项数量
  class="type">int                ItemsTotal(class="type">void)                class="kw">const { class="kw">return(::ArraySize(m_items));      }
  class=class="str">"cmt">//--- 用于设置上下文菜单外观的方法:
  class=class="str">"cmt">//    上下文菜单背景颜色
  class="type">void               MenuBackColor(class="kw">const class="type">color clr)       { m_area_color=clr;                 }
  class="type">void               MenuBorderColor(class="kw">const class="type">color clr)      { m_area_border_color=clr;          }
  class=class="str">"cmt">//--- (class="num">1) 高度, (class="num">2) 背景色 and(class="num">3) 菜单项边框颜色 
  class="type">void               ItemYSize(class="kw">const class="type">int y_size)           { m_item_y_size=y_size;            }
  class="type">void               ItemBackColor(class="kw">const class="type">color clr)        { m_item_back_color=clr;           }
  class="type">void               ItemBorderColor(class="kw">const class="type">color clr)       { m_item_border_color=clr;         }
  class=class="str">"cmt">//--- 当鼠标掠过菜单项时的背景色 (class="num">1) 可用状态 (class="num">2) 禁用状态
  class="type">void               ItemBackColorHover(class="kw">const class="type">color clr)   { m_item_back_color_hover=clr;     }
  class="type">void               ItemBackColorHoverOff(class="kw">const class="type">color clr){ m_item_back_color_hover_off=clr; }
  class=class="str">"cmt">//--- (class="num">1) 标准 (class="num">2) 有焦点时 的文字颜色 
  class="type">void               LabelColor(class="kw">const class="type">color clr)           { m_label_color=clr;               }

「右键菜单类的指针与配色接口」

在 MT5 自定义图形库里,上下文菜单和菜单项都继承自 CElement,二者各自维护一个指向前一节点的 CMenuItem 指针 m_prev_node,用于回溯菜单树。 设置悬停标签色用 LabelColorHover(const color clr),内部直接赋值 m_label_color_hover;右键箭头图标分两种状态,RightArrowFileOn / RightArrowFileOff 各接收一个 bmp 文件路径字符串。分隔线配色拆成深色与浅色两组,SeparateLineDarkColor 和 SeparateLineLightColor 分别写入 m_sepline_dark_color 与 m_sepline_light_color。 获取前节点指针有两个重载:无参版本 CMenuItem *PrevNodePointer(void) const 直接 return m_prev_node;带引用参数版本通过 ::GetPointer(object) 把传入对象转成指针存回 m_prev_node。CContextMenu 与 CMenuItem 里的这段实现完全一致,说明节点回溯逻辑在两层复用。 CContextMenu 还持有三个字符串数组:m_label_text[] 存菜单文字,m_path_bmp_on[] 与 m_path_bmp_off[] 分别存可用和禁用状态下标签的位图路径。在调用创建菜单的方法前,应先通过这些数组成员把每一项属性填好。

MQL5 / C++
class="type">void                LabelColorHover(class="kw">const class="type">color clr)                { m_label_color_hover=clr;                 }
class=class="str">"cmt">//--- 指示菜单项有上下文菜单时的图标
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">//--- 分隔线的 (class="num">1) 深色 和 (class="num">2) 浅色
class="type">void                SeparateLineDarkColor(class="kw">const class="type">color clr)           { m_sepline_dark_color=clr;                }
class="type">void                SeparateLineLightColor(class="kw">const class="type">color clr)           { m_sepline_light_color=clr;                }
class=class="str">"cmt">//---
};
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 指向前一节点的指针
  CMenuItem         *m_prev_node;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 获取和保存前一个节点的指针
  CMenuItem         *PrevNodePointer(class="type">void)                            class="kw">const { class="kw">return(m_prev_node);                    }
  class="type">void               PrevNodePointer(CMenuItem &object)                     { m_prev_node=::GetPointer(object);       }
  class=class="str">"cmt">//---
};
class CMenuItem : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 指向前一节点的指针
  CMenuItem         *m_prev_node;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 获取和保存前一个节点的指针
  CMenuItem         *PrevNodePointer(class="type">void)                            class="kw">const { class="kw">return(m_prev_node);                    }
  class="type">void               PrevNodePointer(CMenuItem &object)                     { m_prev_node=::GetPointer(object);       }
  class=class="str">"cmt">//---
};
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 菜单项属性的数组:
  class=class="str">"cmt">//    (class="num">1) 文字, (class="num">2) 可用项标签, (class="num">3) 禁用项标签
  class="type">class="kw">string             m_label_text[];
  class="type">class="kw">string             m_path_bmp_on[];
  class="type">class="kw">string             m_path_bmp_off[];
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 在创建上下文菜单前使用指定属性增加一个菜单项

◍ 给右键菜单挂上子项与分隔线

在 MT5 自定义图形界面里,上下文菜单靠 CContextMenu 类动态堆项。AddItem 每次调用把四个并行数组(m_items、m_label_text、m_path_bmp_on、m_path_bmp_off)各Resize+1,新项顺次填到 array_size 下标,菜单项类型由 TypeMenuItem(type) 写入。 分隔线不走菜单项数组,而是单独用 m_sep_line 与 m_sep_line_index 记录:AddSeparateLine 传入已有 item_index,扩容后只存索引,绘制时据此在对应项下方画线。 取数侧有三个公开方法:ItemPointerByIndex 返回 CMenuItem*,若 ArraySize(m_items)<1 直接报空;DescriptionByIndex 拿显示文字;TypeMenuItemByIndex 拿枚举类型。你在 EA 里右键弹菜单前,务必先确认 AddItem 调用次数 ≥ 1,否则指针为空会断事件。 外汇与贵金属图表加载这类面板属于高风险的界面扩展,逻辑错误可能阻塞主线程,建议在策略测试器先用模拟 tick 跑通交互再上真仓。

MQL5 / C++
class="type">void AddItem(class="kw">const class="type">class="kw">string text,class="kw">const class="type">class="kw">string path_bmp_on,class="kw">const class="type">class="kw">string path_bmp_off,class="kw">const ENUM_TYPE_MENU_ITEM type);
class=class="str">"cmt">//---
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 增加一个菜单项                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::AddItem(class="kw">const class="type">class="kw">string text,class="kw">const class="type">class="kw">string path_bmp_on,class="kw">const class="type">class="kw">string path_bmp_off,class="kw">const ENUM_TYPE_MENU_ITEM type)
  {
class=class="str">"cmt">//--- 把数组大小增加一个元件
   class="type">int array_size=::ArraySize(m_items);
   ::ArrayResize(m_items,array_size+class="num">1);
   ::ArrayResize(m_label_text,array_size+class="num">1);
   ::ArrayResize(m_path_bmp_on,array_size+class="num">1);
   ::ArrayResize(m_path_bmp_off,array_size+class="num">1);
class=class="str">"cmt">//--- 保存传入参数的值
   m_label_text[array_size]   =text;
   m_path_bmp_on[array_size]  =path_bmp_on;
   m_path_bmp_off[array_size] =path_bmp_off;
class=class="str">"cmt">//--- 设置菜单项类型
   m_items[array_size].TypeMenuItem(type);
   }
class CContextMenu : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 加入分隔线后菜单项的索引编号
   class="type">int                m_sep_line_index[];
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 创建上下文菜单前, 在指定的菜单项处增加分隔线
   class="type">void               AddSeparateLine(class="kw">const class="type">int item_index);
   class=class="str">"cmt">//---
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 增加分隔线                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CContextMenu::AddSeparateLine(class="kw">const class="type">int item_index)
  {
class=class="str">"cmt">//--- 把数组大小增加一个元件
   class="type">int array_size=::ArraySize(m_sep_line);
   ::ArrayResize(m_sep_line,array_size+class="num">1);
   ::ArrayResize(m_sep_line_index,array_size+class="num">1);
class=class="str">"cmt">//--- 保存索引编号
   m_sep_line_index[array_size]=item_index;
   }
class CContextMenu : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 从上下文菜单返回项目指针
   CMenuItem          *ItemPointerByIndex(class="kw">const class="type">int index);
   class=class="str">"cmt">//--- 返回描述 (显示的文字)
   class="type">class="kw">string             DescriptionByIndex(class="kw">const class="type">int index);
   class=class="str">"cmt">//--- 返回菜单项类型
   ENUM_TYPE_MENU_ITEM TypeMenuItemByIndex(class="kw">const class="type">int index);
   class=class="str">"cmt">//---
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据索引返回菜单项指针                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMenuItem *CContextMenu::ItemPointerByIndex(class="kw">const class="type">int index)
  {
   class="type">int array_size=::ArraySize(m_items);
class=class="str">"cmt">//--- 如果上下文菜单没有项目, 报告
   if(array_size<class="num">1)
     {

常见问题

把分隔线控件挂进表单后编译运行,右键或切换标签页看那条横线是否出现在预期位置;若没显示,先查父容器坐标和宽度是否给够。
用菜单类的添加子项方法并传入分隔线类型参数即可;分隔线本身不响应点击,只做视觉分组。
小布可以扫描你的控件代码,标出未初始化的指针与配色字段,并提示菜单外观方法是否缺调用。
主要管子项数组、外观字段和指针引用;构造时常漏掉配色接口赋值,导致菜单文字透明或边框消失。
分隔线只负责画,不绑事件;子项无响应多半是点击事件没挂到表单回调,或指针在构造后被提前释放。