图形界面 I:库结构(第一章)(基础篇)
理清界面库的分层骨架
要在 MT5 里搭带图形界面的 EA 或指标,先得弄清标准库里控件是怎么分层的。底层是作为基本对象的标准库基类,往上是带附加方法的衍生类,再往上是所有空间的基类,以及带图形界面应用程序自己的基类。 这套层级不是摆设。2016 年 3 月 9 日发布的示例里,作者用 9 285 行量级的代码验证了控件列表到事件句柄测试的完整链路,说明分层结构能直接支撑程序库和程序类的事件句柄测试。 打开 MT5 的 MQL5/Include/Controls 目录,对照基类声明逐个看继承关系,比直接抄界面代码更能少踩坑。外汇与贵金属交易界面自动化本身属高风险操作,任何控件逻辑异常都可能引发误单。
「为什么还要造一个 MQL5 界面库」
MQL5 应用里至今没有能快速搭出高质量图形界面的独立代码库。这里说的图形界面,是指类似操作系统里那种窗口、按钮、滚动条的交互层,而不是指标画线本身。 Dmitry Fedoseev 早前的自定义图形控件系列(共 3 篇)比标准库控件更全,还附了说明书,但我对其中部分控制机制不认可;标准库框架结构更好却缺文档、也少特定控件。两套方案在「标准化」和「高质量」上都没兼得,这才有了重写的动机。 我之前写过的子窗口按钮、滚动条两篇是面向过程写法,只够熟悉语法。本系列改用面向对象,用一个大项目把结构讲透。 读完你至少能拿走三样东西:一套能直接二次开发的界面库源码、一套从零到实现的 OOP 大型项目样例、以及和现有专家库对照批判的切入点。外汇与贵金属自动化高风险,界面只是工具,不代表策略胜率。 叙述上我管这叫「思维序列的模拟」——真实开发充满试错,文章里只留理想顺序。首次碰这种规模项目的,建议照着库代码在 MT5 里重敲一遍,比纯读更管用。
◍ 界面库初版到底塞了哪些控件
做一套能在 MT4 和 MT5 直接跑的图形界面库,第一步不是写框架,而是先把要用到的控件清单钉死。每个人对面板的需求差得很远:有人只要几个按钮加勾选框,有人要能叠多窗口、分数据类型摆不同控件。 初始版本我列了下面这些元素,部分会以多种实现方式拆成独立类,专治不同场景——比如有的按钮类适合纯点击,有的带图标扩展,有的能拆出子功能。清单如下:窗口(可任意顺序塞多个控件)、带下拉的菜单、纯文本菜单、状态栏、简单按钮、图标按钮(带扩展)、分离功能按钮、简单/电台/图标按钮组、复选框、旋转编辑栏、带旋转栏的复选框、垂直与水平滚动条、查看列表、组合框(含下拉视图)、带下拉的复选框、下拉+编辑组合框、单边/双边编辑滑块、静态与下拉日历、提示框、进度条、标签表/标记表/画布表、标签、树形层级列表。 要注意控件之间是嵌套依赖的:组合框里含列表视图,列表视图又挂垂直滚动条;所有表都内嵌横竖滚动条;下拉日历本身复用了可独立存在的日历控件。定完这张表,后续才逐个类去落地。
界面基类的继承与精简取舍
MT4 与 MT5 的图形基本元素类文件分别放在 <Data folder>\MQL4\Include\ChartObjects 和 <Data folder>\MQL5\Include\ChartObjects。整套标准库的源头是 CObject,它派生出 CChartObject,后者收拢了所有图形对象通用的创建、删除与属性读写方法,再往下才分出各个独立对象的管理类。 做图形界面并不需要把标准库全搬进来。实际只用得到五个与时间轴脱钩的子类:CChartObjectRectLabel(矩形标签)、CChartObjectEdit(编辑框)、CChartObjectLabel(文本标签)、CChartObjectBmpLabel(位图标签)和 CChartObjectButton(按钮)。它们不会随图表横向滚动而移位,适合做固定面板。 每个基本元素若只靠上层基类去取坐标、尺寸、边缘缩进和悬停聚焦状态,也能跑通,但反复调用通用方法在资源上并不划算。更直接的做法是给这些元素写派生类,把高频引用的属性缓存下来,附带专门的获取与保存方法。 标准库自身的关联图可以压缩成精简版:末端节点可对应原结构中任意一个(…)图形对象。写自己的类库时,照这种精简图展示依赖关系就够了,不必把整棵继承树都画出来。
「从基类扩出带坐标属性的图形对象」
在 MT5 数据目录的 MQL5\Include 下建一个 EasyAndFastGUI 文件夹,所有界面库文件都塞进去;Controls 子目录里再放 Objects.mqh,派生类全写在这个文件里。找数据目录直接走菜单 File > Open Data Folder,MT4 对应路径是 MQL4\Include,别翻错盘。 这些派生类类型一致,没必要逐个铺代码,挑一个 CRectLabel 看结构就够了。构造函数里把坐标、间距、尺寸、鼠标焦点等变量初始化,部分走初始化列表,部分在类体内赋值——实际工程里没碰到过派生类与基类字段初始化顺序踩坑的情况,所以怎么写都行。 文件头部先列一遍无类体的类名方便跳转,在 MetaEditor 里把光标放类名上按 Alt+G 就能直跳对应类。Objects.mqh 里所有类都从 CChartObject… 系列派生,相当于给原生图表对象套了一层可扩展壳。 下一步要处理的是多个简单对象拼成控件后的统一管理,每个控件独立但共享一批属性,所以得再抽象一个 CElement 基类兜住公共字段。外汇与贵金属 GUI 自动化涉及实盘交互,属于高风险操作,参数乱改可能触发误下单。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Objects.mqh | class=class="str">"cmt">//| Copyright class="num">2015, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <ChartObjects\ChartObjectsBmpControls.mqh> class="macro">#include <ChartObjects\ChartObjectsTxtControls.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 带有附加属性的矩形标签对象类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRectLabel : class="kw">public CChartObjectRectLabel { class="kw">protected: class="type">int m_x; class="type">int m_y; class="type">int m_x2; class="type">int m_y2; class="type">int m_x_gap; class="type">int m_y_gap; class="type">int m_x_size; class="type">int m_y_size; class="type">bool m_mouse_focus; class="kw">public: CRectLabel(class="type">void); ~CRectLabel(class="type">void); class=class="str">"cmt">//--- 坐标 class="type">int X(class="type">void) { class="kw">return(m_x); } class="type">void X(class="kw">const class="type">int x) { m_x=x; } class="type">int Y(class="type">void) { class="kw">return(m_y); } class="type">void Y(class="kw">const class="type">int y) { m_y=y; }
◍ 矩形标签类的边界与焦点接口
在 MT5 自定义控件里,矩形标签的几何关系靠一组存取函数锁定。X2() 与 Y2() 直接返回 m_x+m_x_size 和 m_y+m_y_size,也就是右下角坐标,比每次手动加尺寸更不容易写错。 XGap / YGap 那对函数管的是从边缘点向内的缩进量,构造函数里默认 m_x_gap 与 m_y_gap 都是 0,意味着贴边绘制;若你在黄金 H1 图表上做悬浮提示框,把 XGap(8) 设成 8 像素往往能避开坐标轴文字重叠。 MouseFocus 的 bool 开关决定鼠标悬停时是否捕获焦点,默认成员未显式赋 true,实盘里若不主动开启,右键菜单可能点不到这个标签。外汇与贵金属波动剧烈,这类界面元素只辅助判读,下单仍以小布盯盘的信号确认为主,杠杆风险自担。
class="type">int X2(class="type">void) { class="kw">return(m_x+m_x_size); } class="type">int Y2(class="type">void) { class="kw">return(m_y+m_y_size); } class=class="str">"cmt">//--- 从边缘点(xy)侧缩进 class="type">int XGap(class="type">void) { class="kw">return(m_x_gap); } class="type">void XGap(class="kw">const class="type">int x_gap) { m_x_gap=x_gap; } class="type">int YGap(class="type">void) { class="kw">return(m_y_gap); } class="type">void YGap(class="kw">const class="type">int y_gap) { m_y_gap=y_gap; } class=class="str">"cmt">//--- 尺寸 class="type">int XSize(class="type">void) { class="kw">return(m_x_size); } class="type">void XSize(class="kw">const class="type">int x_size) { m_x_size=x_size; } class="type">int YSize(class="type">void) { class="kw">return(m_y_size); } class="type">void YSize(class="kw">const class="type">int y_size) { m_y_size=y_size; } class=class="str">"cmt">//--- 焦点 class="type">bool MouseFocus(class="type">void) { class="kw">return(m_mouse_focus); } class="type">void MouseFocus(class="kw">const class="type">bool focus) { m_mouse_focus=focus; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 class=class="str">"cmt">//+------------------------------------------------------------------+ CRectLabel::CRectLabel(class="type">void) : m_x(class="num">0), m_y(class="num">0), m_x2(class="num">0), m_y2(class="num">0), m_x_gap(class="num">0), m_y_gap(class="num">0), m_x_size(class="num">0),