MQL5 图形界面库开发:从分隔线到上下文菜单的底层封装(基础篇)
🧩

MQL5 图形界面库开发:从分隔线到上下文菜单的底层封装(基础篇)

(1/3)·想把 MT5 界面做得像终端原生却总被基础控件卡住?这一章补齐分隔线与菜单类的地基

含代码示例实战向 第 1/3 篇

很多交易者试着给 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 像素,上下两线间就能留出间隙。

MQL5 / C++
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 省心。 外汇与贵金属图表上挂这类自定义元件,本质不改价格风险,只是视觉辅助;上线前建议在策略测试器里跑一遍非交易图表验证渲染不闪退。

MQL5 / C++
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 就能立刻看出面板分区边界,外汇与贵金属图表叠加自绘控件时需注意重绘开销偏高的高风险。

MQL5 / C++
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 两个分支,面板层级立刻清楚一截。

MQL5 / C++
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();
  }
把界面原型交给小布盯盘比对
这些控件结构和编译避坑点,小布盯盘的 AIGC 已内置常见 MT5 界面范式,打开对应品种页就能对照自己的面板找差异,把重复试错交给小布,你专注决策逻辑。

常见问题

用画布绘制能在一个对象上完成深浅双线制造立体沟槽效果,避免多个基础对象叠加带来的坐标与层级管理麻烦,也方便嵌入其他控件作为子元件。
因 CCanvas 与 CChartObject 都有同名字段,需对 CCanvas 源码做少量改动消除变量遮蔽,编译警告消失后再用于界面库,否则后续扩展类会连带报错。
目前小布盯盘内置的是范式比对与诊断,不代写完整库代码;你可把按本文写的类贴入对照,快速定位与标准结构的偏离点,外汇贵金属界面开发仍属高风险定制。
需要扩充保存全部图形界面元件指针的库,否则菜单弹出时无法统一调度子项与分隔线,本文最后小节即为此做铺垫,完整应用在系列后两篇。