MQL 作为 MQL 程序图形界面的标记工具。 第一部分·进阶篇
「用栈结构接管布局对象的父子归属」
在 MQL5 里手写 UI 布局时,最容易被忽略的是对象销毁时的归属回退。上面的 LayoutBase 析构逻辑里,只要 container 存在就先 stack.pop(),把当前节点从全局栈顶摘掉;随后若自身还挂着 object,就从栈里取新的栈顶作为父节点,把 object 交回去。 这段写法保证了嵌套布局在层层析构时不会悬空:当中间层被析构,它持有的子对象会自动挂到上一层,而不是被直接丢掉。你在 MT5 里跑自定义面板时,若发现关掉某个分组后子控件消失,八成是没做这步 pop 加回退。 实际验证很简单——建一个三层嵌套的 CPanel 类似结构,给每层加不同背景色,逐层 delete 中间层,看子层是否还在上层容器内渲染。外汇与贵金属图表上挂这类 EA 面板属高风险操作,建议先在策略测试器里离线跑。
stack << &this; } } ~LayoutBase() { if(container) { stack.pop(); } if(object) { LayoutBase *up = stack.size() > class="num">0 ? stack.top() : NULL; if(up != NULL) { up.add(object); } } } };
◍ 用模板类接管标准库控件的布局登记
在 MT5 标准库里做 GUI,控件坐标和对齐方式经常打架。以 CWndContainer 和 CWnd 为模板参数派生出的 StdLayoutBase,就是把这种混乱收口的中间层:它用 dynamic_cast 判断一个 CWnd 到底是从 CDialog 还是 CBox 派生,是容器就记下引用,不是就跳过。 setContainer 只认这两种容器,意味着子窗口逻辑默认不在考虑范围;若你要在实盘面板里嵌套多层级浮窗,得自己改这段代码。create 方法强制在 ChartID() 和主窗口里建元素,返回 Name 供后续绑定——这决定了你的布局系统只能贴当前图表,换图表要重跑。 add 方法有个坑:标准库的 Add 不是虚函数,所以这里先尝试把子元素丢进 CDialog,失败再丢进 CWndContainer。若两者都不是,直接 Print 报错而非崩溃,方便你定位哪一层挂了。 _layout 类以单下划线开头,提醒调用者它是非标准用途。构造函数直接调 init,支持引用、指针、甚至对象数组三种传入形态;例如 _layout<CButton> button(m_button, 100, 20) 就能在主对话框里初创一个 100×20 的按钮并自动登记。 对齐优先于坐标这条规则很硬:水平对齐的控件会在父容器里拉伸到整宽减去左右预留区,CBox 里还要看默认水平还是垂直流向。所以写布局时别死磕 x1/y1,先想清楚父容器取向。 属性设置别只靠 edit.Text("text") 这种本地方法,布局系统虽兼容但非最优。建议把详细样式委托给协助类,让 _layout 只管注册,这样换控件库时改动面最小。外汇与贵金属 GUI 自动化伴随平台宕机与滑点高风险,任何面板都先在模拟盘验证。
class StdLayoutBase: class="kw">public LayoutBase<CWndContainer,CWnd> { class="kw">public: class="kw">virtual class="type">bool setContainer(CWnd *control) class="kw">override { CDialog *dialog = class="kw">dynamic_cast<CDialog *>(control); CBox *box = class="kw">dynamic_cast<CBox *>(control); if(dialog != NULL) { container = dialog; } else if(box != NULL) { container = box; } class="kw">return true; } class="kw">virtual class="type">class="kw">string create(CWnd *child, class="kw">const class="type">class="kw">string id = NULL) class="kw">override { child.Create(ChartID(), id != NULL ? id : _id, class="num">0, _x1, _y1, _x2, _y2); class="kw">return child.Name(); } class="kw">virtual class="type">void add(CWnd *child) class="kw">override { CDialog *dlg = class="kw">dynamic_cast<CDialog *>(container); if(dlg != NULL) { dlg.Add(child); } else { CWndContainer *ptr = class="kw">dynamic_cast<CWndContainer *>(container); if(ptr != NULL) { ptr.Add(child); } else { Print("Can&class="macro">#x27;t add ", child.Name(), " to ", container.Name()); } } } }; class="kw">template<class="kw">typename T> class _layout: class="kw">public StdLayoutBase { class="kw">public: _layout(T &ref, class="kw">const class="type">class="kw">string id, class="kw">const class="type">int dx, class="kw">const class="type">int dy) { init(&ref, id, class="num">0, class="num">0, dx, dy); } _layout(T *ptr, class="kw">const class="type">class="kw">string id, class="kw">const class="type">int dx, class="kw">const class="type">int dy) { init(ptr, id, class="num">0, class="num">0, dx, dy); } _layout(T &ref, class="kw">const class="type">class="kw">string id, class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2) { init(&ref, id, x1, y1, x2, y2); } _layout(T *ptr, class="kw">const class="type">class="kw">string id, class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2) { init(ptr, id, x1, y1, x2, y2); } _layout(T &refs[], class="kw">const class="type">class="kw">string id, class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2) { init(refs, id, x1, y1, x2, y2); } };
用运算符重载给控件属性减负
给 GUI 元素配属性时,传统 builder 链式调用要在布局类里堆一大组方法,且逆引用点号不能重载,导致项目很快变臃肿。MQL5 里更顺手的写法是重载 <= 运算符,把样式、颜色、边距一口气串起来:column <= LAYOUT_STYLE_VERTICAL <= clrGray <= 5.0,比嵌套赋值 ((column = LAYOUT_STYLE_VERTICAL) = clrGray) = 5 清晰太多。
LayoutBase 里先放一个模板存根 operator<=,双重作用是声明重载、提醒派生类去重写;中介类 ControlProperties 在构造或 assign 时绑死目标对象,_layout 的模板运算符便能从接口自动推导参数类型。
相同数据类型常对应不同属性,比如 CWnd 的布尔值既可表可见性也可表活动态,CEdit 的布尔是只读、CButton 的布尔是锁定。为此接口加了字符串参数的 [] 运算符设上下文:不写上下文时 CButton 默认走锁定,写 ["visible"] 就初设为不可见。
边距处理上,标准库对齐项会自动加外边距,而 CBox 自己还有 padding 表示容器边界与子控件的缝。两者定位算法不互通,同时用边距和缩进可能让元素偏移超预期;实操建议缩进保持 0,只用边距,特殊容器再单独试缩进。
下表是 POC 验证过的属性—元素映射节选:bool 类 CWnd/CWndObj 全支持 visible/enable;int 类覆盖 width/height/margin 四向/align/fontsize;PackedRect 专用于四向 margin;ENUM_WND_ALIGN_FLAGS 管对齐。完整代码附在文末,示例在 MQL5/Experts/Examples/Layouts/ 可跑 SlidingPuzzle4 比对。
_layout<CBox> column(...); column.style(LAYOUT_STYLE_VERTICAL).class="type">class="kw">color(clrGray).margin(class="num">5); _layout<CBox> column(...); column.properties().style(LAYOUT_STYLE_VERTICAL).class="type">class="kw">color(clrGray).margin(class="num">5); _layout<CBox> column(...); column = LAYOUT_STYLE_VERTICAL = clrGray = class="num">5; class=class="str">"cmt">// &class="macro">#x27;clrGray&class="macro">#x27; - l-value required ... ((column = LAYOUT_STYLE_VERTICAL) = clrGray) = class="num">5; column = LAYOUT_STYLE_VERTICAL; class=class="str">"cmt">// orientation column = clrGray; class=class="str">"cmt">// class="type">class="kw">color column = class="num">5; class=class="str">"cmt">// margin column <= LAYOUT_STYLE_VERTICAL <= clrGray <= class="num">5.0; class="kw">template<class="kw">typename V> LayoutBase<P,C> *class="kw">operator<=(class="kw">const V value) class=class="str">"cmt">// class="kw">template function cannot be class="kw">virtual { Print("Please, class="kw">override " , __FUNCSIG__, " in your concrete Layout class"); class="kw">return &this; } class="kw">template<class="kw">typename T> class ControlProperties { class="kw">protected: T *object;
「正文」
<span class="keyword">string</span> context; <span class="keyword">public</span>: ControlProperties(): object(<span class="macro">NULL</span>), context(<span class="macro">NULL</span>) {} ControlProperties(T *ptr): object(ptr), context(<span class="macro">NULL</span>) {} <span class="keyword">void</span> assign(T *ptr) { object = ptr; } T *get(<span class="keyword">void</span>) { <span class="keyword">return</span> object; } <span class="keyword">virtual</span> ControlProperties<T> *<span class="keyword">operator</span>[](<span class="keyword">const</span> <span class="keyword">string</span> property) { context = property; <span class="functions">StringToLower</span>(context); <span class="keyword">return</span> &<span class="keyword">this</span>; }; <span class="keyword">virtual</span> T *<span class="keyword">operator</span><=(<span class="keyword">const</span> <span class="keyword">bool</span> b) = 0; <span class="keyword">virtual</span> T *<span class="keyword">operator</span><=(<span class="keyword">const</span> ENUM_ALIGN_MODE align) = 0; <span class="keyword">virtual</span> T *<span class="keyword">operator</span><=(<span class="keyword">const</span> <span class="keyword">color</span> c) = 0; <span class="keyword">virtual</span> T *<span class="keyword">operator</span><=(<span class="keyword">const</span> <span class="keyword">string</span> s) = 0; <span class="keyword">virtual</span> T *<span class="keyword">operator</span><=(<span class="keyw
◍ 用重载运算符接管控件属性赋值
在 MQL5 的 GUI 封装里,把 <= 运算符重载成属性写入入口,能省掉一堆 setter 调用。上面这段虚函数覆盖后,遇到上下文带 "font" 就走字体设置,否则默认写文本,返回原对象以支持链式调用。
实际挂控件时,同一运算符被复用了三种语义:传入 PackedRect 是设边距矩形(左、上、右、下各 5/100/5/100 像素),传入 double 5.0 是整体边距值,传入 ["margin"] 下标再加 5 则是单独改 margin 字段。
这种写法在 MT5 里可直接编译验证:建一个 CWndObj 派生按钮,依次跑这三行,用调试器看 ctrl 的边距与文本变化。外汇与贵金属 EA 做面板时高频用到,注意 GUI 逻辑错误不会爆仓但会误触单,属高风险辅助层。
class="kw">virtual T *class="kw">operator<=(class="kw">const class="type">class="kw">string s) class="kw">override { CWndObj *ctrl = class="kw">dynamic_cast<CWndObj *>(object); if(ctrl != NULL) { if(StringFind(context, "font") > -class="num">1) { ctrl.Font(s); } else class=class="str">"cmt">// class="kw">default { ctrl.Text(s); } } class="kw">return object; } button <= PackedRect(class="num">5, class="num">100, class="num">5, class="num">100); class=class="str">"cmt">// left, top, right, bottom button <= class="num">5.0; button["margin"] <= class="num">5;