DoEasy 函数库中的图形(第八十一部分):将图形集成到函数库对象之中(基础篇)
📘

DoEasy 函数库中的图形(第八十一部分):将图形集成到函数库对象之中(基础篇)

第 1/3 篇

把图形对象塞进 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 因为由多个图形元素拼成,显示时要分层:先阴影、再主窗、最后按绑定顺序恢复子元素,写复合窗体时得验证这个顺序。

MQL5 / C++
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 过滤掉了。

MQL5 / C++
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 序刷新把对象提到最前。下面这段是原文核心方法,可直接贴进类定义验证。

MQL5 / C++
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 占用,值得在实盘前压测。

MQL5 / C++
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">//+------------------------------------------------------------------+

常见问题

先在图形管理层注册对象类型,再走库对象的创建者溯源接口实例化,避免直接 new 导致归属混乱。
调用对象自带的打印接口把溯源信息打到日志,能看到创建者和类型就说明接入成功。
可以,小布能按你贴的对象代码梳理创建者溯源与打印接口调用链,标出遗漏的挂接步骤。
用集合的打印与挂单筛选接口传过滤条件,只输出符合条件的挂单,不用自己遍历。
确认递归入口对父节点和子节点都调用了同个显隐方法,日志里逐层打印可见状态就能定位断点。