DoEasy. 控件 (第 1 部分): 第一步·进阶篇
🧩

DoEasy. 控件 (第 1 部分): 第一步·进阶篇

(2/3)· 沿用画布与窗体父类,给库补齐延迟报价与对冲账户属性,再落地第一个容器控件

含代码示例实战向 第 2/3 篇
很多人直接在窗体类里硬塞控件管理逻辑,结果嵌套一层就乱了层级。面板没作为独立容器抽出来前,拖拽和重绘都会互相踩踏。先把父类改利索,后续加按钮文本框才不会返工。

订阅延迟与排序枚举的底层定义

MT5 里符号的整数属性总数被定为 41 个,其中 1 个不参与 Market Watch 排序。宏 SYMBOL_PROP_INTEGER_SKIP 就是那个被跳过的槽位,写 EA 时若自己遍历属性数组,得把偏移算准,否则会漏掉或错位。 订阅制品种报价的延迟属性 SYMBOL_PROP_SUBSCRIPTION_DELAY 单独标出,对应的排序模式 SORT_BY_SYMBOL_SUBSCRIPTION_DELAY 也一并存在。做跨品种监控时,这类延迟可能让 tick 到达时间差出几百毫秒,高频套利逻辑要留缓冲。 图形元素类型枚举 ENUM_GRAPH_ELEMENT_TYPE 从 STANDARD 到 WinForms 的 PANEL 共 7 类。自绘 UI 面板时,用 GRAPH_ELEMENT_TYPE_PANEL 才能挂进系统窗体体系,硬用 STANDARD 对象模拟会丢掉焦点响应。

MQL5 / C++
class="macro">#define SYMBOL_PROP_INTEGER_TOTAL(class="num">41)
class="macro">#define SYMBOL_PROP_INTEGER_SKIP(class="num">1)
class="macro">#define FIRST_SYM_DBL_PROP(SYMBOL_PROP_INTEGER_TOTAL-SYMBOL_PROP_INTEGER_SKIP)
class="macro">#define FIRST_SYM_STR_PROP(SYMBOL_PROP_INTEGER_TOTAL-SYMBOL_PROP_INTEGER_SKIP+SYMBOL_PROP_DOUBLE_TOTAL-SYMBOL_PROP_DOUBLE_SKIP)
enum ENUM_SORT_SYMBOLS_MODE
  {
   SORT_BY_SYMBOL_OPTION_MODE,
   SORT_BY_SYMBOL_OPTION_RIGHT,
   SORT_BY_SYMBOL_SUBSCRIPTION_DELAY,
  };
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,
   GRAPH_ELEMENT_TYPE_ELEMENT,
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,
   GRAPH_ELEMENT_TYPE_FORM,
   GRAPH_ELEMENT_TYPE_WINDOW,
   GRAPH_ELEMENT_TYPE_PANEL,
  };

◍ 画布控件的自适应与层级枚举

在 MT5 自定义面板开发里,画布元素的两套枚举直接决定布局行为和鼠标事件归属。ENUM_CANV_ELEMENT_AUTO_SIZE_MODE 控制元素尺寸随容器变化的逻辑:GROW 只增不减,GROW_SHRINK 则可随容器同步放大缩小。 停靠模式由 ENUM_CANV_ELEMENT_DOCK_MODE 定义,TOP/BOTTOM 贴边并沿容器宽度拉伸,LEFT/RIGHT 贴边沿高度拉伸,FILL 铺满整个容器,NONE 则锁死在指定坐标且尺寸不变。做多控件面板时,用 DOCK 组合比手算坐标更抗分辨率变化。 整数属性总数由宏写死:CANV_ELEMENT_PROP_INTEGER_TOTAL 为 25,其中 SKIP 为 0,说明全部 25 个整型属性都参与排序索引。CANV_ELEMENT_PROP_ZORDER 决定图表点击事件的接收优先级,CANV_ELEMENT_PROP_ENABLED 控制元素是否可用——这两个在交互面板里最常改。 排序起点靠两个宏推算:FIRST_CANV_ELEMENT_DBL_PROP 等于整型总数减跳过数,字符串属性起点再叠加重型属性总数减跳过数。SORT_BY_CANV_ELEMENT_ZORDER 就是按点击优先级排控件,浮层按钮想压在最上面就把 ZORDER 调高。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Mode of automatic interface element resizing               |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_AUTO_SIZE_MODE
  {
   CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                class=class="str">"cmt">// Increase only
   CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,         class=class="str">"cmt">// Increase and decrease
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Control borders bound to the container              |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_DOCK_MODE
  {
   CANV_ELEMENT_DOCK_MODE_TOP,                      class=class="str">"cmt">// Attaching to the top and stretching along the container width
   CANV_ELEMENT_DOCK_MODE_BOTTOM,                   class=class="str">"cmt">// Attaching to the bottom and stretching along the container width
   CANV_ELEMENT_DOCK_MODE_LEFT,                     class=class="str">"cmt">// Attaching to the left and stretching along the container height
   CANV_ELEMENT_DOCK_MODE_RIGHT,                    class=class="str">"cmt">// Attaching to the right and stretching along the container height
   CANV_ELEMENT_DOCK_MODE_FILL,                     class=class="str">"cmt">// Stretching along the entire container width and height
   CANV_ELEMENT_DOCK_MODE_NONE,                     class=class="str">"cmt">// Attached to the specified coordinates, size does not change
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Integer properties of the graphical element on the canvas       |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_INTEGER
  {
...
   CANV_ELEMENT_PROP_ZORDER,                        class=class="str">"cmt">// Priority of a graphical object for receiving the event of clicking on a chart
   CANV_ELEMENT_PROP_ENABLED,                       class=class="str">"cmt">// Element availability flag
  };
class="macro">#define CANV_ELEMENT_PROP_INTEGER_TOTAL(class="num">25)        class=class="str">"cmt">// Total number of integer properties
class="macro">#define CANV_ELEMENT_PROP_INTEGER_SKIP(class="num">0)         class=class="str">"cmt">// Number of integer properties not used in sorting
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Possible sorting criteria of graphical elements on the canvas    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define FIRST_CANV_ELEMENT_DBL_PROP(CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP)
class="macro">#define FIRST_CANV_ELEMENT_STR_PROP(CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_CANV_ELEMENT_MODE
  {
   ... 
   SORT_BY_CANV_ELEMENT_ZORDER,                     class=class="str">"cmt">// Sort by the priority of a graphical object for receiving the event of clicking on a chart

「账户对象里的锁仓与 FIFO 判定」

在封装账户属性的 CAccount 类里,两个布尔字段直接决定你的 EA 该走哪种下单逻辑:fifo_close 对应 ACCOUNT_FIFO_CLOSE,标记该账户是否只能按 FIFO 规则平仓;hedge_allowed 对应 ACCOUNT_HEDGE_ALLOWED,标记是否允许开反向仓位和挂单。 IsHedge() 并不读 hedge_allowed,而是比对 MarginMode() 是否等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING;HedgeAllowed() 才直接返回 ACCOUNT_PROP_HEDGE_ALLOWED 的属性值。两者语义不同,混用会在部分券商账户上误判锁仓权限。 外汇与贵金属保证金交易高风险,同一经纪商可能在净值低于某阈值时动态改 FIFO 标志,EA 每次 OnTick 重读属性比启动时缓存一次更稳。

MQL5 / C++
class="type">bool              fifo_close;             class=class="str">"cmt">// ACCOUNT_FIFO_CLOSE(The flag indicating that positions can be closed only by the FIFO rule)
class="type">bool              hedge_allowed;           class=class="str">"cmt">// ACCOUNT_HEDGE_ALLOWED(Permission to open opposite positions and set pending orders)

class="type">bool              FIFOClose(class="type">void)            const { class="kw">return (class="type">bool)this.GetProperty(ACCOUNT_PROP_FIFO_CLOSE); }
class="type">bool              IsHedge(class="type">void)              const { class="kw">return this.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING; }
class="type">bool              HedgeAllowed(class="type">void)         const { class="kw">return (class="type">bool)this.GetProperty(ACCOUNT_PROP_HEDGE_ALLOWED); }

账户对象里的锁仓与 FIFO 判定逻辑

在封装账户对象的 CAccount 类里,构造函数与 Refresh() 都做了同一件事:根据终端构建号去拉取锁仓与 FIFO 权限,而不是无脑读 AccountInfoInteger。 具体看高亮这段:ACCOUNT_PROP_HEDGE_ALLOWED 在 MQL5 下先判断 TerminalInfoInteger(TERMINAL_BUILD) 是否 ≥ 3245,低于该构建号直接赋 false,达到才调用 AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED)。FIFO 同理,分界构建号是 2155。 这个 3245 的门槛意味着:老版本 MT5 终端(构建号低于 3245)即便券商后台开了对冲,本地 API 也读不到,EA 若依赖该字段做仓位管理会误判。开 MT5 按 Ctrl+O 看终端构建号,低于 3245 就别信 hedge 标志。 外汇与贵金属杠杆高、规则随券商而异,锁仓是否被允许要以券商实际政策为准,代码里的构建号判定只是本地 API 可用性门槛,不等于交易合规结论。

MQL5 / C++
CAccount::CAccount(class="type">void)
  {
   this.m_type=OBJECT_DE_TYPE_ACCOUNT;
class=class="str">"cmt">//--- Initialize control data
   this.SetControlDataArraySizeLong(ACCOUNT_PROP_INTEGER_TOTAL);
   this.SetControlDataArraySizeDouble(ACCOUNT_PROP_DOUBLE_TOTAL);
   this.ResetChangesParams();
   this.ResetControlsParams();
class=class="str">"cmt">//   ... 
   this.m_long_prop[ACCOUNT_PROP_SERVER_TYPE]                = (::TerminalInfoString(TERMINAL_NAME)=="MetaTrader class="num">5" ? class="num">5 : class="num">4);
   this.m_long_prop[ACCOUNT_PROP_FIFO_CLOSE]                 = (class="macro">#ifdef __MQL5__::TerminalInfoInteger(TERMINAL_BUILD)<class="num">2155 ? false : ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE) class="macro">#else false class="macro">#endif );
   this.m_long_prop[ACCOUNT_PROP_HEDGE_ALLOWED]              = (class="macro">#ifdef __MQL5__::TerminalInfoInteger(TERMINAL_BUILD)<class="num">3245 ? false : ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED) class="macro">#else false class="macro">#endif );
class=class="str">"cmt">//   ... 
class=class="str">"cmt">//--- Update the base object data and search for changes
   CBaseObjExt::Refresh();
  }

class="type">void CAccount::Refresh(class="type">void)
  {
class=class="str">"cmt">//--- Initialize event data
   this.m_is_event=false;
   this.m_hash_sum=class="num">0;
class=class="str">"cmt">//   ... 
   this.m_long_prop[ACCOUNT_PROP_FIFO_CLOSE]                 = (class="macro">#ifdef __MQL5__::TerminalInfoInteger(TERMINAL_BUILD)<class="num">2155 ? false : ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE) class="macro">#else false class="macro">#endif );
   this.m_long_prop[ACCOUNT_PROP_HEDGE_ALLOWED]              = (class="macro">#ifdef __MQL5__::TerminalInfoInteger(TERMINAL_BUILD)<class="num">3245 ? false : ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED) class="macro">#else false class="macro">#endif );
class=class="str">"cmt">//...
   CBaseObjExt::Refresh();
   this.CheckEvents();
  }

◍ 账户 FIFO 与锁仓属性的序列化落地

在 MT5 账户对象里,FIFO 平仓规则和锁仓许可是两个容易被忽略但直接影响下单逻辑的整数属性。CAccount 类通过 ObjectToStruct 把运行时获取的 ServerType、FIFOClose、HedgeAllowed 写进内存结构体,再用 StructToCharArray 转成 uchar 数组持久化;任何一步失败都会返回 false 并打印错误码,方便在 EA 初始化阶段拦截异常账户环境。 StructToObject 做反向还原,把结构体里的 fifo_close、hedge_allowed 映射回 m_long_prop 数组对应键(ACCOUNT_PROP_FIFO_CLOSE / ACCOUNT_PROP_HEDGE_ALLOWED)。这样 EA 在后续 tick 中读属性不必反复调终端接口,直接查本地数组,延迟倾向更低。 GetPropertyDescription 给这两个属性做了人类可读包装:命中对应枚举就返回「属性名: 是/否」的字符串。你在写面板或日志时可以直接复用,不用自己拼多语言文案。外汇与贵金属杠杆高,FIFO 账户里部分平仓顺序受限,锁仓被禁时马丁类策略可能直接失效,实盘前务必在策略测试器用不同账户类型跑一遍。

MQL5 / C++
class="type">bool CAccount::ObjectToStruct(class="type">void)
  {
class=class="str">"cmt">//--- Save integer properties
   ... 
   this.m_struct_obj.server_type=(class="type">int)this.ServerType();
   this.m_struct_obj.fifo_close=this.FIFOClose();
   this.m_struct_obj.hedge_allowed=this.HedgeAllowed();
   ... 
   class=class="str">"cmt">//--- Save the structure to the class="type">uchar array
   ::ResetLastError();
   if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array))
     {
       ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY),(class="type">class="kw">string)::GetLastError());
       class="kw">return false;
     }
   class="kw">return true;
  }
class="type">void CAccount::StructToObject(class="type">void)
  {
class=class="str">"cmt">//--- Save integer properties
   ... 
   this.m_long_prop[ACCOUNT_PROP_FIFO_CLOSE]                = this.m_struct_obj.fifo_close;
   this.m_long_prop[ACCOUNT_PROP_HEDGE_ALLOWED]             = this.m_struct_obj.hedge_allowed;
   ... 
  }
class="type">class="kw">string CAccount::GetPropertyDescription(ENUM_ACCOUNT_PROP_INTEGER class="kw">property)
  {
   class="kw">return
     (
...
       class="kw">property==ACCOUNT_PROP_FIFO_CLOSE     ?  CMessage::Text(MSG_ACC_PROP_FIFO_CLOSE)+": "+
         (this.GetProperty(class="kw">property) ? CMessage::Text(MSG_LIB_TEXT_YES) : CMessage::Text(MSG_LIB_TEXT_NO))  :
       class="kw">property==ACCOUNT_PROP_HEDGE_ALLOWED  ?  CMessage::Text(MSG_ACC_PROP_HEDGE_ALLOWED)+": "+
         (this.GetProperty(class="kw">property) ? CMessage::Text(MSG_LIB_TEXT_YES) : CMessage::Text(MSG_LIB_TEXT_NO))  :
       ""
     );
  }

「订阅延迟与期权属性的对象封装」

CSymbol 类把品种的服务端参数收敛成成员函数,减少重复调用。被高亮的 SubscriptionDelay 对应 SYMBOL_PROP_SUBSCRIPTION_DELAY,在构造时由 SymbolSubscriptionDelay() 一次性写入 m_long_prop 数组,之后直接用对象方法读,不必每次走终端属性接口。 OptionMode 与 OptionRight 返回的是强类型枚举,分别映射 SYMBOL_PROP_OPTION_MODE 和 SYMBOL_PROP_OPTION_RIGHT。这两个值对期权类工具才有意义,普通外汇或贵金属现货品种大概率返回空枚举,调用前最好用 Exist() 确认品种在服务器上真实存在。 订阅延迟字段反映客户端订阅行情后、服务端确认推送的间隔毫秒数。高频刷单或依赖盘口深度(BookDepth)的策略应关注它——延迟偏高时,本地看到的买盘卖盘可能已滞后,外汇与贵金属杠杆交易本身高风险,用滞后数据下单会放大滑点概率。 把这些属性包进对象后,验证方式很直接:在 MT5 自建一个 CSymbol 实例,打印 SubscriptionDelay() 返回值,对比不同品种(比如 XAUUSD 与 EURUSD)的数字差异,就能判断你的 VPS 或经纪商行情通道质量。

MQL5 / C++
class="type">long SymbolSubscriptionDelay(class="type">void) const;

class="type">long SubscriptionDelay(class="type">void) const { class="kw">return this.GetProperty(SYMBOL_PROP_SUBSCRIPTION_DELAY); }

this.m_long_prop[SYMBOL_PROP_SUBSCRIPTION_DELAY] = this.SymbolSubscriptionDelay();
把控件层级诊断交给小布
这些库类的继承改动和小布盯盘的 AIGC 已内置,打开对应品种页即可看到结构差异提示,你只需判断要不要沿用旧窗体基类。

常见问题

窗体已具备移动和操纵函数,面板作为容器只需扩展新属性,避免重复实现坐标与重绘底层逻辑,迭代更快。
小布盯盘内置的 AIGC 可识别库类结构并给出适配建议,但 MQL5 源码需自行编译进 EA,它不代替换文件。
该属性仅对订阅型品种且已加入市场观察的才有效,调用前先确认品种在观察列表,否则会返回 ERR_MARKET_NOT_SELECTED。
对冲禁用的账户该值返回 false,写仓位管理逻辑时应先读它区分单边与对冲模式,降低错单概率。
层级越深遍历重绘开销越大,建议把静态控件拍平到二级内,动态区再用容器,终端卡顿倾向更明显时可做脏矩形优化。