MQL 图形界面标记工具:从标准库改造到元素缓存的基础铺垫(基础篇)
(1/3)· 想用 MQL 搭出可扩展 GUI 却总被标准库限制?本篇先拆开控件库改了什么
不少交易者试着用 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 自写控件验证。
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 传进来——这意味着你之后写事件路由可以完全绕开硬编码遍历。
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 的面板越复杂,这种反查越容易因生命周期顺序踩坑,属于高风险调试点。
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;