MQL5 图形界面库开发:从分隔线到上下文菜单的底层封装(基础篇)
(1/3)·想把 MT5 界面做得像终端原生却总被基础控件卡住?这一章补齐分隔线与菜单类的地基
很多交易者试着给 EA 写自定义面板,却在分隔线立体感和右键菜单上反复碰壁,抄来的代码一改就崩。其实缺的不是耐心,而是没把基础元件做成可复用的类。这一章把底层控件拆开讲透,后面才能搭出稳的界面。
◍ 从零搭一个分隔线和右键菜单基类
在 MT5 自定义指标或 EA 面板里,分隔线和右键上下文菜单是最常被忽略、却最影响操作效率的两个元件。Anatoli Kazharski 在 2016-04-09 发布的示例帖累计 2843 次查看、3 条讨论,说明这类底层 GUI 封装仍有稳定需求。 这一节先解决「分隔线」:把它封装成一个独立类,而不是在 OnChartEvent 里硬编码坐标。这样后续面板每增加一组控件,只需 new 一个分隔线对象并挂到父容器,不用重写绘制逻辑。 紧接着是上下文菜单类。MT5 的 CHARTEVENT_OBJECT_CLICK 只告诉你哪个对象被点,不区分左键右键;右键菜单必须自己拦截 CHARTEVENT_MOUSE_CLICK 的按键状态并维护一个隐藏的 CMenu 对象树。封装成类后,挂到任意图表对象上都只要三行调用。 最后一步是把所有元件指针收进一个统一存储类,避免面板销毁时漏掉 Delete 导致图表残留孤儿对象。开 MT5 新建一个 Empty Indicator,把这两类先写出来,比直接抄现成面板更容易摸清事件流向。
从菜单项到分隔线:界面库的下一步拼图
上一章已经写好了菜单项类,它既能独立成控件,也能塞进上下文菜单和主菜单里用。这一章要补的是分隔线元件——同样不只能当独立界面件,也能作为其他元件的一部分存在。 把分隔线做出来之后,开发上下文菜单类所需的基础件就齐了,那部分会在后续文章里展开。另外本章还会补一个指针库类,专门存应用里所有图形界面元件的指针,方便统一调度。 目前库文件在压缩包里的目录结构必须原样落到 MT5 的 MQL5 目录下,否则包含关系会断。系列每章末尾都附了当前阶段完整库下载和文章索引,直接对照落盘即可验证。
「把分隔线做成可复用的画布控件」
MT5 的上下文菜单、状态栏乃至 MetaEditor 里都能看到分隔线,它本质是个独立图形元件。想让它脱离菜单、作为通用界面部件被任意控件调用,就得单独成类。 立体感来自明暗两条线:一条比背景浅、一条比背景深,视觉上像平面上刨出一道槽。实现上有两条路——用两个 CRectLabel 基本对象拼,或建一个 OBJ_BITMAP_LABEL 画布自己画。后者借标准库的 CCanvas 类能省掉大量底层绘图代码。 直接嵌 CCanvas 会踩坑:它和基类 CChartObject 都有 m_chart_id 变量,编译器报“变量名已存在”警告(图 1)。警告不阻断编译,但埋下未知运行风险。做法是把 Canvas.mqh 复制为 Include/Canvas/CustomCanvas.mqh,类改名 CCustomCanvas,继承 CChartObjectBmpLabel,并删掉其中的 m_chart_id。注意终端更新可能覆盖标准库,所以必须放自己库目录、不动原文件。 有了 CCustomCanvas,就能派生 CRectCanvas,再写 CSeparateLine。它声明指向所属表单的指针、持有一个 CRectCanvas 实例,并暴露类型(水平/垂直)、深线色、浅线色三个设置方法。公有方法 CreateSeparateLine() 需传入图表 id、窗口编号、线索引(循环中保证对象名唯一)、坐标和尺寸。 创建时对象类型是 OBJ_BITMAP_LABEL,用 CCustomCanvas::CreateBitmapLabel() 生成;与标准基类不同,它不会自动 Attach,必须手动调 CChartObject::Attach() 挂到图表,否则无法管理。随后 DrawSeparateLine() 清空画布、按方向算两线坐标、画完调 Update() 刷新。画布高度设成大于 2 像素,上下两线间就能留出间隙。
class="macro">#include <Files\FileBin.mqh> class="macro">#include <Controls\Rect.mqh> class="macro">#include <ChartObjects\ChartObjectsBmpControls.mqh> class=class="str">"cmt">//... class CCustomCanvas : class="kw">public CChartObjectBmpLabel { class=class="str">"cmt">//...
◍ 用类封装一条图表分隔线
在 MT5 自定义面板里画一条分隔线,最干净的做法是把它封成一个独立类,而不是在主程序里散着写坐标和画布。下面这段头文件定义了一个 CSeparateLine 类,继承自 CElement,专门负责在窗口内绘制并管理一条矩形画布分隔线。 类里先藏了两个私有成员:m_wnd 是指向所属表单窗口的指针,m_canvas 是 CRectCanvas 实例,真正干活画线的就是它。公开接口里 WindowPointer() 用 GetPointer() 接住外部传进来的窗口引用,避免 MT5 里对象指针失效的老坑。 对外暴露的虚函数覆盖了事件、移动、显隐和销毁:OnEvent() 接图表事件,Moving() 跟着窗口重定位,Show/Hide/Reset/Delete 四件套控制生命周期。你在 EA 或脚本里 new 一个 CSeparateLine,把窗口指针喂进去,就能在面板任意位置挂一条可管理的分隔线,比直接调 OBJ_RECTANGLE 省心。 外汇与贵金属图表上挂这类自定义元件,本质不改价格风险,只是视觉辅助;上线前建议在策略测试器里跑一遍非交易图表验证渲染不闪退。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Enums.mqh" class="macro">#include "Defines.mqh" class="macro">#include "..\Canvas\CustomCanvas.mqh" class="macro">#include <ChartObjects\ChartObjectsBmpControls.mqh> class="macro">#include <ChartObjects\ChartObjectsTxtControls.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| SeparateLine.mqh | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Element.mqh" class="macro">#include "Window.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建分隔线的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSeparateLine : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 指向元件附加表单的指针 CWindow *m_wnd; class=class="str">"cmt">//--- 用于创建分隔线的对象 CRectCanvas m_canvas; class=class="str">"cmt">//--- class="kw">public: CSeparateLine(class="type">void); ~CSeparateLine(class="type">void); class=class="str">"cmt">//--- 保存传来的指针 class="type">void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } class=class="str">"cmt">//--- class="kw">public: 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 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=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CSeparateLine::CSeparateLine(class="type">void) {
分隔线类的构造与绘制接口
在 MT5 自定义面板里画一条视觉分隔线,核心是从 CElement 派生出 CSeparateLine。基类构造函数里用 CElement::ClassName(CLASS_NAME) 把元件类名登记进去,析构函数留空即可,资源管理交给父级。 分隔线支持两种走向,枚举 ENUM_TYPE_SEP_LINE 里写死:H_SEP_LINE=0 是横向,V_SEP_LINE=1 是纵向。类内私有成员 m_type_sep_line 记类型,m_dark_color 和 m_light_color 分别存暗、亮两色,默认在构造函数初始化列表里给成 clrBlack 与 clrDimGray。 对外只暴露三个 setter:TypeSepLine() 改走向,DarkColor() / LightColor() 改双色。真正落屏靠 CreateSeparateLine(),它吃 chart_id、子窗口号、索引、x/y 坐标和 x_size/y_size 尺寸;内部再调私有的 CreateSepLine() 建画布、DrawSeparateLine() 执行绘制。 开 MT5 把这段抄进 EA 的界面模块,改 m_dark_color 为 clrDarkOrange 就能立刻看出面板分区边界,外汇与贵金属图表叠加自绘控件时需注意重绘开销偏高的高风险。
class=class="str">"cmt">//--- 在基类中保存元件类的名称 CElement::ClassName(CLASS_NAME); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 析构函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CSeparateLine::~CSeparateLine(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 分隔线类型的枚举 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_TYPE_SEP_LINE { H_SEP_LINE =class="num">0, V_SEP_LINE =class="num">1 }; class CSeparateLine : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 属性 ENUM_TYPE_SEP_LINE m_type_sep_line; class="type">color m_dark_color; class="type">color m_light_color; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- (class="num">1) 线的类型, (class="num">2) 线的颜色 class="type">void TypeSepLine(class="kw">const ENUM_TYPE_SEP_LINE type) { m_type_sep_line=type; } class="type">void DarkColor(class="kw">const class="type">color clr) { m_dark_color=clr; } class="type">void LightColor(class="kw">const class="type">color clr) { m_light_color=clr; } class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CSeparateLine::CSeparateLine(class="type">void) : m_type_sep_line(H_SEP_LINE), m_dark_color(clrBlack), m_light_color(clrDimGray) { } class CSeparateLine : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 创建分隔线 class="type">bool CreateSeparateLine(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int index, class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int x_size,class="kw">const class="type">int y_size); class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 创建用于绘制分隔线的画布 class="type">bool CreateSepLine(class="type">void); class=class="str">"cmt">//--- 绘制分隔线 class="type">void DrawSeparateLine(class="type">void);
「用画布给面板画出凹凸分隔线」
在 MT5 自定义指标面板里,纯色块看久了容易串行。用 CCanvas 的位图标签画一条带高光和阴影的分隔线,视觉上就有内凹或浮起的质感,比直接 Rectangle 描边更耐看。 CreateSepLine 先拼出全局唯一对象名:程序名 + “_separate_line_” + 元素索引 + “__” + 元素 ID,避免同图表多个实例重名冲突。随后 CreateBitmapLabel 用 COLOR_FORMAT_ARGB_NORMALIZE 建标签,Attach 挂到子窗口序号 1,Background(false) 让它浮在背景之上。 XGap / YGap 取的是线坐标减去所属窗口左上角坐标,保证画布贴着你想要的位置。最后调 DrawSeparateLine 并丢进元素数组,函数返回 true 即代表这条线已经活在主图之外的子窗口里。 DrawSeparateLine 里先按画布宽高减 1 拿到 x_size、y_size,Erase 用 clrNONE 全透明清场。水平线模式下,顶部画 m_dark_color 深线、底部画 m_light_color 浅线;垂直线则左边深、右边浅——这一深一浅就是立体感的来源。改 m_dark_color / m_light_color 两个成员,能直接换出金属灰或暗金风格,外汇和贵金属图表高频盯盘时少一分眼疲劳。 线画完必须 m_canvas.Update() 推到屏幕,漏掉这行线就只存在于内存。开 MT5 把这段塞进你的 CElement 派生类,调一下 H_SEP_LINE 和 V_SEP_LINE 两个分支,面板层级立刻清楚一截。
class="type">bool CSeparateLine::CreateSepLine(class="type">void) { class=class="str">"cmt">//--- 构造对象名称 class="type">class="kw">string name=CElement::ProgramName()+"_separate_line_"+((class="type">class="kw">string)CElement::Index())+"__"+((class="type">class="kw">string)CElement::Id()); class=class="str">"cmt">//--- Creating an object if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,m_x,m_y,m_x_size,m_y_size,COLOR_FORMAT_ARGB_NORMALIZE)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- Attaching to the chart if(!m_canvas.Attach(m_chart_id,name,m_subwin,class="num">1)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 属性 m_canvas.Background(class="kw">false); class=class="str">"cmt">//--- 到边缘的距离 m_canvas.XGap(m_x-m_wnd.X()); m_canvas.YGap(m_y-m_wnd.Y()); class=class="str">"cmt">//--- 绘制分隔线 DrawSeparateLine(); class=class="str">"cmt">//--- 加到数组中 CElement::AddToArray(m_canvas); class="kw">return(true); } class="type">void CSeparateLine::DrawSeparateLine(class="type">void) { class=class="str">"cmt">//--- 线的坐标 class="type">int x1=class="num">0,x2=class="num">0,y1=class="num">0,y2=class="num">0; class=class="str">"cmt">//--- 画布大小 class="type">int x_size =m_canvas.X_Size()-class="num">1; class="type">int y_size =m_canvas.Y_Size()-class="num">1; class=class="str">"cmt">//--- 清空画布 m_canvas.Erase(::ColorToARGB(clrNONE,class="num">0)); class=class="str">"cmt">//--- 如果是水平线 if(m_type_sep_line==H_SEP_LINE) { class=class="str">"cmt">//--- 上方的深色线 x1=class="num">0; y1=class="num">0; x2=x_size; y2=class="num">0; class=class="str">"cmt">//--- m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color)); class=class="str">"cmt">//--- 下方的浅色线 x1=class="num">0; x2=x_size; y1=y_size; y2=y_size; class=class="str">"cmt">//--- m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color)); } class=class="str">"cmt">//--- 如果是垂直线 else { class=class="str">"cmt">//--- 左边的深色线 x1=class="num">0; x2=class="num">0; y1=class="num">0; y2=y_size; class=class="str">"cmt">//--- m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color)); class=class="str">"cmt">//--- 右边的浅色线 x1=x_size; y1=class="num">0; x2=x_size; y2=y_size; class=class="str">"cmt">//--- m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color)); } class=class="str">"cmt">//--- 刷新画布 m_canvas.Update(); }