MQL 作为 MQL 程序图形界面的标记工具。 第一部分(基础篇)
📘

MQL 作为 MQL 程序图形界面的标记工具。 第一部分(基础篇)

第 1/3 篇

MQL 图形界面:用还是不用

在 MT5 里用 MQL 写程序,到底要不要带图形窗口界面,社区里一直没统一说法。有人觉得交易者就该一键启动机器人然后等钱进来,但也有人清楚那是幻想——实盘前往往要花大量时间调参,跑起来还得盯盘手动干预。 对于纯手动交易者,一个顺手的下单面板基本决定了执行效率的下限。Stanislav Korotky 在 2020-07-13 发的原帖有 4224 次浏览、54 条回帖,说明这块需求真实存在。 结论不绝对:早一点给程序配个简单界面,比一直裸跑命令行式 EA 更利于排查状态和介入。外汇与贵金属杠杆高,界面再直观也替代不了你对风险的实时把控。

「MQL5 界面布局为什么该和代码拆开」

MT5 标准库里画一个交易面板,往往要塞进去几十个控件。上面这段 CPanelDialog::Create 就是典型命令式写法:每加一个按钮就手写一组坐标常量,BUTTON_WIDTH、INDENT_TOP 这类“魔幻数字”散落各处,复制粘贴时改错一个就埋雷。 其他语言早把界面描述和逻辑解耦了。安卓用 XML 写 LinearLayout、TextView,属性标 match_parent 或 wrap_content,层级一眼能看清;.Net 走 XAML,控件、容器、事件响应也是声明式。好处很实在:层次直观、逻辑分组、对齐统一、布局可复用,还能拿外部图形编辑器拖完再生成代码。 MQL 环境里这块一直半残。MasterWindows 的可视化设计器元素类型受限;CBox/CGrid 容器思路先进却没设计器,仍要手敲排布。作者早前在 OLAP 文章里扩出的“橡胶”属性控件,正好能拿来自动排列标准库对象,省掉手工重算坐标。 外汇和贵金属图表上加 GUI 本身不影响行情,但面板逻辑写错可能误发订单,这类开发风险集中在代码维护期而非市场端。

MQL5 / C++
class="type">bool CPanelDialog::Create(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin, 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)
  {
    if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) class="kw">return(class="kw">false);
    class=class="str">"cmt">// create dependent controls
    if(!CreateEdit()) class="kw">return(class="kw">false);
    if(!CreateButton1()) class="kw">return(class="kw">false);
    if(!CreateButton2()) class="kw">return(class="kw">false);
    if(!CreateButton3()) class="kw">return(class="kw">false);
    ...
    if(!CreateListView()) class="kw">return(class="kw">false);
    class="kw">return(true);
  }
  
  class="type">bool CPanelDialog::CreateButton2(class="type">void)
  {
    class=class="str">"cmt">// coordinates
    class="type">int x1 = ClientAreaWidth() - (INDENT_RIGHT + BUTTON_WIDTH);
    class="type">int y1 = INDENT_TOP + BUTTON_HEIGHT + CONTROLS_GAP_Y;
    class="type">int x2 = x1 + BUTTON_WIDTH;
    class="type">int y2 = y1 + BUTTON_HEIGHT;
  
    if(!m_button2.Create(m_chart_id, m_name + "Button2", m_subwin, x1, y1, x2, y2)) class="kw">return(class="kw">false);
    if(!m_button2.Text("Button2")) class="kw">return(class="kw">false);
    if(!Add(m_button2)) class="kw">return(class="kw">false);
    m_button2.Alignment(WND_ALIGN_RIGHT, class="num">0, class="num">0, INDENT_RIGHT, class="num">0);
    class="kw">return(true);
  }
  ...
  CEdit        m_edit;          class=class="str">"cmt">// the display field object
  CButton      m_button1;       class=class="str">"cmt">// the button object
  CButton      m_button2;       class=class="str">"cmt">// the button object
  CButton      m_button3;       class=class="str">"cmt">// the fixed button object
  CSpinEdit    m_spin_edit;     class=class="str">"cmt">// the up-down object
  CDatePicker  m_date;          class=class="str">"cmt">// the datepicker object
  CListView    m_list_view;     class=class="str">"cmt">// the list object
  CComboBox    m_combo_box;     class=class="str">"cmt">// the dropdown list object
  CRadioGroup  m_radio_group;   class=class="str">"cmt">// the radio buttons group object
  CCheckGroup  m_check_group;   class=class="str">"cmt">// the check box group object

◍ 图形编辑器背后的控件双版本陷阱

MT5 标准库里的界面控件,本质上要维护两套形态:运行时版本负责窗口里真实交互,设计时版本则是包了一层的属性查看/修改壳。给每种控件手写这个包装类工程量极大,理论上可以借 MQL 解析器自动扫出每个类里的同名「镜像方法」对——一个传值、一个返回值——再按继承链拼成通用列表,生成包装器。 以 CEdit 为例,只读属性就是靠一对 bool ReadOnly(void) const 和 bool ReadOnly(const bool flag) 实现的;CSpinEdit 的上限值也用 int MaxValue(void) const 配 void MaxValue(const int value) 镜像出来。MQL 本身没有语言级的 setter/getter,标准库是用这种约定模拟的。 每个控件类只需用解析器跑一次包装生成,前提是它公共属性不变。但项目再大也得先想清利弊:核心其实就两件事——理清元素层次依赖、理清属性。类型、尺寸、对齐、文本、颜色这些基础属性大多能在 MQL 代码里直接设,而且类型/尺寸/对齐往往被对象嵌套结构隐式决定了。 所以多数情况下,与其造一个重型可视化编辑器,不如用带缩进的嵌套描述来声明界面树。下面这段结构里,m_edit_row、m_button_row 是一级容器,m_lists_column1 又嵌在 m_lists_row 里,控件随层级自行定位。外汇与贵金属 EA 开发用这套思路改 UI,能省掉大量重复包装代码,但 MT5 界面逻辑改错易致面板空白,属高风险调试项。

MQL5 / C++
class="type">bool ReadOnly(class="type">void) class="kw">const;
    class="type">bool ReadOnly(class="kw">const class="type">bool flag);
    class="type">int  MaxValue(class="type">void) class="kw">const;
    class="type">void MaxValue(class="kw">const class="type">int value);
    CBox m_main;                       class=class="str">"cmt">// main client window
    
        CBox m_edit_row;                   class=class="str">"cmt">// top level container/group
            CEdit m_edit;                      class=class="str">"cmt">// control
      
        CBox m_button_row;                 class=class="str">"cmt">// top level container/group
            CButton m_button1;                 class=class="str">"cmt">// control
            CButton m_button2;                 class=class="str">"cmt">// control
            CButton m_button3;                 class=class="str">"cmt">// control
      
        CBox m_spin_date_row;              class=class="str">"cmt">// top level container/group
            SpinEdit m_spin_edit;              class=class="str">"cmt">// control
            CDatePicker m_date;                class=class="str">"cmt">// control
      
        CBox m_lists_row;                  class=class="str">"cmt">// top level container/group
      
            CBox m_lists_column1;              class=class="str">"cmt">// nested container/group
                ComboBox m_combo_box;              class=class="str">"cmt">// control
                CRadioGroup m_radio_group;         class=class="str">"cmt">// control
                CCheckGroup m_check_group;         class=class="str">"cmt">// control
        
            CBox m_lists_column2;              class=class="str">"cmt">// nested container/group
                CListView m_list_view;             class=class="str">"cmt">// control

用 MQL5 大括号当 GUI 描述语法

做界面标记不一定非得搬 XML 再写解析器。MQL5 本身的函数/方法代码块大括号就构成了上下文嵌套,进入块建局部对象、退出块析构,这个天然层次可以直接拿来描述窗口与控件的从属关系。 思路来自早年有人用同样机制写过执行速度探查器:在代码单元里声明局部类对象,构造函数记时间、析构函数算区间,靠对象生命周期自动归集。把这套生命周期绑定换成界面容器,就能在执行期搭出动态层次,而不依赖编译前的静态类继承,也绕开了 MQL5 没有 RTTI/反射的限制。 具体落点是 container 与 control 两套模板类。container 对应可容纳子元素的窗口或面板,control 对应具体控件;所有布局对象按进入顺序压进一个特殊数组栈,栈底是对话框(this),栈顶是当前块,块关闭时其内部控件自动挂到栈顶的父容器上。上面代码里 Dialog、classA、classB、classC 都是容器,classX/classY 是真实控件,object1 到 object8 共 8 个 GUI 对象在嵌套关闭时逐级注册进父级。 短期布局辅助对象(main、top_level 等)和受控界面对象(object4、5、7、8 等)要分开看:前者常由布局系统隐式建、不暴露给算法;后者若后续要交互就显式传类变量。下一段我们会定一套属性记录语法,把 name、property 这类参数规范化。

MQL5 / C++
{
    ProfilerObject obj;
    
    ... class=class="str">"cmt">// code lines of your actual algorithm
  }
  container<Dialog> dialog(&this);
  {
    container<classA> main; class=class="str">"cmt">// create classA internal object class="num">1
    
    {
      container<classB> top_level(name, class="kw">property, ...); class=class="str">"cmt">// create classB internal object class="num">2
      
      {
        container<classC> next_level_1(name, class="kw">property, ...); class=class="str">"cmt">// create classC internal object class="num">3
        
        {
          control<classX> ctrl1(object4, name, class="kw">property, ...); class=class="str">"cmt">// create classX object class="num">4
          control<classX> ctrl2(object5, name, class="kw">property, ...); class=class="str">"cmt">// create classX object class="num">5
        } class=class="str">"cmt">// register objects class="num">4&class="num">5 in object class="num">3 (via ctrl1, ctrl2 in next_level_1) 
      } class=class="str">"cmt">// register object class="num">3 in object class="num">2 (via next_level_1 in top_level)
      
      {
        container<classC> next_level2(name, class="kw">property, ...); class=class="str">"cmt">// create classC internal object class="num">6
        
        {
          control<classY> ctrl3(object7, name, class="kw">property, ...); class=class="str">"cmt">// create classY object class="num">7
          control<classY> ctrl4(object8, name, class="kw">property, ...); class=class="str">"cmt">// create classY object class="num">8
        } class=class="str">"cmt">// register objects class="num">7&class="num">8 in object class="num">6 (via ctrl3, ctrl4 in next_level_2)
      } class=class="str">"cmt">// register object class="num">6 in object class="num">2 (via next_level_2 in top_level)
    } class=class="str">"cmt">// register object class="num">2 in object class="num">1 (via top_level in main)
  } class=class="str">"cmt">// register object class="num">1 (main) in the dialog(this)

「用抽象类把界面布局从控件库里抽出来」

想把 MT5 界面元素做成可复用的层次结构,第一步是把「通用逻辑」和「具体控件库实现」拆开。抽象层只管最少信息:每个布局元素有唯一 _id 和坐标,具体库里再映射成自己的控件属性——标准库里对应 CWnd::Name 拿到的 m_name,别和 CWnd 内部用于消息分派的 m_id 搞混。 LayoutData 是这套机制的核心,内部用静态堆栈 stack 和 rootId 记录窗口实例。MQL 程序单线程跑,哪怕开多个窗口也是逐个创建,画完一个清一次堆栈,所以这两个静态成员不会冲突。标准库里 rootId 对应 CAppDialog 的 m_instance_id,换别的库也得有等价唯一标识,否则窗口会撞车。 LayoutBase 从 LayoutData 继承,带两个模板参数 P(容器类)和 C(控件类)。标准库里 P 选 CWndContainer、C 选 CWnd 最合适:CWndObj 不能当 C,因为 CComboBox、CListView 这些是从 CWndContainer 派生的;CBox 分组用的祖先也是 CWndContainer,所以它是最近公共祖先。 init 是模板方法,从构造函数调用,把外部传进来的元素指针存好,用 setContainer 判断是不是容器,再从参数或栈顶父容器扒坐标,调 create 生成界面元素。栈空时把第一个元素的标识写进 rootId,容器则压栈。析构时容器出栈,当前元素通过 add 挂到栈顶容器——这两段就是布局自动绑定的关键。 下面这段是抽象层骨架,注意 container 指针同时充当「是否为容器」的标志位,array[] 则用来批量生成同类型控件如按钮组。

MQL5 / C++
<span class="keyword">class </span>LayoutData
{
<span class="keyword">class="kw">protected</span>:
<span class="keyword">class="kw">static</span> RubbArray&lt;LayoutData *&gt; stack;
<span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">string</span> rootId;
<span class="keyword">class="type">int</span> _x1, _y1, _x2, _y2;
<span class="keyword">class="type">class="kw">string</span> _id;
<span class="keyword">class="kw">public</span>:
LayoutData()
{
_x1 = _y1 = _x2 = _y2 = class="num">0;
_id = <span class="macro">NULL</span>;
}
};
<span class="keyword">class="kw">template</span>&lt;<span class="keyword">class="kw">typename</span> P,<span class="keyword">class="kw">typename</span> C&gt;
<span class="keyword">class </span>LayoutBase: <span class="keyword">class="kw">public</span> LayoutData
{
...
<span class="keyword">class="kw">protected</span>:
P *container; <span class="comment">class=class="str">"cmt">// not null if container(can be used as flag)</span>
C *object;
C *array[];
<span class="keyword">class="kw">public</span>:
LayoutBase(): container(<span class="macro">NULL</span>), object(<span class="macro">NULL</span>) {}
<span class="keyword">class="kw">protected</span>:
<span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> setContainer(C *control) = class="num">0;
<span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">class="kw">string</span> create(C *object, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> id = <span class="macro">NULL</span>) = class="num">0;
<span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> add(C *object) = class="num">0;
<span class="keyword">class="kw">template</span>&lt;<span class="keyword">class="kw">typename</span> T&gt;
<span class="keyword">class="type">void</span> init(T *ref, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> id = <span class="macro">NULL</span>, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> x1 = class="num">0, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> y1 = class="num">0, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> x2 = class="num">0, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> y2 = class="num">0)
{
object = ref;
setContainer(ref);
_x1 = x1;
_y1 = y1;
_x2 = x2;
_y2 = y2;
<span class="keyword">if</span>(stack.size() &gt; class="num">0)
{
<span class="keyword">if</span>(_x1 == class="num">0 &amp;&amp; _y1 == class="num">0 &amp;&amp; _x2 == class="num">0 &amp;&amp; _y2 == class="num">0)
{
_x1 = stack.top()._x1;
_y1 = stack.top()._y1;
_x2 = stack.top()._x2;
_y2 = stack.top()._y2;
}
_id = rootId + (id == <span class="macro">NULL</span> ? <span class="keyword">class="kw">typename</span>(T) + <span class="functions">StringFormat</span>(<span class="class="type">class="kw">string">"%d"</span>, object) : id);
}
<span class="keyword">else</span>
{
_id = (id == <span class="macro">NULL</span> ? <span class="keyword">class="kw">typename</span>(T) + <span class="functions">StringFormat</span>(<span class="class="type">class="kw">string">"%d"</span>, object) : id);
}
<span class="keyword">class="type">class="kw">string</span> newId = create(object, _id);
<span class="keyword">if</span>(stack.size() == class="num">0)
{
rootId = newId;
}
<span class="keyword">if</span>(container)
{

常见问题

简单工具可以硬编码按钮,但面板稍复杂就用标记语言描述布局,改界面不用动逻辑代码,后期好维护。
布局依赖具体坐标和屏幕尺寸,和控件逻辑耦合就会适配崩;把布局抽成独立描述,控件只管功能就行。
可以,小布能扫你的面板工程结构,标出布局与逻辑耦合的地方,并给拆分层级的改法建议。
编辑器常内置旧版控件渲染,实盘用新版,双版本差异导致显示分裂;以实盘环境为准做校验。
只在布局描述里加控件声明,抽象类接口不变就无需碰底层库,降低改坏概率。