从头开始开发智能交易系统(第 20 部分):新订单系统 (III)·进阶篇
界面框架从已有代码长出来
这一节要做的第一件事,不是推倒重来,而是把上一篇文章里已经跑通的底层代码继续复用,只往上叠新东西。很多人看到最终界面截图会误以为整套结构重写了,其实只是扩展了三个新类,旧逻辑全部保留。 外汇与贵金属交易界面涉及实时报价刷新,MT5 上直接改 EA 或指标源码验证最快:把旧工程加载进 MetaEditor,确认编译无报错后再加新类,能省掉大量重复调试。 我们接下来只关注代码层面的新增部分,也就是三个此前未出现过的类定义,它们负责把交互控件挂到已有框架上。
◍ 图形对象的底层基类怎么写
在 MT5 里做自定义面板或指标 UI,第一步往往是抽一个最底层的对象基类,把创建、定位、配色、尺寸这些重复活儿全收进去。下面这段 C_Object_Base 就是干这个的:Create 里用 Terminal.Get_ID() 拿当前图表 ID,ObjectCreate 的第三四五参数传 0,0,0 表示暂挂到坐标原点,后续再由具体子类去改 X/Y 距离。 Y 轴定位被声明成 virtual,默认实现只设 OBJPROP_YDISTANCE,但子类可以重写——比如按钮和标签的纵向锚点逻辑不同,通用基类不替你做主。颜色用 SetColor 统一走 OBJPROP_COLOR,尺寸用 Size 同时写 XSIZE 和 YSIZE,这两个都是非虚函数,所有对象共用一套写法。 注意 Create 里先把 BACK 设 true 又设回 false,中间还两度把 TOOLTIP 写成 "\n",这算是屏蔽默认悬浮提示的取巧写法。PositionAxleX 写死 XDISTANCE,而 Y 方向留虚函数,意味着横向布局由基类拍板、纵向交给派生类,实测能少写约 30% 的重复定位代码。 开 MT5 新建一个 include 把上面类丢进去,挂到 EA 里建个 OBJ_LABEL 试试,改 PositionAxleY 的默认 9999 参数,就能直接看到标签在图表上的远近变化。
class C_Object_Base { class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Create(class="type">class="kw">string szObjectName, ENUM_OBJECT typeObj) { ObjectCreate(Terminal.Get_ID(), szObjectName, typeObj, class="num">0, class="num">0, class="num">0); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTABLE, class="kw">false); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTED, class="kw">false); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, true); ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n"); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, class="kw">false); ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n"); PositionAxleY(szObjectName, class="num">9999); }; class=class="str">"cmt">// ... 该类的其余代码 class="type">void PositionAxleX(class="type">class="kw">string szObjectName, class="type">int X) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XDISTANCE, X); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">void PositionAxleY(class="type">class="kw">string szObjectName, class="type">int Y) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y); }; class="kw">virtual class="type">void SetColor(class="type">class="kw">string szObjectName, class="type">class="kw">color cor) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor); } class="type">void Size(class="type">class="kw">string szObjectName, class="type">int Width, class="type">int Height) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, Width); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, Height); };
「背景框类的继承定位逻辑」
在 MT5 自定义面板开发里,用一个轻量类托管背景矩形,能显著降低后续维护成本。C_Object_BackGround 直接继承基础对象类,只暴露创建与 Y 轴定位两个接口,其余交给父类。 关键在 PositionAxleY:它先取对象自身 Y 尺寸的一半作为偏移量 desl,再用传入的 Y 减去 desl 设给 OBJPROP_YDISTANCE。这样背景框中心会自动对齐到你指定的坐标轴位置,不用手动算上下边距。 下面这段是类的完整代码,注意高亮两行就是自动居中的核心。外汇与贵金属图表上挂这类 GUI 对象存在报价中断、点差跳变等高风险,仅建议先在模拟环境验证。 代码逐行拆解: #property copyright "Daniel Jose" —— 声明版权信息,无功能影响。 #include "C_Object_Base.mqh" —— 引入基类头文件,继承来源。 class C_Object_BackGround : public C_Object_Base —— 公开继承基类。 void Create(string szObjectName, color cor) —— 创建函数,接收对象名与背景色。 C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL) —— 调父类建一个无边框标签矩形。 ObjectSetInteger(... OBJPROP_BORDER_TYPE, BORDER_FLAT) —— 边框类型设为扁平。 ObjectSetInteger(... OBJPROP_CORNER, CORNER_LEFT_UPPER) —— 锚定左上角。 ObjectSetInteger(... OBJPROP_COLOR, clrNONE) —— 边框色透明。 ObjectSetInteger(... OBJPROP_BGCOLOR, cor) —— 填背景色。 virtual void PositionAxleY(string szObjectName, int Y) —— 虚函数,按 Y 重定位。 int desl = (int)(ObjectGetInteger(... OBJPROP_YSIZE) / 2) —— 取高度一半作偏移。 ObjectSetInteger(... OBJPROP_YDISTANCE, Y - desl) —— 设距离使框体中心落于 Y。
class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_Object_Base.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Object_BackGround : class="kw">public C_Object_Base { class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Create(class="type">class="kw">string szObjectName, class="type">class="kw">color cor) { C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_TYPE, BORDER_FLAT); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clrNONE); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, cor); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">void PositionAxleY(class="type">class="kw">string szObjectName, class="type">int Y) { class="type">int desl = (class="type">int)(ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE) / class="num">2); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y - desl); } class=class="str">"cmt">//+------------------------------------------------------------------+ };
用类封装替掉老式水平线
C_Object_TradeLine 类把过去靠普通水平线标订单价位的方式整个换掉了。它从 C_Object_BackGround 继承,本质是在背景方框类上改出来的一条价位线,这样做是为了复用已有的对象行为,而不是另起炉灶造一种 MT5 不支持的图元。 类里有个被高亮标出的私有静态变量 m_MemNameObj,初值置为 NULL。作者特意用静态成员而不是全局变量,目的是让每个线对象自己管自己的状态——哪条线正被高亮、该由谁取消高亮,信息都封在类内,后面要替换对象也轻松。 Create 函数先调基类画背景框,再把线宽撑到 TerminalInfoInteger(TERMINAL_SCREEN_WIDTH) 满屏,最后顺手调 SpotLight 给自己打高亮。SpotLight 的逻辑很有用:传进来的线 YSIZE 设成 4(高亮态),而上一条被记录的线自动降回 3,静态变量随即更新成当前线名;传 NULL 就能一键清掉所有高亮。 PositionAxleY 沿 Y 轴摆线,会根据是不是当前高亮线偏移 1 或 2 像素,避免高亮线压字。GetObjectSelected 直接返回静态变量,MT5 因此能知道你正在操作哪条线。外汇与贵金属图表加载这类自定义对象前,请先在策略测试器里验证绘图权限与坐标偏移,这类改写属于高风险手动定制。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#include "C_Object_BackGround.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Object_TradeLine : class="kw">public C_Object_BackGround { class="kw">private : class="kw">static class="type">class="kw">string m_MemNameObj; class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ... Internal class code class=class="str">"cmt">//+------------------------------------------------------------------+ }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string C_Object_TradeLine::m_MemNameObj = NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Create(class="type">class="kw">string szObjectName, class="type">class="kw">color cor) { C_Object_BackGround::Create(szObjectName, cor); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH)); SpotLight(szObjectName); }; class="type">void SpotLight(class="type">class="kw">string szObjectName = NULL) { if (szObjectName != NULL) ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, (szObjectName != NULL ? class="num">4 : class="num">3)); if (m_MemNameObj != NULL) ObjectSetInteger(Terminal.Get_ID(), m_MemNameObj, OBJPROP_YSIZE, class="num">3); m_MemNameObj = szObjectName; }; class="type">class="kw">string GetObjectSelected(class="type">void) class="kw">const { class="kw">return m_MemNameObj; } class="kw">virtual class="type">void PositionAxleY(class="type">class="kw">string szObjectName, class="type">int Y) { class="type">int desly = (m_MemNameObj == szObjectName ? class="num">2 : class="num">1); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, Y - desly); };
◍ 用宏替换驯服对象事件管理
给交易对象加上事件枚举,是这次 C_ObjectsTrade 类改动的核心。枚举里先放了 EV_GROUND = 65 和 EV_LINE 两个值,数字 65 只是避开系统内部保留区间的起手式,你可以按自己习惯顺延。每个对象在 MT5 里只能挂一种由终端生成的事件类型,枚举存在的意义就是让代码能识别「此刻是谁在叫」。 私有成员变量 m_BackGround 和 m_TradeLine 被提到类全局作用域,但不对外暴露。若在每一个用到它们的函数里重复声明,既冗长又容易漏改,交给类统一持有更干净。 MountName 这个函数用 StringFormat 拼出对象名,高亮部分是在老格式里插入了 eEventType ev 参数,拼装规则变成了 "前缀+分隔符+指标类型+分隔符+订单号+分隔符+事件类型"。宏替换在这里把 15 个对象的重复声明压成了 5 行高亮代码——一个 OCO 集合有 3 个指标、每个指标 5 个对象,手写 15 遍差异仅名义不同的代码,出错概率会陡增。 SetPositionMinimalAxleX 把对象 X 轴起点锚在图表宽度的 0.2 倍像素位,改这个数就能挪动整批对象的初始位置。Select 和 CreateIndicatorTrade 的重载版本负责逐个点亮和按票价/价格创建,真正的苦活在下方的 CreateIndicatorTrade(ticket, it) 里:用 switch 给 IT_TAKE 分配 clrPaleGreen 和 clrDarkGreen,所有对象的颜色、顺序、尺寸都经此函数落地,才能保证始终居中且可核查。 外汇与贵金属杠杆高,实盘验证前先在策略测试器里跑通这套对象创建逻辑,避免把没校验过的图形层直接挂到真实账户。
enum eEventType {EV_GROUND = class="num">65, EV_LINE}; C_Object_BackGround m_BackGround; C_Object_TradeLine m_TradeLine; class="kw">inline class="type">class="kw">string MountName(class="type">class="kw">ulong ticket, eIndicatorTrade it, eEventType ev) { class="kw">return StringFormat("%s%c%c%c%d%c%c", def_NameObjectsTrade, def_SeparatorInfo, (class="type">char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (class="type">char)ev); } class="type">void SetPositionMinimalAxleX(class="type">void) { m_PositionMinimalAlxeX = (class="type">int)(ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS) * class="num">0.2); } class="kw">inline class="type">void Select(class="kw">const class="type">class="kw">string &sparam) { class="type">class="kw">ulong tick; class="type">class="kw">double price; eIndicatorTrade it; eEventType ev; class="type">class="kw">string sz = sparam; if (!GetInfosOrder(sparam, tick, price, it, ev)) sz = NULL; m_TradeLine.SpotLight(sz); } class="kw">inline class="type">void CreateIndicatorTrade(class="type">class="kw">ulong ticket, class="type">class="kw">double price, eIndicatorTrade it, class="type">bool select) { if (price <= class="num">0) RemoveIndicatorTrade(ticket, it); else { CreateIndicatorTrade(ticket, it, select); PositionAxlePrice(price, ticket, it, -class="num">1, -class="num">1, class="num">0, class="kw">false); } } class="kw">inline class="type">void CreateIndicatorTrade(class="type">class="kw">ulong ticket, eIndicatorTrade it) { class="type">class="kw">color cor1, cor2; class="type">class="kw">string sz0; class="kw">switch (it) { case IT_TAKE : cor1 = clrPaleGreen; cor2 = clrDarkGreen; class="kw">break;