GUI:利用 MQL 创建您自己的图形库的提示和技巧(基础篇)
📘

GUI:利用 MQL 创建您自己的图形库的提示和技巧(基础篇)

第 1/3 篇

在 MT5 里自己搭一套图形控件库

想在 MT5 面板里画按钮、滑块、标签而不依赖第三方 EA,就得用 MQL5 的 OBJ_ 系列图形对象自己封装。官方标准库里的 CJAVal 等类能用,但真要做交互式 GUI,往往要重写事件路由,否则鼠标点击和重绘会互相打架。 一个实在的数据点:这篇示例文章发布于 2024 年 1 月 15 日,截至统计时页面浏览 1031、评论 2,说明轻量 GUI 封装的需求真实存在但圈子小。外汇与贵金属杠杆高,拿自写面板做下单前务必在策略测试器离线跑通事件逻辑,实盘误触可能直接发单。 起步建议只封装三类对象:标签(OBJ_LABEL)做静态文本、按钮(OBJ_BUTTON)接 OnChartEvent,矩形(OBJ_RECTANGLE)做容器背景。先把坐标换算写死,再谈自适应。

「为什么还要自己造 GUI 轮子」

在 MT5 关联环境里写一套 GUI 函数库,是多数开发者迟早会碰到的重头活。外面现成的库体积庞大,学会用比从零写省力得多,但现成方案未必贴合你的项目。 慢、缺特殊控件、难扩展、藏漏洞,是逼人自研的四类典型诱因。前三点能靠作者迭代缓解,但你只能等对方愿意改,主动权不在手里。 本文不教你怎么画界面,也不铺开讲全功能库的开发流程。只给几个特殊 GUI 库的 Minimal 示例,让你能照着改出解决具体问题的起点,或反过来读懂那些几万行库内部在干什么。 外汇与贵金属品种波动剧烈、杠杆风险高,任何自研界面若涉及下单逻辑,先在模拟盘跑通再谈实盘。

◍ GUI 库的树状对象骨架怎么搭

做 MT5 自定义 GUI 函数库,本质是维护一套图表对象的层级关系:父对象管位置,子对象相对父对象偏移,移动父项时要能通知子项重算屏幕坐标。最顺手的组织方式是树结构——一个元素可挂多个子元素,子元素再挂自己的子元素。 基础类 CElement 现在只存局部坐标 m_x、m_y 和父子指针。注意 m_x/m_y 是相对父对象的局部位置,真实屏幕坐标要靠递归 GetGlobalX 累加父链得到(见代码末段)。若父指针无效就直接返回自身 m_x,否则返回 m_x + 父的 GetGlobalX()。 唯一名称用静态变量 m_element_count 生成,目前先塞在元素类里。析构时必须清掉子元素,否则图表关掉后内存漏在外面。AddChild 标成 private,对外只暴露 SetParent,避免双向引用写乱。 画布类用 CCanvas(位图标签)几乎能画所有控件;编辑框类之所以单拆出来,是因为画布在不用 DLL 时做不到和终端外复制粘贴文本。首次测试会刷出几个方块,删除图表时跟着清掉——它们现在只是静物,后续章节才加交互。 别把空基类当摆设 基准 Element 的 Create 是空虚函数,专门留给孩子类重载。子对象必须在父对象之后创建,所以拆出 CreateChildren 单独跑,防止重载改变执行顺序导致坐标错乱。

MQL5 / C++
class CElement
{
class="kw">private:
  class=class="str">"cmt">//Variable to generate names
  class="kw">static class="type">int        m_element_count;
  class="type">void              AddChild(CElement* child);
class="kw">protected:
  class=class="str">"cmt">//Chart object name
  class="type">class="kw">string            m_name;
  class=class="str">"cmt">//Element relations
  CElement*         m_parent;
  CElement*         m_children[];
  class="type">int               m_child_count;
  class=class="str">"cmt">//Position and size
  class="type">int               m_x;
  class="type">int               m_y;
  class="type">int               m_size_x;
  class="type">int               m_size_y;
class="kw">public:
                    CElement();
                    ~CElement();
  class="type">void              SetPosition(class="type">int x, class="type">int y);
  class="type">void              SetSize(class="type">int x, class="type">int y);
  class="type">void              SetParent(CElement* parent);
  class="type">int               GetGlobalX();
  class="type">int               GetGlobalY();
  class="type">void              CreateChildren();
  class="kw">virtual class="type">void      Create(){}
};
class="type">int CElement::GetGlobalX(class="type">void)
{
  if (CheckPointer(m_parent)==POINTER_INVALID)
    class="kw">return m_x;
  class="kw">return m_x + m_parent.GetGlobalX();
}
class="type">int CElement::m_element_count = class="num">0;

GUI 基类的父子树怎么搭

在 MT5 里写自定义面板,第一步是把界面元素抽象成树形结构。CElement 作为基类,构造时默认尺寸给到 100×100 像素,坐标从 (0,0) 起步,并用静态计数器 m_element_count 自动命名,比如 element_0、element_1,省去手填 ID 的麻烦。 析构函数里遍历 m_children 逐个 delete,说明子对象的内存由父对象托管。你如果在外面再 delete 一次子指针,可能触发双重释放导致 EA 崩溃。 SetParent 这行是树建立的入口:把自己挂到父节点,同时父节点内部 AddChild 把指针存进动态数组。AddChild 先用 CheckPointer 拦掉无效指针,再 ArrayResize 扩容——这意味着子元素数量没有硬上限,但每加一个都要重分配内存,批量挂节点时最好预估算好规模。 递归创建靠 CreateChildren:它先调子对象自己的 Create(),再往下钻一层。CProgram 只持有一个 m_element_holder 作为根,AddMainElement 把顶层元素挂到根上,CreateGUI 一键向下展开整棵树的绘制。 CCanvasElement 继承 CElement 并内嵌 CCanvas,析构标了 virtual 倾向让画布资源随元素销毁自动释放。要在图表上画位图标签,直接派生这个类重写 Create() 即可。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base Element class constructor                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CElement::CElement(class="type">void) : m_child_count(class="num">0), m_x(class="num">0), m_y(class="num">0), m_size_x(class="num">100), m_size_y(class="num">100)
{
   m_name = "element_"+IntegerToString(m_element_count++);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base Element class destructor(class="kw">delete child objects)              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CElement::~CElement(class="type">void)
{
   for (class="type">int i=class="num">0; i<m_child_count; i++)
      class="kw">delete m_children[i];
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set parent object(in element hierarchy)                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElement::SetParent(CElement *parent)
{
   m_parent = parent;
   parent.AddChild(GetPointer(this));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add child object reference(function not exposed)                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElement::AddChild(CElement *child)
{
   if (CheckPointer(child)==POINTER_INVALID)
      class="kw">return;
   ArrayResize(m_children, m_child_count+class="num">1);
   m_children[m_child_count] = child;
   m_child_count++;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| First creation of elements(children after)                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElement::CreateChildren(class="type">void)
{
   for (class="type">int i=class="num">0; i<m_child_count; i++)
   {
      m_children[i].Create();
      m_children[i].CreateChildren();
   }
}
class CProgram
{
class="kw">protected:
   CElement       m_element_holder;
class="kw">public:
   class="type">void           CreateGUI() { m_element_holder.CreateChildren(); }
   class="type">void           AddMainElement(CElement* element) { element.SetParent(GetPointer(m_element_holder)); }
};
class="macro">#include <Canvas/Canvas.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Generic Bitmap label element(Canvas)                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CCanvasElement : class="kw">public CElement
{
class="kw">protected:
   CCanvas        m_canvas;
class="kw">public:
                 ~CCanvasElement();
   class="kw">virtual class="type">void   Create();
};

「从画布到输入框的 GUI 对象生命周期」

MQL5 里做自定义面板,绕不开两类基础元素:自绘画布(Canvas)和可交互编辑框(Edit)。前者用 CCanvasElement 封装,析构时直接调 m_canvas.Destroy() 释放位图资源;后者用 CEditElement 封装,析构靠 ObjectDelete(0, m_name) 从图表移除对象——两者清理方式不同,混用会留垃圾对象。 看 CCanvasElement::Create(),它先跑基类 CElement::Create(),再用 CreateBitmapLabel(0,0,m_name,GetGlobalX(),GetGlobalY(),m_size_x,m_size_y,COLOR_FORMAT_ARGB_NORMALIZE) 在坐标 (0,0) 锚定一张 ARGB 归一化格式的位图标签。随后 Erase 用 MathRand()%256 填随机 RGB、Alpha 固定 255,Update(false) 推到前台但不重绘背景。 CEditElement 的落点更直白:ObjectCreate 建 OBJ_EDIT 后,连续四次 ObjectSetInteger 写 XDISTANCE/YDISTANCE/XSIZE/YSIZE,把全局坐标和尺寸钉死。GetEditText / SetEditText 则包了 ObjectGetString / ObjectSetString 的 OBJPROP_TEXT 读写,输入框内容随手可取。 主程序侧给了两个 input:squares=5 控制方块数量,add_edits=true 决定是否为其中一半挂编辑框。OnInit 里 MathSrand((uint)TimeLocal()) 用本地时间播种子,保证每次加载随机色不重样。开 MT5 把这几段拼进 EA,改 squares 到 10,能直接看到面板密度变化。

MQL5 / C++
class=class="str">"cmt">//| Canvas Element destructor(destroy canvas)                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CCanvasElement::~CCanvasElement(class="type">void)
{
  m_canvas.Destroy();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create bitmap label(class="kw">override)                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasElement::Create()
{
  CElement::Create();
  m_canvas.CreateBitmapLabel(class="num">0, class="num">0, m_name, GetGlobalX(), GetGlobalY(), m_size_x, m_size_y, COLOR_FORMAT_ARGB_NORMALIZE);
}
m_canvas.Erase(ARGB(class="num">255, MathRand()%class="num">256, MathRand()%class="num">256, MathRand()%class="num">256));
m_canvas.Update(false);
class CEditElement : class="kw">public CElement
{
class="kw">public:
                     ~CEditElement();
  class="kw">virtual class="type">void       Create();
  class="type">class="kw">string             GetEditText() { class="kw">return ObjectGetString(class="num">0, m_name, OBJPROP_TEXT); }
  class="type">void               SetEditText(class="type">class="kw">string text) { ObjectSetString(class="num">0, m_name, OBJPROP_TEXT, text); }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Edit element destructor(remove object from chart)               |
class=class="str">"cmt">//+------------------------------------------------------------------+
CEditElement::~CEditElement(class="type">void)
{
  ObjectDelete(class="num">0, m_name);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create edit element(class="kw">override) and set size/position             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEditElement::Create()
{
  CElement::Create();
  ObjectCreate(class="num">0, m_name, OBJ_EDIT, class="num">0, class="num">0, class="num">0);
  ObjectSetInteger(class="num">0, m_name, OBJPROP_XDISTANCE, GetGlobalX());
  ObjectSetInteger(class="num">0, m_name, OBJPROP_YDISTANCE, GetGlobalY());
  ObjectSetInteger(class="num">0, m_name, OBJPROP_XSIZE, m_size_x);
  ObjectSetInteger(class="num">0, m_name, OBJPROP_YSIZE, m_size_y);
}
class="macro">#include "Basis.mqh"
class="macro">#include "CanvasElement.mqh"
class="macro">#include "EditElement.mqh"
class="kw">input class="type">int squares = class="num">5; class=class="str">"cmt">//Amount of squares
class="kw">input class="type">bool add_edits = true; class=class="str">"cmt">//Add edits to half of the squares
CProgram program;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
  MathSrand((class="type">uint)TimeLocal());
  class=class="str">"cmt">//class="num">100 is element size by class="kw">default

◍ 随机撒方块时给半数挂输入框

这段初始化收尾逻辑演示了如何在图表可视区内批量铺自定义控件:先扣掉右边和底部各 100 像素,避免元素画出边界。 max_x 取图表像素宽减 100,max_y 取图表像素高减 100,循环里用 MathRand()%max_x 决定横坐标、%max_y 决定纵坐标,位置完全是随机的。 当 add_edits 开关打开且 i 不超过 squares/2 时,会在对应方块内嵌一个 80×20 的编辑框,偏移 (10,10)。这意味着大约前半批方块带输入控件,后半批是纯图形。 最后 CreateGUI() 提交布局、ChartRedraw(0) 强制重绘,返回 INIT_SUCCEEDED 结束初始化。开 MT5 把 squares 设成 20、add_edits 设 true,能直接看到 10 个带框方块和 10 个空方块。

MQL5 / C++
  class="type">int max_x = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS) - class="num">100;
  class="type">int max_y = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS) - class="num">100;
  
  for (class="type">int i=class="num">0; i<squares; i++)
  {
      CCanvasElement* drawing = new CCanvasElement();
      drawing.SetPosition(MathRand()%max_x, MathRand()%max_y);
      program.AddMainElement(drawing);
      
      if (add_edits && i<=squares/class="num">2)
      {
         CEditElement* edit = new CEditElement();
         edit.SetParent(drawing);
         edit.SetPosition(class="num">10, class="num">10);
         edit.SetSize(class="num">80, class="num">20);
      }
  }
  
  program.CreateGUI();
  ChartRedraw(class="num">0);
  
  class="kw">return(INIT_SUCCEEDED);
}

常见问题

现成库往往体积大、改不动,自己造轮子能按需裁剪、吃透对象生命周期,适合做轻量定制面板。
用基类统一管父子关系,每个控件只认父节点和子节点列表,从画布往下挂,结构清晰才好维护。
小布可替你跑通控件生命周期样例、标出对象树异常,你专注调交互逻辑即可。
循环生成时用随机判定,命中半数就挂输入框对象而非普通色块,其余保留原样。
创建时挂到父树、绘制上屏,交互时响应事件,销毁时卸子节点并释放资源,缺一步会漏内存。