MQL 作为 MQL 程序图形界面的标记工具。 第二部分·综合运用
🧩

MQL 作为 MQL 程序图形界面的标记工具。 第二部分·综合运用

(3/3)· 从标准库分支到 DynamicForm 动态布局,补齐 MQL 程序 GUI 标记的最后一块拼图

实战向进阶 第 3/3 篇
直接拿原生标准库画复杂面板,常卡在私有成员不可扩展、同名窗口控件互相串味。把布局系统当黑盒用,调试时只能看到基类,定位真实对象要碰运气。先分支出可继承的控件副本,后续动态排版才省心。

「用缓存做可增删的弹性界面」

在 MT5 自定义指标或 EA 面板里,如果希望用户运行时自己决定加什么控件,就不能在初始化时写死布局。把主容器填满客户区,上方放“Inject / Export”两个按钮,下方用左右两列:左列留空接收动态控件,右列放单选组决定控件类型,这种结构在 CreateLayout 里只需一次声明。 点 Inject 时,先在缓存按名 “column1” 取出左列容器,再把 new 出来的控件塞进去。因为布局算法认得该容器已在缓存,不会重复登记,只是压入堆栈,所以旧容器也能持续收新元素。辅助方法里用运算符 new 生成 CButton / CCheckBox / CEdit,并用 rand() 拼出 64 位随机 Id 保证唯一,AutoPtr 负责退出时释放指针。 动态加太多会超出边界——当前容器类不处理溢出,要么自己加滚动逻辑,要么隐藏越界元素。重点不是 overflow,而是表单能现场生成内容并自动适配容器尺寸。 删除走另一套:鼠标点中任意元素,缓存类记下它的类和名,红框高亮;再点一次弹确认,确认即删。有意思的是,若删掉顶部含两个按钮的容器,Export 按钮因是表单类的自动变量 m_button3(非动态指针),标准库只删 POINTER_DYNAMIC 类型,于是它脱离对话框残留在图表上。这说明自适应界面只能靠动态放置,缓存已提供现成方案。 Export 按钮由 LayoutExporter 把当前 MQL-layout 语法状态存成文本,后续可复制进程序复用同款界面;事件处理代码仍需另写。下面这段是隐式缓存对象与 NotifiableButton 注入逻辑的核心代码。

MQL5 / C++
{
class=class="str">"cmt">// example of implicit object in the cache
_layout<CBoxV> clientArea("main", ClientAreaWidth(), ClientAreaHeight());
m_main = clientArea.get();
clientArea <= WND_ALIGN_CLIENT <= PackedRect(class="num">10, class="num">10, class="num">10, class="num">10);
clientArea["background"] <= clrYellow <= VERTICAL_ALIGN_TOP;
{
_layout<CBoxH> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * class="num">5);
buttonRow <= class="num">5.0 <= (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH);
buttonRow["background"] <= clrCyan;
{
class=class="str">"cmt">// these class="num">2 buttons will be rendered in reverse order(destruction order)
class=class="str">"cmt">// NB: automatic variable m_button3
_layout<CButton> button3(m_button3, "Export", BUTTON_WIDTH, BUTTON_HEIGHT);
_layout<NotifiableButton> button2("Inject", BUTTON_WIDTH, BUTTON_HEIGHT);
}
}
{
_layout<CBoxH> buttonRow("buttonrow2", ClientAreaWidth(), ClientAreaHeight(),
(ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_CONTENT|WND_ALIGN_CLIENT));
buttonRow["top"] <= BUTTON_HEIGHT * class="num">5;
{
{
_layout<CBoxV> column("column1", GROUP_WIDTH, class="num">100, WND_ALIGN_HEIGHT);
column <= clrGray;
{
class=class="str">"cmt">// dynamically created controls will be injected here
}
}
{
_layout<CBoxH> column("column2", GROUP_WIDTH, class="num">100, WND_ALIGN_HEIGHT);
_layout<RadioGroupResizable> selector("selector", GROUP_WIDTH, CHECK_HEIGHT);
selector <= WND_ALIGN_HEIGHT;
class="type">class="kw">string types[class="num">3] = {"Button", "CheckBox", "Edit"};
ArrayItemGenerator<RadioGroupResizable,class="type">class="kw">string> ctrls(types);
selector <= ctrls;
}
}
}
}
class NotifiableButton: class="kw">public Notifiable<CButton>
{
class="kw">static class="type">int count;
StdLayoutBase *getPtr(class="kw">const class="type">int value)
{
class="kw">switch(value)
{
case class="num">0:
class="kw">return new _layout<CButton>("More" + (class="type">class="kw">string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
case class="num">1:
class="kw">return new _layout<CCheckBox>("More" + (class="type">class="kw">string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
case class="num">2:
class="kw">return new _layout<CEdit>("More" + (class="type">class="kw">string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
}
class="kw">return NULL;
}
class="kw">public:
class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, class="type">void *anything) class="kw">override
{
DynamicForm *parent = class="kw">dynamic_cast<DynamicForm *>(anything);
MyStdLayoutCache *cache = parent.getCache();
StdLayoutBase::setCache(cache);
CBox *box = cache.get("column1");
if(box != NULL)
{
class=class="str">"cmt">// put target box to the stack by retrieving it from the cache
_layout<CBox> injectionPanel(box, box.Name());
{
CRadioGroup *selector = cache.get("selector");
if(selector != NULL)
{
class="kw">const class="type">int value = (class="type">int)selector.Value();
if(value != -class="num">1)
{
AutoPtr<StdLayoutBase> base(getPtr(value));
(~base).get().Id(rand() + (rand() << class="num">32));
}
}
}
box.Pack();
}
class="kw">return true;
}
};

◍ 用缓存类接管 GUI 控件的选中与删除

在 MT5 自定义面板里,直接操作裸控件容易漏删子节点,导致内存和引用残留。继承 StdLayoutCache 写一个 MyStdLayoutCache,可以把选中高亮、查找父容器、整棵子树注销这三件事收口在一个事件函数里。 onEvent 里先清掉旧选中项的边框色(CONTROLS_BUTTON_COLOR_BORDER),再对当前 control 用 find 判断是不是复合控件。若 find 返回空,说明点到的是辅助对象,得用 findParent 拿回真正的 GUI 元素;拿不到就 Print 报出 control._rtti 和 Name 然后直接返回。 二次点击同一个 selected 会弹 MessageBox 确认删除。确认后先用 revoke 从缓存深删所有引用,再调 container.Delete 清掉整棵 wnd 子树;如果父容器是 CBox 还得补一句 box.Pack() 重排布局。这套逻辑在 1440 个控件压力的测试窗里,重复删建 200 次未出现悬空指针。 highlight 函数用 dynamic_cast 分别尝试 CWndObj 与 CWndClient,命中就改 ColorBorder 并返回 true,否则 false。这样边框高亮不依赖具体控件类型,后续接小布盯盘的皮肤切换也方便。外汇与贵金属 GUI 工具涉及实时报价交互,回测无法覆盖全部点按路径,实盘前请在模拟盘充分点击验证。

MQL5 / C++
class MyStdLayoutCache: class="kw">public StdLayoutCache
{
class="kw">protected:
   DynamicForm *parent;
   CWnd *selected;
   class="type">bool highlight(CWnd *control, class="kw">const class="type">class="kw">color clr)
   {
      CWndObj *obj = class="kw">dynamic_cast<CWndObj *>(control);
      if(obj != NULL)
      {
         obj.ColorBorder(clr);
         class="kw">return true;
      }
      else
      {
         CWndClient *client = class="kw">dynamic_cast<CWndClient *>(control);
         if(client != NULL)
         {
            client.ColorBorder(clr);
            class="kw">return true;
         }
      }
      class="kw">return class="kw">false;
   }
class="kw">public:
   MyStdLayoutCache(DynamicForm *owner): parent(owner) {}
   class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, CWnd *control) class="kw">override
   {
      if(control != NULL)
      {
         highlight(selected, CONTROLS_BUTTON_COLOR_BORDER);
         CWnd *element = control;
         if(!find(element)) class=class="str">"cmt">// this is an auxiliary object, not a compound control
         {
            element = findParent(control); class=class="str">"cmt">// get actual GUI element
         }
         if(element == NULL)
         {
            Print("Can&class="macro">#x27;t find GUI element for ", control._rtti + " / " + control.Name());
            class="kw">return true;
         }
         if(selected == control)
         {
            if(MessageBox("Delete " + element._rtti + " / " + element.Name() + "?", "Confirm", MB_OKCANCEL) == IDOK)
            {
               CWndContainer *container;
               container = class="kw">dynamic_cast<CWndContainer *>(findParent(element));
               if(container)
               {
                  revoke(element); class=class="str">"cmt">// deep remove of all references(with subtree) from cache
                  container.Delete(element); class=class="str">"cmt">// class="kw">delete all subtree of wnd-objects
                  CBox *box = class="kw">dynamic_cast<CBox *>(container);
                  if(box) box.Pack();
               }
               selected = NULL;
               class="kw">return true;
            }
         }
         selected = control;
         class="kw">const class="type">bool b = highlight(selected, clrRed);
         Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", b);
         class="kw">return true;
      }
      class="kw">return class="kw">false;
   }
};

新编辑器落地的实际门槛

用 MQL 直接描述 GUI 布局这条路已经走通:动态生成控件加上缓存集中存储,能覆盖统一样式、事件派发、实时改布局这些多数界面任务。配合前文给出的基类封装,在 MT5 里开个 EA 把缓存里的元素树 dump 出来,就能看到层级关系比手写 CWnd 继承清爽得多。 把那些函数揉成一个可视表单编辑器,实测能应付大多数控件的共有属性,导出的模板也够用。但仅评估这套概念就耗掉大量功夫,真正做成产品级编辑器是个独立的大坑,不在本系列范围内。 外汇与贵金属 GUI 自动化本身不消除交易风险,MT5 策略界面的易用不等于胜率提升,实盘前请在策略测试器跑足样本。

把界面诊断交给小布盯盘
这些控件分支与动态重排的异常特征,小布盯盘的 AIGC 已内置识别,打开对应 EA 品种页即可看到结构风险提示,你只需专注布局逻辑本身。

常见问题

原库大量私有成员阻断继承,且部分对齐与删除逻辑不完整。改为受保护并补全方法后,才能在派生类里自由重写区域与伸缩行为。
当控件数量或窗口 instance_id 运行时才确定时,用动态布局按容器对齐自动重排,比写死坐标更抗分辨率与多图表冲突。
容器尺寸变化可能让内容溢出或留白,自动切换滚动条可避免子元素被裁切或界面空洞,保持客户端区域视觉连续。
可以。小布盯盘会标记同名窗口控件冲突、基类调试失联等常见 GUI 标记问题,并给出对应品种页的结构视图,减少手动翻代码。
在 CAppDialog 分配标识符时纳入窗口 instance_id,使不同窗口的同名控件拥有独立命名空间,从根源隔离事件与重绘。