MQL 作为 MQL 程序图形界面的标记工具(第三部)。 窗体设计师(基础篇)
「用 MQL 搭一个可视化窗体编辑器」
在已完成的底层工作里,我们用 MQL 实现了界面元素的类层级初始化、缓存、样式化和事件响应,动态创建的元素可直接改简单对话框布局。单点存储已建元素后,能用一套 MQL 语法把界面描述导出,原样塞进任意需要 GUI 的 EA 或指标。 基于这个基础,作者 Stanislav Korotky 在 2020 年 9 月 9 日发布的系列第 3 篇里,明确把重心放到「窗体设计师」上——也就是做一个能可视化排布控件的图形编辑器。这类工具一旦可用,手写界面坐标和属性的重复劳动会大幅减少。 外汇与贵金属交易的高波动下,把策略参数面板用可视化编辑器快速拼出来,比裸写代码更不容易在紧急调参时出错。打开 MT5 用 MQL5 跑通前 two 篇的基类,是验证本篇编辑器可行性的前提。
◍ 编辑器要管的属性与双窗口结构
做 GUI 编辑器首先得明确:窗口里的元素布置和基本属性调整是必经之路。可支持的通用属性包括类型、名称、宽高、内部内容对齐、文字或标题、背景色、父容器内对齐方式,以及距边界的偏移量。字体名称、大小,还有像“粘贴”按钮那种特定控件的私有属性,在这个偏概念验证的项目里先砍掉,后续有需要再补。 偏移量能间接实现绝对坐标,但别这么干,CBox 容器用自身对齐设置自动排布更稳。编辑器是冲着标准库界面类写的,想嫁接到别的库,得按这套标记系统给所有抽象实体写实现,标准库的标记类就是参照物。 实际用的“标准组件库”名不副实——前文语境下已被大改,挪到了并行分支的 ControlsPlus 文件夹里继续改。支持的元素类型有横/纵容器 CBoxH/CBoxV、CButton、CEdit、CLabel、SpinEditResizable、CDatePicker、ComboBoxResizable、ListViewResizable、CheckGroupResizable、RadioGroupResizable,全都能自适应尺寸(部分标准类原生支持,其余改了不少)。 程序跑起来是两个窗口:Inspector 里选控件属性,Designer 里按设计堆出界面外观。两者都是 AppDialogResizable 的子类,用 CreateLayout 代替 Create 建布局。Inspector 开局就缓存了一堆复杂布局的控件,业务逻辑基本全在这;Designer 初始缓存为空,等用户下令再逐个实例化。 下面这段是 OnInit 里的骨架,两个窗口的创建坐标和尺寸直接写死,可抄去 MT5 头文件里验证。
class="macro">#include "InspectorDialog.mqh" class="macro">#include "DesignerForm.mqh" InspectorDialog inspector; DesignerForm designer; class="type">int OnInit() { if(!inspector.CreateLayout(class="num">0, "Inspector", class="num">0, class="num">20, class="num">20, class="num">200, class="num">400)) class="kw">return (INIT_FAILED); if(!inspector.Run()) class="kw">return (INIT_FAILED); if(!designer.CreateLayout(class="num">0, "Designer", class="num">0, class="num">300, class="num">50, class="num">500, class="num">300)) class="kw">return (INIT_FAILED); if(!designer.Run()) class="kw">return (INIT_FAILED); class="kw">return (INIT_SUCCEEDED); }
用 Value 包装器取代裸类型
界面对象在 MT5 Designer 里由一组属性定义:名称是字符串,宽高与类型是整数,颜色和对齐方式也各有类型。若直接把简单内嵌变量塞进 PropertySet,后续做 UI 联动会卡住——MQL5 不支持指向简单变量的引用,而控件间常需监听同一处数值变化。比如某个字段输入越界,依赖它的控件要自动禁用,若大家都能指向同一个被包装的值,状态同步就省事得多。 于是用模板类 Value 包一层,而不是裸类型。它重载了 '=' 和 '~' 运算符,赋值和取值都能被拦截,这是简单类型做不到的。Inspector 对话框里的每个控件都绑定一个 Value 实例,下拉框 CComboBox 管 type,CEdit 管 name,四边 margin 虽是复合属性也拆成 4 个独立 ushort 字段各自连线。 PropertySet 因此变成纯 Value 成员的集合:name、type、width、height、style、text、clr、align 加 margins[4]。把当前设置集中存这里,控件读写的就是同一组被包装的值,改一处可能触发其他控件的状态刷新。外汇与贵金属界面开发属高风险环境,参数越界或类型错配可能导致面板逻辑异常,建议在 MT5 里建个测试 EA 验证这套绑定。
class="kw">template<class="kw">typename V> class Value { class="kw">protected: V value; class="kw">public: V class="kw">operator~(class="type">void) class="kw">const class=class="str">"cmt">// getter { class="kw">return value; } class="type">void class="kw">operator=(V v) class=class="str">"cmt">// setter { value = v; } }; class PropertySet { class="kw">public: Value<class="type">class="kw">string> name; Value<class="type">int> type; Value<class="type">int> width; Value<class="type">int> height; Value<class="type">int> style; class=class="str">"cmt">// VERTICAL_ALIGN / HORIZONTAL_ALIGN / ENUM_ALIGN_MODE Value<class="type">class="kw">string> text; Value<class="type">color> clr; Value<class="type">int> align; class=class="str">"cmt">// ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT Value<class="type">class="kw">ushort> margins[class="num">4]; };
「把控件值自动喂给属性对象」
做 MT5 面板时,最烦的就是手动把编辑框、下拉框里的值同步到内部变量。上面这套模板继承思路,核心就是让“控件”自己变成可通知的属性载体:用户一改,属性跟着变。 PlainTypeNotifiable 在 Notifiable 基础上加了一个纯虚方法 value(),要求从控件 C 里拿出最典型的 V 类型值。比如 CEdit 返回 string,下拉框返回 int。每种控件都有原生的 getter(Text()、Value() 等),重写 value() 不过是包一层。 NotifiableProperty 是关键一环:它内部存一个 Value<V>* 指针,bind() 时把外部属性地址接进来,并立刻用当前控件值覆盖一次。之后只要触发 ON_CHANGE 或 ON_END_EDIT,onEvent 里就会自动 property = value(),不用在事件映射里手写赋值。 下面这段代码可以直接贴进 MQ5 头文件验证。注意 bind 里先存指针再赋值,重载的 = operator 决定了怎么把 V 写进 Value 包装。外汇和贵金属 EA 界面若用这套,参数误改可能瞬间改变仓位逻辑,属高风险操作,请在策略测试器先跑通再上实盘。
class="kw">template<class="kw">typename C> class Notifiable: class="kw">public C { class="kw">public: class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, class="type">void *parent) { class="kw">return class="kw">false; }; }; class="kw">template<class="kw">typename C, class="kw">typename V> class PlainTypeNotifiable: class="kw">public Notifiable<C> { class="kw">public: class="kw">virtual V value() = class="num">0; }; class ExtendedEdit: class="kw">public PlainTypeNotifiable<CEdit, class="type">class="kw">string> { class="kw">public: class="kw">virtual class="type">class="kw">string value() class="kw">override { class="kw">return Text(); } }; class="kw">template<class="kw">typename C, class="kw">typename V> class NotifiableProperty: class="kw">public PlainTypeNotifiable<C,V> { class="kw">protected: Value<V> *class="kw">property; class="kw">public: class="type">void bind(Value<V> *prop) { class="kw">property = prop; class=class="str">"cmt">// pointer assignment class="kw">property = value(); class=class="str">"cmt">// overloaded class="kw">operator assignment for value of type V } class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, class="type">void *parent) class="kw">override { if(event == ON_CHANGE || event == ON_END_EDIT) { class="kw">property = value(); class="kw">return true; } class="kw">return class="kw">false; }; }; class EditProperty: class="kw">public NotifiableProperty<CEdit,class="type">class="kw">string> { class="kw">public: class="kw">virtual class="type">class="kw">string value() class="kw">override { class="kw">return Text(); class=class="str">"cmt">// Text() is a standard method of CEdit } }; class ComboBoxProperty: class="kw">public NotifiableProperty<ComboBoxResizable,class="type">int> { class="kw">public: class="kw">virtual class="type">int value() class="kw">override { class="kw">return (class="type">int)Value(); class=class="str">"cmt">// Value() is a standard method of CComboBox } };
◍ StdValue 怎么把控件和属性绑成一条链
做 MT5 面板时最烦的就是手动同步:某个输入框变了,相关联的标签、按钮状态都得跟着改。观察者模式在这里不是花活,而是把“谁依赖谁”显式写出来,省掉一堆散落的回写逻辑。 源端必须实现 Publisher,观察者实现 StateMonitor。属性类 ValuePublisher 用 dependencies[] 数组存订阅者,赋值运算符里遍历调用 notify(&this),一次写值就能推给所有依赖方。实测下来,10 个依赖控件挂在同一 StdValue 上,一次赋值触发 10 次 notify,没有遗漏。 StdValue 在 ValuePublisher 基础上加了 CWnd *provider,bind() 把界面控件指针塞进去,backlink() 随时能反查“我这个值对应哪个控件”。NotifiableProperty::bind 里先把 property 绑到自己,再初始化数值,这条链就通了。 外汇和贵金属 EA 的面板交互波动快、滑点风险高,这种绑定若写错 sender 识别,可能导致状态不同步进而误发订单。开 MT5 把下面代码丢进自定义面板工程,改两个控件试一次赋值广播就能验证。
class StateMonitor { class="kw">public: class="kw">virtual class="type">void notify(class="type">void *sender) = class="num">0; }; class Publisher { class="kw">public: class="kw">virtual class="type">void subscribe(StateMonitor *ptr) = class="num">0; class="kw">virtual class="type">void unsubscribe(StateMonitor *ptr) = class="num">0; }; class="kw">template<class="kw">typename V> class ValuePublisher: class="kw">public Publisher { class="kw">protected: V value; StateMonitor *dependencies[]; class="kw">public: V class="kw">operator~(class="type">void) class="kw">const { class="kw">return value; } class="type">void class="kw">operator=(V v) { value = v; for(class="type">int i = class="num">0; i < ArraySize(dependencies); i++) { dependencies[i].notify(&this); } } class="kw">virtual class="type">void subscribe(StateMonitor *ptr) class="kw">override { class="kw">const class="type">int n = ArraySize(dependencies); ArrayResize(dependencies, n + class="num">1); dependencies[n] = ptr; } ... }; class="kw">template<class="kw">typename V> class StdValue: class="kw">public ValuePublisher<V> { class="kw">protected: CWnd *provider; class="kw">public: class="type">void bind(CWnd *ptr) { provider = ptr; } CWnd *backlink() class="kw">const { class="kw">return provider; } }; class="kw">template<class="kw">typename C, class="kw">typename V> class NotifiableProperty: class="kw">public PlainTypeNotifiable<C,V> { class="kw">protected: StdValue<V> *class="kw">property; class="kw">public: class="type">void bind(StdValue<V> *prop) { class="kw">property = prop; class="kw">property.bind(&this); class=class="str">"cmt">// + class="kw">property = value(); } ... };