MQL 作为 MQL 程序图形界面的标记工具(第三部)。 窗体设计师(基础篇)
📘

MQL 作为 MQL 程序图形界面的标记工具(第三部)。 窗体设计师(基础篇)

第 1/3 篇

「用 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 头文件里验证。

MQL5 / C++
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 验证这套绑定。

MQL5 / C++
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 界面若用这套,参数误改可能瞬间改变仓位逻辑,属高风险操作,请在策略测试器先跑通再上实盘。

MQL5 / C++
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 把下面代码丢进自定义面板工程,改两个控件试一次赋值广播就能验证。

MQL5 / C++
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();
      }
      ...
  };

常见问题

先搭双窗口结构:左侧放控件树、右侧放属性面板,用编辑器统一管理控件属性,再逐步加按钮和输入框。
裸类型没有统一读写入口,改动不触发通知。用 Value 包装器封装后,取值赋值都走同一接口,方便联动。
小布可根据你描述的控件清单自动生成双窗口结构草图与绑定示例,你复制进工程改参数即可跑。
StdValue 在控件和属性对象间建一条链:属性框改值→包装器通知→对象字段自动更新,无需手写回调。
用 Value 包装器加 StdValue 链式绑定,把控件值自动喂给属性对象,新增属性只改声明不改胶水代码。