MQL 图形界面标记工具:从标准库改造到元素缓存的基础铺垫(基础篇)
🛠️

MQL 图形界面标记工具:从标准库改造到元素缓存的基础铺垫(基础篇)

(1/3)· 想用 MQL 搭出可扩展 GUI 却总被标准库限制?本篇先拆开控件库改了什么

进阶 第 1/3 篇

不少交易者试着用 MQL 写自定义面板,直接套用标准库后很快发现控件无法继承扩展、多窗口同名互相串扰。这类问题不是用法不对,而是底层类封装太死。先理清库到底动了哪些刀,后面写界面才不会再踩坑。

◍ 从基础布局到复杂界面示例

在前面研究过用 MQL 描述程序图形界面布局的基本原理后,这一节直接上更复杂的例子。为了避免被具体业务干扰,作者选择只围绕标准组件库来搭示例,不展开实际交易逻辑。 标准组件库的价值在于:它已经封装好了界面元素的初始化、层级组合和属性调整,开发者调用即可,不必每次重写底层类。 原文系列发布于 2020 年 7 月 29 日,该篇在站内有 2675 次查看、16 条讨论,说明这类 GUI 标记实战内容在 MQL5 圈子里有稳定需求。

「给标准控件库动刀:ControlsPlus 分支做了什么」

把 MQL5 标准库直接拿来搭 OLAP 风格窗口界面时,会发现原生组件不够用:既要扩功能,又要修 bug。实际做法是把全部类完整复制成一个分支,塞进 ControlsPlus 文件夹,后续只碰这套副本,标准库原版不再改动。 最基础的一刀是把所有类的 private 访问级别改成 protected,否则继承时根本伸不进手。为了方便在调试器里看清真实对象类型,CWnd 里加了字符串成员 _rtti,并在每个派生类构造函数用 RTTI 宏填进类名和地址——调试器里即便通过基类指针解引用,也能看到实际派生类,而不是只显示 CWnd。 CWnd 的区域与对齐信息新增了两个重载访问方法,可分别改对齐标志和边距。原版 CWnd::Align 在双向拉伸时无法保证平移进预设边界,已按预期行为重写。CWndContainer 补了 DeleteAll,删容器时顺带清掉所有子元素;若传入指针是容器,Delete 内部会调它。 CWndClient 在不同位置加了滚动条可见性调节逻辑,避免 resize 后滚动条状态错乱。CAppDialog 分配控件 ID 时纳入窗口 instance_id,否则同名不同窗的控件会互相干扰。CRadioGroup、CCheckGroup、CListView 里 rubber 子类的 Redraw 改成虚函数,并微调了子元素宽度重算。 CDatePicker、CCheckBox、CRadioButton 加了虚方法 OnResize 正确响应尺寸变化;CDatePicker 修了个低优先级 bug——弹窗日历误吞了传给它的鼠标单击。CEdit::OnClick 也不再吃掉鼠标单击。项目还扩展了 rubber 类数量,文件落在 Layouts 文件夹,如 ComboBoxResizable、SpinEditResizable、ListViewResizable、CheckGroupResizable、RadioGroupResizable;按钮和输入字段原生就支持拉伸,无需额外类。 下方代码是分支里对齐与边距访问的典型片段,可直接抄进 MT5 自写控件验证。

MQL5 / C++
class="macro">#define RTTI _rtti = StringFormat("%s %d", class="kw">typename(this), &this);
    ENUM_WND_ALIGN_FLAGS Alignment(class="type">void) class="kw">const
    {
      class="kw">return (ENUM_WND_ALIGN_FLAGS)m_align_flags;
    }
    CRect Margins(class="type">void) class="kw">const
    {
      CRectCreator rect(m_align_left, m_align_top, m_align_right, m_align_bottom);
      class="kw">return rect;
    }
    class="type">void Alignment(class="kw">const class="type">int flags)
    {
      m_align_flags = flags;
    }
    class="type">void Margins(class="kw">const class="type">int left, class="kw">const class="type">int top, class="kw">const class="type">int right, class="kw">const class="type">int bottom)
    {
      m_align_left = left;
      m_align_top = top;
      m_align_right = right;
      m_align_bottom = bottom;
    }

用静态缓存接管 GUI 元素的生成

在 MT5 的 GUI 布局里,对象窗口里的元素原本靠实例构造时自动生成,本质是先放一批“空壳”,等 Create 之类的方法再来填实。若让布局系统独立造元素、不依赖窗口抓取,就需要一个共用的存储单元,这里用基类指针数组 LayoutCache 来承接,所有元素都能通过 save 方法塞进去。 MQL5 程序是单线程的,多个窗口不会同时抢同一个 cacher 指针,所以把缓存做成 LayoutBase 的静态成员就够用。每个窗口在 CreateLayout 开头用 setCache 把自己的缓存实例挂上,析构时若布局栈已空就把指针清掉,避免遗留。 重置链接不等于清缓存,只是防止下一个布局误把别的窗口控件混进来。填充缓存时,LayoutBase 里新增了不带第三方指针的 init 模板方法,默认一次建 1 个对象,也可传参 m 批量建。下面这段代码是核心骨架,逐行拆完你就能在 MT5 里照抄验证。 缓存带来的直接好处是窗口类描述大幅缩减。StdLayoutCache 里 get 方法靠正索引号或负号前缀的标识符找控件,标识符就是标准库分派事件用的唯一编号,事件里通过 lparam 传进来——这意味着你之后写事件路由可以完全绕开硬编码遍历。

MQL5 / C++
class="kw">template<class="kw">typename C>
class LayoutCache
{
class="kw">protected:
C *cache[];   class=class="str">"cmt">// autocreated controls and boxes
class="kw">public:
class="kw">virtual class="type">void save(C *control)
{
class="kw">const class="type">int n = ArraySize(cache);
ArrayResize(cache, n + class="num">1);
cache[n] = control;
}
class="kw">virtual C *get(class="kw">const class="type">long m)
{
if(m < class="num">0 || m >= ArraySize(cache)) class="kw">return NULL;
class="kw">return cache[(class="type">int)m];
}
class="kw">virtual C *get(class="kw">const class="type">class="kw">string name) = class="num">0;
class="kw">virtual class="type">bool find(C *control);
class="kw">virtual class="type">int indexOf(C *control);
class="kw">virtual C *findParent(C *control) = class="num">0;
class="kw">virtual class="type">bool revoke(C *control) = class="num">0;
class="kw">virtual class="type">int cacheSize();
};
class="kw">template<class="kw">typename P,class="kw">typename C>
class LayoutBase: class="kw">public LayoutData
{
class="kw">protected:
...
class="kw">static LayoutCache<C> *cacher;
class="kw">public:
class="kw">static class="type">void setCache(LayoutCache<C> *c)
{
cacher = c;
}
~LayoutBase()
{
...
if(stack.size() == class="num">0)
{
cacher = NULL;
}
}
class=class="str">"cmt">// nonbound layout, control T is implicitly stored in internal cache
class="kw">template<class="kw">typename T>
T *init(class="kw">const class="type">class="kw">string name, class="kw">const class="type">int m = class="num">1, class="kw">const class="type">int x1 = class="num">0, class="kw">const class="type">int y1 = class="num">0, class="kw">const class="type">int x2 = class="num">0, class="kw">const class="type">int y2 = class="num">0)
{
T *temp = NULL;
for(class="type">int i = class="num">0; i < m; i++)
{
temp = new T();
if(save(temp))
{
init(temp, name + (m > class="num">1 ? (class="type">class="kw">string)(i + class="num">1) : ""), x1, y1, x2, y2);
}
else class="kw">return NULL;
}
class="kw">return temp;
}
class="kw">virtual class="type">bool save(C *control)
{
if(cacher != NULL)
{
cacher.save(control);
class="kw">return true;
}
class="kw">return class="kw">false;
}
class=class="str">"cmt">// CWnd implementation specific!
class StdLayoutCache: class="kw">public LayoutCache<CWnd>
{
class="kw">public:
...
class="kw">virtual CWnd *get(class="kw">const class="type">long m) class="kw">override
{
if(m < class="num">0)
{
for(class="type">int i = class="num">0; i < ArraySize(cache); i++)
{
if(cache[i].Id() == -m) class="kw">return cache[i];
CWndContainer *container = class="kw">dynamic_cast<CWndContainer *>(cache[i]);
if(container != NULL)
{
for(class="type">int j = class="num">0; j < container.ControlsTotal(); j++)
{
if(container.Control(j).Id() == -m) class="kw">return container.Control(j);
}
}
}
class="kw">return NULL;
}
else if(m >= ArraySize(cache)) class="kw">return NULL;
class="kw">return cache[(class="type">int)m];
}
class="kw">virtual CWnd *findParent(CWnd *control) class="kw">override
{

◍ 用动态转型从缓存里反查容器指针

这段逻辑跑在 GUI 框架的辅助函数里,目的是在一层缓存数组 cache 中找出某个控件 control 所属的 CWndContainer。cache 里存的是基类指针,直接比地址没用,必须先 dynamic_cast 到 CWndContainer* 再判空。 外层循环遍历 cache,长度由 ArraySize(cache) 决定;只要转型成功且非空,就进内层循环扫该容器的 ControlsTotal() 个子控件。内层用 container.Control(j) 逐个比对指针,命中即返回容器,全扫完没命中则返回 NULL。 在 MT5 里验证时,可以把这段塞进你自己的 CAppDialog 派生类,打印返回的 container 地址:若频繁返回 NULL,说明控件没挂到任何已缓存容器,大概率是在 Create() 之前就调了查找。外汇与贵金属 EA 的面板越复杂,这种反查越容易因生命周期顺序踩坑,属于高风险调试点。

MQL5 / C++
for(class="type">int i = class="num">0; i < ArraySize(cache); i++)
{
CWndContainer *container = class="kw">dynamic_cast<CWndContainer *>(cache[i]);
if(container != NULL)
{
for(class="type">int j = class="num">0; j < container.ControlsTotal(); j++)
{
if(container.Control(j) == control)
{
class="kw">return container;
}
}
}
}
class="kw">return NULL;
把界面调试交给小布盯盘
这些控件库改动点和 RTTI 调试技巧,小布盯盘的 AIGC 已整理成品种页的速查卡片,打开对应 MQL 开发笔记即可对照,不用自己翻类定义。

常见问题

原版很多成员是 private 级别,派生类无法直接重写或读取,本篇将私密访问改为 protected 才打通扩展路径,这属于进阶改造。
标准 CAppDialog 分配标识符时没带 instance_id,同名牌会冲突;改造后按窗口实例隔离,才不会串控。
可以,小布盯盘的知识库卡片按类梳理了 ControlsPlus 的改动,省去你逐文件比对标准库的时间。
删除容器时若手动清子元素容易漏,DeleteAll 会递归清理,避免界面对象残留导致内存或绘制异常。