MQL 作为 MQL 程序图形界面的标记工具。 第二部分·综合运用
(3/3)· 从标准库分支到 DynamicForm 动态布局,补齐 MQL 程序 GUI 标记的最后一块拼图
「用缓存做可增删的弹性界面」
在 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 注入逻辑的核心代码。
{
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 工具涉及实时报价交互,回测无法覆盖全部点按路径,实盘前请在模拟盘充分点击验证。
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 策略界面的易用不等于胜率提升,实盘前请在策略测试器跑足样本。