MQL 作为 MQL 程序图形界面的标记工具。 第一部分(基础篇)
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 本身不影响行情,但面板逻辑写错可能误发订单,这类开发风险集中在代码维护期而非市场端。
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 界面逻辑改错易致面板空白,属高风险调试项。
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 这类参数规范化。
{
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[] 则用来批量生成同类型控件如按钮组。
<span class="keyword">class </span>LayoutData { <span class="keyword">class="kw">protected</span>: <span class="keyword">class="kw">static</span> RubbArray<LayoutData *> 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><<span class="keyword">class="kw">typename</span> P,<span class="keyword">class="kw">typename</span> C> <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><<span class="keyword">class="kw">typename</span> T> <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() > class="num">0) { <span class="keyword">if</span>(_x1 == class="num">0 && _y1 == class="num">0 && _x2 == class="num">0 && _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) {