DoEasy 函数库中的图形(第八十一部分):将图形集成到函数库对象之中(基础篇)
把图形对象塞进 DoEasy 对象体系
在 DoEasy 函数库里做图形扩展,核心不是重写绘图逻辑,而是把 MT5 的图形对象管理类挂进已有的库对象树,让后续调用走统一接口。 原文示例发布于 2021 年 10 月 4 日,在 MetaTrader 5 环境下跑通,页面统计显示有 1 543 次访问、5 条反馈,说明这类底层集成对实盘工具开发有稳定需求。 具体做法是在库内新增图形对象管理类,把 OBJ_ 系列对象的创建、修改、删除封装成成员方法;这样在 EA 或指标里调图形,就不用每次都写一堆 ObjectCreate / ObjectSetInteger 散代码。 外汇与贵金属图表上叠加自绘图形波动快、重绘频繁,这类封装能降低脚本卡顿概率,但高频重绘仍属高风险操作,参数没调好会拖慢终端。
◍ 先把柱线对象接进图形管理层
这一节不引入新图形结构,而是把已有图形元素类塞进函数库对象里,给后续移动、校验复合对象铺路。外汇与贵金属 MT5 开发高风险,改底层对象前务必在策略测试器备份 EA。 目前只拿柱线对象(Candle)做集成试验:它原本对图形对象一无所知,现在要给它挂一个图形对象管理类作为成员。该类提供创建交互窗和图形元素的方法,并返回指向新对象的指针,方便后续像普通库对象一样调用。 概念落地分四步:现有绘制方法保留;柱线对象保持原状;新建管理类负责生产图形对象并回传指针;将该管理类做成柱线对象的内置组件。跑通后,任意已有库对象都能借它生成并持有图形对象。 调试完这套机制,我会把同样改造推到全部函数库对象上。届时每个对象多出“视觉”维度,你也多了一个在 MT5 里直接操控图形交互的工具——可开 MT5 自建 Candle 类验证成员挂载是否生效。
「给库对象补上创建者溯源与打印接口」
函数库里每个能自建图形对象的实例,都得知道是谁在内部生成了它。目前只有柱线对象会自建图形,但一旦所有库对象都具备这种能力,新图形就必须能反向引用创建者,才能拉取数据或搭建更复杂的对象关系。 第一步先做最简改造:用对象集合 ID 标记创建者的类型。ID 只能说明属于哪类对象,不够精确到实例,但作为起点够用。同时给 CBaseObj 父类加两个虚方法 Print() 和 PrintShort(),分别打印完整属性和简版属性,参数必须在所有派生类里统一,否则虚拟化会失效。 以 COrder 为例,原本 Print() 只有单个参数,调用写法是 bookseries.Print(true);。改完之后新参数放在原参数前,调用变成 bookseries.Print(false, true);——先传 false 给新增参数,再传 true 给原来的必要参数。类似改动波及 50 多个头文件,包括 Account.mqh、SymbolFX.mqh、BufferCandles.mqh 等。 部分类把标准 Print() 换成了 CMessage 的 ToLog() 来写日志,涉及 EventsCollection.mqh、HistoryCollection.mqh 等。图形侧则在 CGCnvElement 加了两个虚方法控制跨周期显隐,CForm 因为由多个图形元素拼成,显示时要分层:先阴影、再主窗、最后按绑定顺序恢复子元素,写复合窗体时得验证这个顺序。
class=class="str">"cmt">//--- Return an object type class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return this.m_type; } class=class="str">"cmt">//--- Display the description of the object properties in the journal(full_prop=true - all properties, false - supported ones only - implemented in descendant classes) class="kw">virtual class="type">void Print(const class="type">bool full_prop=false,const class="type">bool dash=false) { class="kw">return; } class=class="str">"cmt">//--- Display a class="type">short description of the object in the journal class="kw">virtual class="type">void PrintShort(const class="type">bool dash=false,const class="type">bool symbol=false){ class="kw">return; } class=class="str">"cmt">//--- Constructor class=class="str">"cmt">//--- Return order/position direction class="type">class="kw">string DirectionDescription(class="type">void) const; class=class="str">"cmt">//--- Display the description of the object properties in the journal(full_prop=true - all properties, false - supported ones only - implemented in descendant classes)
订单与深度簿对象的日志打印接口
在 MT5 的自定义订单封装类里,COrder::Print 提供了把挂单/成交单属性甩到专家日志的入口。它的两个布尔参数很实用:full_prop 控制是否连不被当前订单类型支持的属性也打印,dash 则是排版用的分隔线开关。默认两个都是 false,意味着只输出该订单实际支持的字段,日志不会被垃圾行淹没。 Print 内部按整数、双精度、字符串三组属性分块遍历。以整数组为例,循环上界直接用 ORDER_PROP_INTEGER_TOTAL 这个枚举总数,若 !full_prop 且 SupportProperty 返回否就 continue 跳过,所以你看到日志里缺了某行,不一定是 bug,可能是该订单本来就不带这个属性。 另一头 CMBookSeriesCollection::Print 负责把整个市场深度簿序列集合打印出来。它先打一行集合标识,再对 m_list 里的每个 CMBookSeries 指针做空判断后逐个输出。外汇与贵金属市场深度变化极快,这类打印仅适合调试,实盘高频调用会拖慢 EA 执行速度,存在滑点与延迟放大的高风险。 开 MT5 接上你自己的 COrder 派生类,随便下一张测试单,调用 Print(true) 就能看到全量字段;对比 Print() 默认调用,能直观确认哪些属性被 SupportProperty 过滤掉了。
class="kw">virtual class="type">void Print(const class="type">bool full_prop=false,const class="type">bool dash=false); class=class="str">"cmt">//--- Display a class="type">short description of the object in the journal class="kw">virtual class="type">void PrintShort(const class="type">bool dash=false,const class="type">bool symbol=false); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Send order properties to the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void COrder::Print(const class="type">bool full_prop=false,const class="type">bool dash=false) { ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_BEG),": \"",this.StatusDescription(),"\" ============="); class="type">int beg=class="num">0, end=ORDER_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_ORDER_PROP_INTEGER prop=(ENUM_ORDER_PROP_INTEGER)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=ORDER_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_ORDER_PROP_DOUBLE prop=(ENUM_ORDER_PROP_DOUBLE)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=ORDER_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_ORDER_PROP_STRING prop=(ENUM_ORDER_PROP_STRING)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("================== ",CMessage::Text(MSG_LIB_PARAMS_LIST_END),": \"",this.StatusDescription(),"\" ==================\n"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display complete collection description to the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMBookSeriesCollection::Print(const class="type">bool full_prop=false,const class="type">bool dash=false) { ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":"); for(class="type">int i=class="num">0;i<this.m_list.Total();i++) { CMBookSeries *bookseries=this.m_list.At(i); if(bookseries==NULL) class="kw">continue;
◍ 订单簿集合的打印与挂单筛选实现
在订单簿序列集合类里,PrintShort 方法负责向 MT5 专家日志输出精简描述:先打印集合标识文本,再遍历内部链表逐个调用单序列的 PrintShort(true)。这种分层打印结构方便在日志里快速核对当前内存中挂了几组 bookseries。 筛选市场挂单时,GetListMarketPendings 会先校验传入列表的类型必须是 COLLECTION_MARKET_ID,否则写日志并返回 NULL。校验通过后用 CSelect::ByOrderProperty 按 ORDER_PROP_STATUS 等于 ORDER_STATUS_MARKET_PENDING 提取子列表,外汇与贵金属市场的高波动下这类挂单可能在毫秒级频繁增减,需警惕列表为空的概率。 图形对象的置顶靠先隐藏再显示实现:BringToTop 里连续调 SetVisible(false) 再 SetVisible(true),利用 Z 序刷新把对象提到最前。下面这段是原文核心方法,可直接贴进类定义验证。
class="type">void CMBookSeriesCollection::PrintShort(const class="type">bool dash=false,const class="type">bool symbol=false) { ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":"); for(class="type">int i=class="num">0;i<this.m_list.Total();i++) { CMBookSeries *bookseries=this.m_list.At(i); if(bookseries==NULL) class="kw">continue; bookseries.PrintShort(true); } } CArrayObj* CEventsCollection::GetListMarketPendings(CArrayObj* list) { if(list.Type()!=COLLECTION_MARKET_ID) { CMessage::ToLog(DFUN,MSG_LIB_SYS_ERROR_NOT_MARKET_LIST); class="kw">return NULL; } CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_PENDING,EQUAL); class="kw">return list_orders; } class="type">void BringToTop(class="type">void){ CGBaseObj::SetVisible(false); CGBaseObj::SetVisible(true); }
「表单显隐的递归遍历逻辑」
在自绘 UI 里,一个 CForm 往往挂着阴影对象和若干子图元。Show() 与 Hide() 的核心不是只动自己,而是先把 m_shadow_obj 处理了,再循环 m_list_elements 里每一个 CGCnvElement 逐个显隐。 循环从 i=0 跑到 m_list_elements.Total()-1,用 At(i) 取指针,遇到 NULL 直接 continue 跳过——这能避免空指针把整个表单渲染卡死。 Show() 末尾调了一次 CGCnvElement::Update(),而 Hide() 在 Hide() 之后也调了 Update()。也就是说,无论显示还是隐藏,画布都会强制刷新一次,MT5 上你能观察到图表重绘计数 +1。外汇与贵金属图表高频刷新时,这种冗余 Update 可能带来轻微 CPU 占用,值得在实盘前压测。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Methods of simplified access to object properties | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Show the form | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CForm::Show(class="type">void) { class=class="str">"cmt">//--- If the object has a shadow, display it if(this.m_shadow_obj!=NULL) this.m_shadow_obj.Show(); class=class="str">"cmt">//--- Display the main form CGCnvElement::Show(); class=class="str">"cmt">//--- In the loop by all bound graphical objects, for(class="type">int i=class="num">0;i<this.m_list_elements.Total();i++) { class=class="str">"cmt">//--- get the next graphical element CGCnvElement *elment=this.m_list_elements.At(i); if(elment==NULL) class="kw">continue; class=class="str">"cmt">//--- and display it elment.Show(); } class=class="str">"cmt">//--- Update the form CGCnvElement::Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide the form | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CForm::Hide(class="type">void) { class=class="str">"cmt">//--- If the object has a shadow, hide it if(this.m_shadow_obj!=NULL) this.m_shadow_obj.Hide(); class=class="str">"cmt">//--- In the loop by all bound graphical objects, for(class="type">int i=class="num">0;i<this.m_list_elements.Total();i++) { class=class="str">"cmt">//--- get the next graphical element CGCnvElement *elment=this.m_list_elements.At(i); if(elment==NULL) class="kw">continue; class=class="str">"cmt">//--- and hide it elment.Hide(); } class=class="str">"cmt">//--- Hide the main form and update the object CGCnvElement::Hide(); CGCnvElement::Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+