GUI:利用 MQL 创建您自己的图形库的提示和技巧(基础篇)
在 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 单独跑,防止重载改变执行顺序导致坐标错乱。
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() 即可。
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,能直接看到面板密度变化。
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 个空方块。
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); }