图形界面 II: 分隔线和上下文菜单元件 (第二章)·进阶篇
把分隔线挂进表单跑通测试
前面一篇已经把菜单项塞进表单,分隔线走的是同一套附加逻辑:把它当独立界面元件处理即可。库里若还没有这个元件类,第一步是在 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。
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 高风险,实盘前务必在策略测试器里先跑通事件响应。
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 响应,属高风险环境下的性能权衡。
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[] 分别存可用和禁用状态下标签的位图路径。在调用创建菜单的方法前,应先通过这些数组成员把每一项属性填好。
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 跑通交互再上真仓。
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) {