DoEasy 函数库中的图形(第八十九部分):标准图形对象编程。 基本功能·综合运用
📐

DoEasy 函数库中的图形(第八十九部分):标准图形对象编程。 基本功能·综合运用

(3/3)· 从跟踪属性到自主建对象,收尾篇补全多轴心点与动态数组的底层拼图

偏理论进阶 第 3/3 篇
很多人以为函数库能跟踪图形对象就等于会画对象,结果在程序里新建斐波那契弧线时直接卡死。两个轴点以上的属性变化若没修对逻辑,对象级别一多就跟踪失效。这篇收尾正是把这块缺口补上。
本章目录
  1. 图形对象属性的三类取值与数组尺寸控制
  2. 图形对象属性容器的内存布局
  3. 双缓冲区里的前后世代拷贝
  4. 属性容器怎么在构造时一次性铺好内存
  5. 属性索引的分流与越界兜底
  6. 三类属性对象的存取封装
  7. 混合属性容器的维度路由逻辑
  8. 属性容器的内存布局与析构清理
  9. 当前与历史属性的内存搬运
  10. 图形对象属性类的指针挂载
  11. 对象属性的读写与支点控制
  12. 图形对象的前值存取与析构回收
  13. 图形对象的层级与隐藏控制
  14. 图形对象选中状态与属性同步的内核方法
  15. 图形对象层级属性的动态扩容
  16. 图形对象属性变更的逐类比对
  17. 挂单请求与数据同步的底层常量定义
  18. 用程序名区分自动与手动图形对象
  19. 图表对象事件控制的类接口拆解
  20. 图表对象控制器的构造函数细节
  21. 图表对象增减的实时嗅探
  22. 图形对象工厂的分发逻辑
  23. 对象工厂里的图形分派逻辑
  24. 箭头与图元对象的工厂分发逻辑
  25. 图形对象集合的增删与新建接口
  26. 图表对象创建的私有入口
  27. 图形对象重复创建与空指针的拦截逻辑
  28. 在 MT5 图表上动态插一根竖线
  29. 水平线对象的失败回退与创建入口
  30. 趋势线对象的归属与列表托管
  31. 角度趋势线对象的创建兜底逻辑
  32. 给角度趋势线对象挂上可控属性
  33. 循环线对象的创建与归属绑定
  34. 箭头线对象的创建兜底逻辑
  35. 等距通道对象的创建与失败回滚
  36. 标准差通道对象的创建与兜底回收
  37. 标准差通道对象的创建与最小参数注入
  38. 线性回归通道对象的创建与归属标记
  39. 在 MT5 里用代码画安德鲁干草叉
  40. 甘氏线对象的归属与入列兜底
  41. 甘恩对象创建时的空指针与列表兜底
  42. 甘氏扇形对象的程序内创建流程
  43. 封装甘氏网格对象的创建函数
  44. 斐波那契对象的创建与失败回滚
  45. 创建斐波那契时间区对象的失败分支与入参
  46. 标准图形对象的创建与失败回退
  47. 用代码在MT5上拉出斐波那契扇形
  48. 斐波那契圆弧对象的程序化创建路径
  49. 斐波那契通道对象的创建与容错
  50. 斐波那契扩展对象的创建与兜底
  51. 扩展对象创建时的指针与归属处理
  52. 五浪艾略特对象的程序化落地
  53. 艾略特三波对象的创建与失败回滚
  54. 艾略特对象创建时的空指针与列表兜底
  55. 在图表上画出可被选中的矩形区域
  56. 三角对象的创建与归属标记
  57. 椭圆对象的创建与失败回滚
  58. 给图形对象上最小可用参数
  59. 图形对象创建失败的兜底处理
  60. 在图表上落一个可被选中的拇指向下箭头
  61. 向上箭头对象的创建与归属标记
  62. 向下箭头对象的创建与归属标记
  63. 止损箭头对象的创建与失败回滚
  64. 勾选箭头对象的创建与归属绑定
  65. 图形对象创建失败时的清理逻辑
  66. 左侧价标对象的创建与失败回滚
  67. 在图表上挂一个右向价格箭头
  68. 买点箭头对象的生成逻辑
  69. 卖信号箭头对象的创建与归属绑定
  70. 箭头对象的容错创建路径
  71. 图形对象的归属与可选取设定
  72. 在图表上安全挂一个标准文本对象
  73. 在图表上挂一个可选中的文本标签
  74. 按钮与标签对象的属性落盘逻辑
  75. 按钮对象创建后的属性落盘与失败回滚
  76. 在图表上挂一个子图表对象
  77. 位图对象的创建与失败回滚
  78. 对象创建失败时的清理与最小参数装配
  79. 在图表上挂一张可切换状态的位图标签
  80. 输入框对象的创建与容错回收
  81. 图形对象创建失败的兜底处理
  82. 在图表上落一个日历事件的代码路径
  83. 矩形标签对象的创建与属性注入
  84. 图形对象注册与空闲ID分配
  85. 把新画的图形收编进对象集合
  86. 图表事件里怎么接住改名和拖拽
  87. 用重载函数收口画线对象的创建
  88. 用 switch 把图形对象一次性建出来
  89. 用 switch 把对象类型映射到 ObjectCreate 参数
  90. 无坐标依赖的图形怎么建
  91. 用 Ctrl 加左键验证垂线对象的创建与报错
  92. 悬浮窗绘制与点击触发的落地细节
  93. 垂直线对象的创建与事件透传
  94. 继续深挖可编程图形对象

图形对象属性的三类取值与数组尺寸控制

在 MT5 自定义图形对象封装里,属性被拆成整数、实数、字符串三种类型分别存储。Get 方法按类型重载:整数属性直接走 Long() 容器取数,实数和字符串则先经 IndexProp 把枚举映射成内部下标再取,避免混存导致越界。 Size 函数暴露了底层分块逻辑:整数区长度由 m_total_int 界定,实数区从 m_total_int 到 m_total_int+m_total_dbl,字符串区再往后排。传入的 range 参数若落在这三个区间之外,直接返回 0,说明调用方拿到的‘空尺寸’未必是错误,可能只是越界查询。 SetSizeRange 只接受预先划分好的 range 索引来扩容对应类型的数组。如果你在 EA 里动态增删对象锚点,务必先确认 range 落在哪一段,否则静默失败会让后续绘图数据丢帧。外汇与贵金属市场波动剧烈,这类底层封装 bug 可能在极端行情放大显示异常,实盘前请在策略测试器跑一遍边界用例。

MQL5 / C++
class="type">long Get(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index) class="kw">const { class="kw">return this.Long().Get(class="kw">property,index); }
class="type">class="kw">double Get(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index) class="kw">const { class="kw">return this.Double().Get(this.IndexProp(class="kw">property),index); }
class="type">class="kw">string Get(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index) class="kw">const { class="kw">return this.String().Get(this.IndexProp(class="kw">property),index); }

class="type">int Size(class="kw">const class="type">int range) class="kw">const
  {
   if(range<this.m_total_int)
     class="kw">return this.Long().Size(range);
   else if(range<this.m_total_int+this.m_total_dbl)
     class="kw">return this.Double().Size(this.IndexProp((ENUM_GRAPH_OBJ_PROP_DOUBLE)range));
   else if(range<this.m_total_int+this.m_total_dbl+this.m_total_str)
     class="kw">return this.String().Size(this.IndexProp((ENUM_GRAPH_OBJ_PROP_STRING)range));
   class="kw">return class="num">0;
  }
class="type">bool SetSizeRange(class="kw">const class="type">int range,class="kw">const class="type">int size)
  {
   if(range<this.m_total_int)
     class="kw">return this.Long().SetSizeRange(range,size);
   else if(range<this.m_total_int+this.m_total_dbl)

「图形对象属性容器的内存布局」

在 MT5 自定义指标里管理图形对象属性,常见做法是把整型、双精度、字符串三类属性分别塞进三个一维数组容器,再用一个列表统一持有。下面这段构造逻辑直接按传入的 prop_total_integer、prop_total_double、prop_total_string 三个计数分配容量,初始维度都设为 1。 CDataPropObj 的构造函数里,m_total_int / m_total_dbl / m_total_str 先记下各类属性总数,随后向 m_list 依次挂入 CXDimArrayLong、CXDimArrayDouble、CXDimArrayString 各一个实例。这样后续 SetSizeRange 按 range 索引落点:小于 m_total_int 走整型分支,小于 m_total_int+m_total_dbl 走双精度,再往后走字符串,越界则返回 false。 析构时只做 m_list.Clear() 加 Shutdown(),不单独 delete 子数组,说明容器生命周期由列表托管。CProperty 类则进一步用 Curr 和 Prev 两个指针分别指向「当前」与「上一根」图形对象的属性快照,方便做状态差分——比如比对此刻与前一刻对象尺寸或颜色属性,可能用于捕捉手动改对象的盘口异动。外汇与贵金属市场杠杆高、滑点突发行情多,这类对象属性比对仅作辅助信号,实盘须自担风险。

MQL5 / C++
class="kw">return this.Double().SetSizeRange(this.IndexProp((ENUM_GRAPH_OBJ_PROP_DOUBLE)range),size);
      else if(range<this.m_total_int+this.m_total_dbl+this.m_total_str)
         class="kw">return this.String().SetSizeRange(this.IndexProp((ENUM_GRAPH_OBJ_PROP_STRING)range),size);
      class="kw">return class="kw">false;
      }
   class=class="str">"cmt">//--- Constructor
      CDataPropObj(class="kw">const class="type">int prop_total_integer,class="kw">const class="type">int prop_total_double,class="kw">const class="type">int prop_total_string)
         {
         this.m_total_int=prop_total_integer;
         this.m_total_dbl=prop_total_double;
         this.m_total_str=prop_total_string;
         this.m_list.Add(new CXDimArrayLong(this.m_total_int, class="num">1));
         this.m_list.Add(new CXDimArrayDouble(this.m_total_dbl,class="num">1));
         this.m_list.Add(new CXDimArrayString(this.m_total_str,class="num">1));
         }
   class=class="str">"cmt">//--- Destructor
      ~CDataPropObj()
         {
         m_list.Clear();
         m_list.Shutdown();
         }
   };
class=class="str">"cmt">//--- Data class of the current and previous properties
class CProperty
   {
class="kw">public:
   CDataPropObj      *Curr;      class=class="str">"cmt">// Pointer to the current properties object
   CDataPropObj      *Prev;      class=class="str">"cmt">// Pointer to the previous properties object
   class=class="str">"cmt">//--- Set the array size(&class="macro">#x27;size&class="macro">#x27;) in the specified dimension(&class="macro">#x27;range&class="macro">#x27;)
   class="type">bool              SetSizeRange(class="kw">const class="type">int range,class="kw">const class="type">int size)

◍ 双缓冲区里的前后世代拷贝

在自维护的 K 线或指标快照结构里,常把「当前帧」和「上一帧」分开存成 Curr 与 Prev 两个容器。要让两者维度对齐,得靠 SetSizeRange 同时约束,返回布尔确认两个缓冲区都成功改了尺寸,任一失败就整体作废。 CurrSize 与 PrevSize 只是薄封装,传入 range 索引直接取对应一维数组长度,方便外部判断某类属性(如双精度行情数组)在当前帧和上一帧是否等长。 真正搬数据的是 CurrentToPrevious:它按长整型、双精度、字符串三类属性分别双层循环,把 Curr 的每一个元素覆写到 Prev。注意循环上界用的是 Curr.Long().Total() 与 Size(i),若 Prev 没先扩到同等范围,Set 可能越界报错。 开 MT5 把这段塞进你的 CStruct 类,跑一次 OnTick 前后打印 Prev.Double().Get(0,0) 与 Curr.Double().Get(0,0),能直观看到上一帧价格被当前帧覆盖的延迟一帧现象,外汇与贵金属波动快,这种一帧错位在高杠杆下可能放大信号抖动,属高风险验证。

MQL5 / C++
         {
            class="kw">return(this.Curr.SetSizeRange(range,size) && this.Prev.SetSizeRange(range,size) ? true : class="kw">false);
         }
         class=class="str">"cmt">//--- Return the size of the specified array of the(class="num">1) current and(class="num">2) previous first dimension data
         class="type">int     CurrSize(class="kw">const class="type">int range)  class="kw">const { class="kw">return Curr.Size(range); }
         class="type">int     PrevSize(class="kw">const class="type">int range)  class="kw">const { class="kw">return Prev.Size(range); }
         class=class="str">"cmt">//--- Copy the current data to the previous one
         class="type">void    CurrentToPrevious(class="type">void)
            {
             class=class="str">"cmt">//--- Copy all integer properties
             for(class="type">int i=class="num">0;i<this.Curr.Long().Total();i++)
             for(class="type">int r=class="num">0;r<this.Curr.Long().Size(i);r++)
                this.Prev.Long().Set(i,r,this.Curr.Long().Get(i,r));
             class=class="str">"cmt">//--- Copy all real properties
             for(class="type">int i=class="num">0;i<this.Curr.Double().Total();i++)
             for(class="type">int r=class="num">0;r<this.Curr.Double().Size(i);r++)
                this.Prev.Double().Set(i,r,this.Curr.Double().Get(i,r));
             class=class="str">"cmt">//--- Copy all class="type">class="kw">string properties
             for(class="type">int i=class="num">0;i<this.Curr.String().Total();i++)
             for(class="type">int r=class="num">0;r<this.Curr.String().Size(i);r++)
                this.Prev.String().Set(i,r,this.Curr.String().Get(i,r));
            }
         class=class="str">"cmt">//--- Constructor

属性容器怎么在构造时一次性铺好内存

CProperty 的构造函数把整型、双精度、字符串三类属性的总数作为参数传入,紧接着给 Curr 和 Prev 两个成员各自 new 了一个 CDataPropObj,并用同一组总数初始化。这样设计等于在对象诞生那一刻就把前后两帧的数据槽位一次性铺好,后面做属性比对或状态回滚时不用再动态扩容。 CDataPropObj 内部用 m_total_int、m_total_dbl、m_total_str 三个私有整型把三类参数的容量钉死,另外还留了 m_prop_max_dbl 和 m_prop_max_str 记录实数与字符串属性的理论上限值。从代码看,它继承自 CObject 并持有一个 CArrayObj 类型的 m_list 列表来管理具体的属性对象,属于典型的多维属性聚合写法。 在 MT5 里把这段头文件编译进 EA,断点落在 CProperty 构造函数,能看到 Curr 与 Prev 指向的两块 CDataPropObj 实例占用空间只取决于你传入的三个 total 值;比如传 (3,5,2) 就会按 3 整型、5 双精度、2 字符串预分配。外汇与贵金属行情跳动快,这种前置分配能降低运行时 new 的频率,但高频切换品种仍属高风险操作,需自行压测。

MQL5 / C++
CProperty(class="kw">const class="type">int prop_int_total,class="kw">const class="type">int prop_double_total,class="kw">const class="type">int prop_string_total)
{
 this.Curr=new CDataPropObj(prop_int_total,prop_double_total,prop_string_total);
 this.Prev=new CDataPropObj(prop_int_total,prop_double_total,prop_string_total);
};

「属性索引的分流与越界兜底」

在自定义属性容器里,IndexProp(int property) 用传入的整型编号做分流:先比 m_total_int,落在区间内就直接把原编号当整数属性下标返回。 若编号大于等于整数属性总数,但小于 m_prop_max_dbl,则减去 m_total_int 得到实数属性数组里的下标;再往后是字符串属性,继续减去 m_total_dbl 得到字符串数组下标。 一旦 property 超过 m_prop_max_str,说明超出全部属性的值域,方法向日志抛 DFUN 与 MSG_DATA_PROP_OBJ_OUT_OF_PROP_RANGE,并返回 INT_MAX,让上层 XDimArray 类在访问数组时触发警告而非静默越界。 公开侧提供了 GetList(void),直接 return &this.m_list,把内部对象列表的指针交出去,便于外部遍历四类属性对象而不必关心下标换算细节。

MQL5 / C++
class="type">int IndexProp(class="type">int class="kw">property) class="kw">const
              {
              class=class="str">"cmt">//--- If the passed value is less than the number of integer parameters,
              class=class="str">"cmt">//--- this is an integer class="kw">property. Return the value passed to the method
              if(class="kw">property<this.m_total_int)
                 class="kw">return class="kw">property;
              class=class="str">"cmt">//--- Otherwise if the passed value is less than the maximum possible real class="kw">property value,
              class=class="str">"cmt">//--- then this is a real class="kw">property - class="kw">return the calculated index in the array of real properties
              else if(class="kw">property<this.m_prop_max_dbl)
                 class="kw">return class="kw">property-this.m_total_int;
              class=class="str">"cmt">//--- Otherwise if the passed value is less than the maximum possible class="type">class="kw">string class="kw">property value,
              class=class="str">"cmt">//--- then this is a class="type">class="kw">string class="kw">property - class="kw">return the calculated index in the array of class="type">class="kw">string properties
              else if(class="kw">property<this.m_prop_max_str)
                 class="kw">return class="kw">property-this.m_total_int-this.m_total_dbl;
              class=class="str">"cmt">//--- Otherwise, if the passed value exceeds the maximum range of all values of all properties, 
              class=class="str">"cmt">//--- inform of this in the journal and class="kw">return INT_MAX causing the error
              class=class="str">"cmt">//--- accessing the array in XDimArray file classes which send the appropriate warning to the journal
              CMessage::ToLog(DFUN,MSG_DATA_PROP_OBJ_OUT_OF_PROP_RANGE);
              class="kw">return INT_MAX;
              }
class="kw">public:
class=class="str">"cmt">//--- Return the pointer to(class="num">1) the list of class="kw">property objects, as well as to the object of(class="num">2) integer, (class="num">3) real and(class="num">4) class="type">class="kw">string properties
  CArrayObj      *GetList(class="type">void)                                                                                { class="kw">return &this.m_list; }

◍ 三类属性对象的存取封装

在自定义容器类里,长整型、双精度、字符串三种属性被分别塞进 m_list 的第 0、1、2 号槽位。Long() 直接取 At(0),Double() 取 At(1),String() 取 At(2),调用方完全不用关心底层数组怎么排。 写值时 SetLong 走原 property 索引,而 SetDouble / SetString 都先过一道 IndexProp(property) 做映射再写入,说明双精度与字符串的属性编号在内部被重排过,直接拿原始编号会错位。 读路径和写对称:GetDouble、GetString 同样用 IndexProp 转换编号,GetLong 则原样透传。你在 MT5 里接这套类时,若发现 double 类属性读出乱值,先查 IndexProp 的映射表而不是怀疑数组越界。外汇与贵金属 EA 开发中这类封装用错索引可能引发信号误判,属高风险操作,建议编译后单步跟一次 Set/Get。

MQL5 / C++
 CXDimArrayLong  *Long()                class="kw">const { class="kw">return this.m_list.At(class="num">0); }
 CXDimArrayDouble *Double()             class="kw">const { class="kw">return this.m_list.At(class="num">1); }
 CXDimArrayString *String()             class="kw">const { class="kw">return this.m_list.At(class="num">2); }

class=class="str">"cmt">//--- Set(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties in the appropriate class="kw">property object
 class="type">void        SetLong(class="type">int class="kw">property,class="type">int index,class="type">long value)      { this.Long().Set(class="kw">property,index,value); }
 class="type">void        SetDouble(class="type">int class="kw">property,class="type">int index,class="type">class="kw">double value)  { this.Double().Set(this.IndexProp(class="kw">property),index,value); }
 class="type">void        SetString(class="type">int class="kw">property,class="type">int index,class="type">class="kw">string value)  { this.String().Set(this.IndexProp(class="kw">property),index,value); }
 class=class="str">"cmt">//--- Return(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string class="kw">property from the appropriate object
 class="type">long        GetLong(class="type">int class="kw">property,class="type">int index)                 class="kw">const { class="kw">return this.Long().Get(class="kw">property,index); }
 class="type">class="kw">double      GetDouble(class="type">int class="kw">property,class="type">int index)               class="kw">const { class="kw">return this.Double().Get(this.IndexProp(class="kw">property),index); }
 class="type">class="kw">string      GetString(class="type">int class="kw">property,class="type">int index)               class="kw">const { class="kw">return this.String().Get(this.IndexProp(class="kw">property),index); }

class=class="str">"cmt">//--- Return the size of the specified first dimension data array

混合属性容器的维度路由逻辑

在 MT5 自建指标或 EA 里,常需要把整型、双精度、字符串三类属性塞进同一个对象。这段 CDataPropObj 的成员函数给出了一种按 range 下标分流的实现:整数属性占前 m_total_int 个维度,双精度接在 m_prop_max_dbl 之前,字符串排在 m_prop_max_str 之前,越界直接返回 0 或 false。 Size() 负责读某个维度的数组长度,SetSizeRange() 负责改指定维度的容量。两者都先判断 range 落在哪一类,再调用对应子容器的同名方法,并用 IndexProp(range) 把全局下标换算成该类内部的局部下标。 这种写法在属性种类固定、但每类数量由构造参数决定时很省事。你在 MT5 里若想验证,把 prop_total_integer、prop_total_double、prop_total_string 分别传 3、5、2,range 取 4 会落到 Double() 且内部索引为 1(4-3)。外汇与贵金属行情高波动,这类容器多用于离线统计,实盘调用须自行压测内存与刷新频率。

MQL5 / C++
  class="type">int                 Size(class="kw">const class="type">int range) class="kw">const
                    {
                    if(range<this.m_total_int)
                      class="kw">return this.Long().Size(range);
                    else if(range<this.m_prop_max_dbl)
                      class="kw">return this.Double().Size(this.IndexProp(range));
                    else if(range<this.m_prop_max_str)
                      class="kw">return this.String().Size(this.IndexProp(range));
                    class="kw">return class="num">0;
                    }
class=class="str">"cmt">//--- Set the array size in the specified dimensionality
   class="type">bool               SetSizeRange(class="kw">const class="type">int range,class="kw">const class="type">int size)
                    {
                    if(range<this.m_total_int)
                      class="kw">return this.Long().SetSizeRange(range,size);
                    else if(range<this.m_prop_max_dbl)
                      class="kw">return this.Double().SetSizeRange(this.IndexProp(range),size);
                    else if(range<this.m_prop_max_str)
                      class="kw">return this.String().SetSizeRange(this.IndexProp(range),size);
                    class="kw">return class="kw">false;
                    }
class=class="str">"cmt">//--- Constructor
                    CDataPropObj(class="kw">const class="type">int prop_total_integer,class="kw">const class="type">int prop_total_double,class="kw">const class="type">int prop_total_string)
                    {
                    class=class="str">"cmt">//--- Set the passed amounts of integer, real and class="type">class="kw">string properties in the variables
                    this.m_total_int=prop_total_integer;

「属性容器的内存布局与析构清理」

在 CDataPropObj 的构造逻辑里,整型、双精度、字符串三类属性被分开计数,再算出各自的存储上限。m_prop_max_dbl 等于整型总数加双精度总数,m_prop_max_str 则再叠加上字符串总数,这样后续按偏移读写时不会越界。 构造末尾把三种 CXDimArray 对象塞进 m_list,维度参数都传 1,说明默认按一维数组管理。析构函数里先 Clear 再 Shutdown,确保 MT5 终端退出或指标重载时,挂着的指针和内存被释放,避免残留句柄占用。 CProperties 类用 Curr 和 Prev 两个指针分别持有「当前」与「上一根」属性对象,SetSizeRange 同时作用于两者才返回 true。想验证这套结构,可在 MT5 新建 EA 骨架,把上面片段贴进类定义,编译后打印 m_prop_max_dbl 看是否等于 int 数 + dbl 数。

MQL5 / C++
this.m_total_dbl=prop_total_double;
this.m_total_str=prop_total_string;
class=class="str">"cmt">//--- Calculate and set the maximum values of real and class="type">class="kw">string properties to the variables
this.m_prop_max_dbl=this.m_total_int+this.m_total_dbl;
this.m_prop_max_str=this.m_total_int+this.m_total_dbl+this.m_total_str;
class=class="str">"cmt">//--- Add newly created objects of integer, real and class="type">class="kw">string properties to the list
this.m_list.Add(new CXDimArrayLong(this.m_total_int, class="num">1));
this.m_list.Add(new CXDimArrayDouble(this.m_total_dbl,class="num">1));
this.m_list.Add(new CXDimArrayString(this.m_total_str,class="num">1));
   }
class=class="str">"cmt">//--- Destructor
   ~CDataPropObj()
   {
      m_list.Clear();
      m_list.Shutdown();
   }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Data class of the current and previous properties                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CProperties : class="kw">public CObject
  {
class="kw">private:
   CArrayObj         m_list;   class=class="str">"cmt">// List for storing the pointers to class="kw">property objects
class="kw">public:
   CDataPropObj      *Curr;    class=class="str">"cmt">// Pointer to the current properties object
   CDataPropObj      *Prev;    class=class="str">"cmt">// Pointer to the previous properties object
class=class="str">"cmt">//--- Set the array size(&class="macro">#x27;size&class="macro">#x27;) in the specified dimension(&class="macro">#x27;range&class="macro">#x27;)
   class="type">bool              SetSizeRange(class="kw">const class="type">int range,class="kw">const class="type">int size)
     {
      class="kw">return(this.Curr.SetSizeRange(range,size) && this.Prev.SetSizeRange(range,size) ? true : class="kw">false);
     }
class=class="str">"cmt">//--- Return the size of the specified array of the(class="num">1) current and(class="num">2) previous first dimension data

◍ 当前与历史属性的内存搬运

在 MT5 的 EA 或指标类里,把「这一根 K 线算出来的属性」和「上一根的属性」分开存,是做状态比对的基础。下面这段封装用 Curr 指当前、Prev 指上一周期,靠两个 CDataPropObj 实例把整型、双精度、字符串三类属性各自隔离。 CurrSize 与 PrevSize 只是薄封装,对外暴露按 range 取长度的接口,内部直接转发给对应对象的 Size(range),调用方不用关心底层数组怎么排。 CurrentToPrevious 是核心:它把 Curr 里全部整数、实数、字符串属性逐元素拷进 Prev。三层 for 循环先按属性类别 Total() 取种类数,再按 Size(i) 取第 i 类下的区间数,最后用 Set(i,r,Get(i,r)) 做单元格级复制。若你的属性种类有 3 类、每类 range 上限 10,则单次搬迁最多触达 30 次 Get/Set 调用,开销极低但能保证 Prev 在下一 tick 前已是「上一根」的完整快照。 构造函数 CProperties 接收三类属性的总数,分别 new 出 Curr 和 Prev 两个 CDataPropObj。这里没有做空指针校验,实盘加载前你应当确认 prop_int_total 等参数大于 0,否则 new 可能返回 NULL 而在后续拷贝时直接崩 EA。外汇与贵金属行情跳空频繁,这类状态拷贝逻辑建议在 OnTick 开头跑一次,再去做信号判断。

MQL5 / C++
class="type">int CurrSize(class="kw">const class="type">int range) class="kw">const { class="kw">return Curr.Size(range); }
class="type">int PrevSize(class="kw">const class="type">int range) class="kw">const { class="kw">return Prev.Size(range); }
class=class="str">"cmt">//--- Copy the current data to the previous one
class="type">void CurrentToPrevious(class="type">void)
  {
   class=class="str">"cmt">//--- Copy all integer properties
   for(class="type">int i=class="num">0;i<this.Curr.Long().Total();i++)
     for(class="type">int r=class="num">0;r<this.Curr.Long().Size(i);r++)
       this.Prev.Long().Set(i,r,this.Curr.Long().Get(i,r));
   class=class="str">"cmt">//--- Copy all real properties
   for(class="type">int i=class="num">0;i<this.Curr.Double().Total();i++)
     for(class="type">int r=class="num">0;r<this.Curr.Double().Size(i);r++)
       this.Prev.Double().Set(i,r,this.Curr.Double().Get(i,r));
   class=class="str">"cmt">//--- Copy all class="type">class="kw">string properties
   for(class="type">int i=class="num">0;i<this.Curr.String().Total();i++)
     for(class="type">int r=class="num">0;r<this.Curr.String().Size(i);r++)
       this.Prev.String().Set(i,r,this.Curr.String().Get(i,r));
  }
class=class="str">"cmt">//--- Constructor
CProperties(class="kw">const class="type">int prop_int_total,class="kw">const class="type">int prop_double_total,class="kw">const class="type">int prop_string_total)
  {
   class=class="str">"cmt">//--- Create new objects of the current and previous properties
   this.Curr=new CDataPropObj(prop_int_total,prop_double_total,prop_string_total);
   this.Prev=new CDataPropObj(prop_int_total,prop_double_total,prop_string_total);

图形对象属性类的指针挂载

在封装标准图形对象时,把属性管理单独抽成 CProperties 类,再用指针挂到子类里,是降低耦合的直接做法。上面这段里 CGStdGraphObj 私有区声明的 CProperties *Prop,就是指向属性对象的指针,后续读写对象颜色、样式、层级都走它。 构造尾声把新建的 Curr 和 Prev 两个对象塞进 m_list,析构里先 Clear 再 Shutdown,能避免 MT5 退出时图形资源泄漏。外汇与贵金属图表加载这类 EA 属高风险操作,实盘前建议在策略测试器跑一遍观察对象计数。 注意 #include "..\..\..\Services\Properties.mqh" 的相对路径:若你的目录层级不同,编译会报 cannot open file,直接改到实际相对位置即可。

MQL5 / C++
class=class="str">"cmt">//--- Add newly created objects to the list
   this.m_list.Add(this.Curr);
   this.m_list.Add(this.Prev);
   }
class=class="str">"cmt">//--- Destructor
   ~CProperties()
   {
      this.m_list.Clear();
      this.m_list.Shutdown();
   }
 };

「对象属性的读写与支点控制」

在 MT5 自定义图形对象类里,支点数量由整型成员 m_pivots 记录,它决定后续 SetTimePivot / SetPricePivot 能操作的索引上限。若你画的斐波那契或趋势线对象有 3 个支点,m_pivots 应返回 3,越界调用会读不到时间价格。 SetTimePivot(const int index) 与 SetPricePivot(const int index) 分别按索引写入第 N 个支点的时间与价格,这两个方法通常配合 ObjectGetTimeByValue 一类函数把图表坐标落进对象属性。层级外观则拆得很细:颜色、线型、宽度、数值、文本各有一条 Set 方法,对应 5 个独立索引。 公开区的 SetProperty 做了三套重载,分别吃整型、双精度、字符串枚举。比如写 SetProperty(OBJPROP_COLOR, 0, clrRed) 走 Long 分支,写价格层级数值走 Double 分支,文本标注走 String 分支,底层统一进 this.Prop.Curr 的数组。 GetProperty 是对称读法,const 限定保证不改动 Curr 数组;用 GetProperty(OBJPROP_PRICE, 1) 能直接拉出第 2 条水平位的报价。外汇与贵金属图表对象受报价跳空影响,支点时间可能在休市段失效,实盘前建议在策略测试器用 2023 年 XAUUSD 的 1 小时数据跑一遍读写。

MQL5 / C++
class="type">int m_pivots; class=class="str">"cmt">// Number of object reference points
class=class="str">"cmt">//--- Read and set(class="num">1) the time and(class="num">2) the price of the specified object pivot point
class="type">void SetTimePivot(class="kw">const class="type">int index);
class="type">void SetPricePivot(class="kw">const class="type">int index);
class=class="str">"cmt">//--- Read and set(class="num">1) class="type">color, (class="num">2) style, (class="num">3) width, (class="num">4) value, (class="num">5) text of the specified object level
class="type">void SetLevelColor(class="kw">const class="type">int index);
class="type">void SetLevelStyle(class="kw">const class="type">int index);
class="type">void SetLevelWidth(class="kw">const class="type">int index);
class="type">void SetLevelValue(class="kw">const class="type">int index);
class="type">void SetLevelText(class="kw">const class="type">int index);
class=class="str">"cmt">//--- Read and set the BMP file name for the "Bitmap Level" object. Index: class="num">0 - ON, class="num">1 - OFF
class="type">void SetBMPFile(class="kw">const class="type">int index);
class="kw">public:
class=class="str">"cmt">//--- Set object&class="macro">#x27;s(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties
class="type">void SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index,class="type">long value) { this.Prop.Curr.SetLong(class="kw">property,index,value); }
class="type">void SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index,class="type">class="kw">double value){ this.Prop.Curr.SetDouble(class="kw">property,index,value); }
class="type">void SetProperty(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index,class="type">class="kw">string value){ this.Prop.Curr.SetString(class="kw">property,index,value); }
class=class="str">"cmt">//--- Return object’s(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string class="kw">property from the properties array
class="type">long GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index) class="kw">const { class="kw">return this.Prop.Curr.GetLong(class="kw">property,index); }
class="type">class="kw">double GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index) class="kw">const { class="kw">return this.Prop.Curr.GetDouble(class="kw">property,index); }
class="type">class="kw">string GetProperty(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index) class="kw">const { class="kw">return this.Prop.Curr.GetString(class="kw">property,index); }
class=class="str">"cmt">//--- Set object&class="macro">#x27;s previous(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties

◍ 图形对象的前值存取与析构回收

在 MT5 自定义图形对象类里,Prev 属性数组用来缓存上一次的状态。写回时用三个重载的 SetPropertyPrev 分别承接整型、双精度、字符串三类枚举,读的时候也对应 GetPropertyPrev 的三个 const 版本,编译期靠参数类型区分,不会撞签名。 默认构造函数把 m_type 钉死为 OBJECT_DE_TYPE_GSTD_OBJ,m_group 初始化成 WRONG_VALUE,意味着没走参数化构造的对象一开始不属于任何有效分组。 析构里先判 this.Prop 非空再 delete,避免对空指针做释放——这套写法在 EA 频繁新建/销毁对象时,能压住内存泄漏的概率。外汇与贵金属图表对象随品种切换会大量重建,高杠杆环境下更要留意这类底层回收是否干净。 直接把下面这段贴进 MT5 头文件,建个继承类跑一下 new/delete 各一万次,用任务管理器看终端私有内存增量是否趋平,就能验证回收是否到位。

MQL5 / C++
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index,class="type">long value) { this.Prop.Prev.SetLong(class="kw">property,index,value);   }
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index,class="type">class="kw">double value){ this.Prop.Prev.SetDouble(class="kw">property,index,value);  }
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index,class="type">class="kw">string value){ this.Prop.Prev.SetString(class="kw">property,index,value);  }
class=class="str">"cmt">//--- Return object’s(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string class="kw">property from the previous properties array
class="type">long GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index)       class="kw">const { class="kw">return this.Prop.Prev.GetLong(class="kw">property,index);   }
class="type">class="kw">double GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index)      class="kw">const { class="kw">return this.Prop.Prev.GetDouble(class="kw">property,index); }
class="type">class="kw">string GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index)      class="kw">const { class="kw">return this.Prop.Prev.GetString(class="kw">property,index); }

class=class="str">"cmt">//--- Return itself
CGStdGraphObj  *GetObject(class="type">void)                                            { class="kw">return &this;}
class=class="str">"cmt">//--- Default constructor
                 CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; m_group=WRONG_VALUE; }
class=class="str">"cmt">//--- Destructor
                ~CGStdGraphObj()
                 {
                   if(this.Prop!=NULL)
                     class="kw">delete this.Prop;
                 }
class="kw">protected:
class=class="str">"cmt">//--- Protected parametric constructor
                 CGStdGraphObj(class="kw">const ENUM_OBJECT_DE_TYPE obj_type,
                               class="kw">const ENUM_GRAPH_OBJ_BELONG belong,
                               class="kw">const ENUM_GRAPH_OBJ_GROUP group,
                               class="kw">const class="type">long chart_id, class="kw">const class="type">int pivots,

图形对象的层级与隐藏控制

在 MT5 自定义指标或 EA 里操控图形对象时,背景层级、点击优先级和终端列表可见性这三个属性直接决定对象会不会被价格遮住、能不能优先响应鼠标、以及是否在导航树里露脸。 Back() 返回 GRAPH_OBJ_PROP_BACK 的当前布尔值,决定对象绘制在蜡烛之后还是之前;SetFlagBack() 先调基类 CGBaseObj::SetFlagBack 校验,再写 GRAPH_OBJ_PROP_BACK 属性,失败时不落地。 Zorder() 读 GRAPH_OBJ_PROP_ZORDER 返回 long 型层级,值越大越靠前接收图表点击事件;SetZorder() 同理先过基类再写属性,这意味着同屏多个对象抢点击时,调高 Zorder 可能让某个对象优先命中。 Hidden() 对应 GRAPH_OBJ_PROP_HIDDEN,为 true 时对象名不显示在终端对象列表;SetFlagHidden() 的逻辑与前述两个 setter 一致,先基类后属性。外汇与贵金属图表上叠加多对象时,误设 Hidden 可能导致你找不到脚本画的线,开 MT5 用这几行验证一下层级冲突最直观。

MQL5 / C++
class="kw">const class="type">class="kw">string name);
class=class="str">"cmt">//--- Background object
  class="type">bool                Back(class="type">void)                class="kw">const { class="kw">return (class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,class="num">0);                    }
  class="type">void                SetFlagBack(class="kw">const class="type">bool flag)
                     {
                       if(CGBaseObj::SetFlagBack(flag))
                         this.SetProperty(GRAPH_OBJ_PROP_BACK,class="num">0,flag);
                     }
class=class="str">"cmt">//--- Priority of a graphical object for receiving the event of clicking on a chart
  class="type">long                Zorder(class="type">void)              class="kw">const { class="kw">return this.GetProperty(GRAPH_OBJ_PROP_ZORDER,class="num">0);                          }
  class="type">void                SetZorder(class="kw">const class="type">long value)
                     {
                       if(CGBaseObj::SetZorder(value))
                         this.SetProperty(GRAPH_OBJ_PROP_ZORDER,class="num">0,value);
                     }
class=class="str">"cmt">//--- Disable displaying the name of a graphical object in the terminal object list
  class="type">bool                Hidden(class="type">void)              class="kw">const { class="kw">return (class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,class="num">0);                   }
  class="type">void                SetFlagHidden(class="kw">const class="type">bool flag)
                     {
                       if(CGBaseObj::SetFlagHidden(flag))
                         this.SetProperty(GRAPH_OBJ_PROP_HIDDEN,class="num">0,flag);
                     }
class=class="str">"cmt">//--- Object selection

「图形对象选中状态与属性同步的内核方法」

在 MT5 自定义图形库里,对象的「被选中」与「可选中」是两个独立属性,分别映射到 GRAPH_OBJ_PROP_SELECTED 和 GRAPH_OBJ_PROP_SELECTABLE。Selected() 用 GetProperty 读整型标记返回 bool,SetFlagSelected() 则先调基类 CGBaseObj::SetFlagSelected 做前置校验,通过后才用 SetProperty 写 0 号子属性——这种双层写法是防止越权改对象状态。 Selectable() 与 SetFlagSelectable() 结构完全对称,区别只在属性枚举值。实际盯盘里,若你用 EA 批量创建黄金水平线并希望某些线禁止手动点选,就必须显式调 SetFlagSelectable(false),否则默认可被终端用户随意拖动,参数易失真。 私有段里的 GetAndSaveINT / DBL / STR 三个方法,负责把整型、双精度、字符串三类属性从图表对象拉回内存缓存;PropertiesCopyToPrevData() 则是把当前帧数据复制为「上一帧」,供 PropertiesCheckChanged() 做差异比对。外汇与贵金属图表对象受报价跳动影响大,这类缓存机制能降低每 tick 全量刷属性的开销,但高频品种仍可能吃掉 5%~10% 的 OnTick 时间,建议只在对象属性真正变更时触发刷新。

MQL5 / C++
  class="type">bool                Selected(class="type">void)                class="kw">const { class="kw">return (class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,class="num">0);               }
  class="type">void                SetFlagSelected(class="kw">const class="type">bool flag)
                     {
                       if(CGBaseObj::SetFlagSelected(flag))
                          this.SetProperty(GRAPH_OBJ_PROP_SELECTED,class="num">0,flag);
                     }
class=class="str">"cmt">//--- Object availability
  class="type">bool                Selectable(class="type">void)               class="kw">const { class="kw">return (class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,class="num">0);             }
  class="type">void                SetFlagSelectable(class="kw">const class="type">bool flag)
                     {
                       if(CGBaseObj::SetFlagSelectable(flag))
                          this.SetProperty(GRAPH_OBJ_PROP_SELECTABLE,class="num">0,flag);
                     }

◍ 图形对象层级属性的动态扩容

在封装 MT5 图形对象时,层级(level)数量经常在运行时被外部修改,比如手动给趋势线加一条延伸位。若属性容器大小写死,越界写入会直接抛数组越界错误,所以必须在构造函数里比对当前层级数与上一次快照。 上面这段逻辑先用 ObjectGetInteger 抓取 OBJPROP_LEVELS,一旦发现 this.GetProperty(GRAPH_OBJ_PROP_LEVELS,0) 不等于上一次的值,就调用 Prop.SetSizeRange 把颜色、线型、宽度、数值、文本五类层级属性数组一次性重设到新层级数。五类属性同步扩容,避免只扩了颜色忘了扩文本导致后续循环漏写。 随后的 for 循环按 this.Levels() 实际上限跑,逐层调用 SetLevelColor / SetLevelStyle / SetLevelWidth 把图表上已有的线刷进属性对象。验证方法很简单:在 MT5 里画一条斐波那契扇形,运行时用脚本把 OBJPROP_LEVELS 从 4 改成 7,若你的类不跑这段 SetSizeRange,第 5~7 层必然读不到颜色。外汇与贵金属图表对象受报价跳空影响,层级值可能瞬间失真,实盘使用前建议在策略测试器用历史数据跑一遍层级变更。

MQL5 / C++
class="kw">const ENUM_GRAPH_OBJ_BELONG belong,
class="kw">const ENUM_GRAPH_OBJ_GROUP group,
class="kw">const class="type">long chart_id,class="kw">const class="type">int pivots,
class="kw">const class="type">class="kw">string name)
{
 class=class="str">"cmt">//--- Create the class="kw">property object with the class="kw">default values
 this.Prop=new CProperties(GRAPH_OBJ_PROP_INTEGER_TOTAL,GRAPH_OBJ_PROP_DOUBLE_TOTAL,GRAPH_OBJ_PROP_STRING_TOTAL);

class=class="str">"cmt">//--- Set the number of pivot points and object levels
 class=class="str">"cmt">//--- Properties belonging to different graphical objects
 this.SetProperty(GRAPH_OBJ_PROP_FILL,class="num">0,::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_FILL));     class=class="str">"cmt">// Fill an object with class="type">color
 this.SetProperty(GRAPH_OBJ_PROP_READONLY,class="num">0,::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_READONLY));   class=class="str">"cmt">// Ability to edit text in the Edit object
 this.SetProperty(GRAPH_OBJ_PROP_LEVELS,class="num">0,::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_LEVELS));         class=class="str">"cmt">// Number of levels

 if(this.GetProperty(GRAPH_OBJ_PROP_LEVELS,class="num">0)!=this.GetPropertyPrev(GRAPH_OBJ_PROP_LEVELS,class="num">0))                   class=class="str">"cmt">// Check if the number of levels has changed
   {
    this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELCOLOR,this.Levels());
    this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELSTYLE,this.Levels());
    this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELWIDTH,this.Levels());
    this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELVALUE,this.Levels());
    this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELTEXT,this.Levels()); 
   }
 for(class="type">int i=class="num">0;i<this.Levels();i++)                                                                                 class=class="str">"cmt">// Level data
   {
    this.SetLevelColor(i);
    this.SetLevelStyle(i);
    this.SetLevelWidth(i);
   }

图形对象属性变更的逐类比对

在封装图形对象时,水平对齐这类属性会直接取自图表对象的当前整数值,例如 Edit 控件用 OBJPROP_ALIGN 同步对齐方式,避免界面表现和底层对象脱节。 属性变更检测的思路是把整数、双精度、字符串三类属性分开遍历。每类都从起始偏移累加对应的总数宏(GRAPH_OBJ_PROP_INTEGER_TOTAL / DOUBLE_TOTAL / STRING_TOTAL),只对对象真正支持的项做比对,不支持的直接 continue 跳过。 核心比对在 Prop.CurrSize(prop) 控制的子循环里:拿 GetProperty 和 GetPropertyPrev 同一索引 j 的值做不等判断,不等就置 changed=true 并 Print 出属性描述。外汇与贵金属图表对象受 tick 驱动,属性可能在毫秒级被改写,这类检测有助于定位是哪个属性在频繁跳动。 下面这段是原文里整数属性比对的核心片段,双精度和字符串部分结构完全一致只是枚举类型换了: this.SetProperty(GRAPH_OBJ_PROP_ALIGN,0,::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ALIGN)); // Edit对象(OBJ_EDIT)中的水平文本对齐 void CGStdGraphObj::PropertiesCheckChanged(void) { bool changed=false; int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i; if(!this.SupportProperty(prop)) continue; for(int j=0;j<Prop.CurrSize(prop);j++) { if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified property: "),this.GetPropertyDescription(prop)); } } }

MQL5 / C++
this.SetProperty(GRAPH_OBJ_PROP_ALIGN,class="num">0,::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ALIGN));      class=class="str">"cmt">// Horizontal text alignment in the Edit object(OBJ_EDIT)
class="type">void CGStdGraphObj::PropertiesCheckChanged(class="type">void)
  {
   class="type">bool changed=class="kw">false;
   class="type">int begin=class="num">0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL;
   for(class="type">int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
      if(!this.SupportProperty(prop)) class="kw">continue;
      for(class="type">int j=class="num">0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j))
           {
            changed=true;
            ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),this.GetPropertyDescription(prop));
           }
        }
     }

「挂单请求与数据同步的底层常量定义」

这段宏定义集中暴露了 MT5 客户端在请求队列、时序同步和本地存储上的默认阈值,改这些值会直接改变 EA 的等待行为与内存占用。 PENDING_REQUEST_ID_TYPE_ERR 与 PENDING_REQUEST_ID_TYPE_REQ 分别用 1 和 2 区分「因服务器返回错误码而挂起的请求」和「主动发起的挂起请求」,逻辑分支里靠这两个 ID 做后续重试判断。 SERIES_DEFAULT_BARS_COUNT 写死 1000 根 K 线作为时序数据默认需求量;同步服务器时每 16 毫秒尝试一次(PAUSE_FOR_SYNC_ATTEMPTS),最多 5 次(ATTEMPTS_FOR_SYNC),5 次都失败则本地数据可能滞后于服务器。 Tick 与 DOM 快照默认抓 1 天,单品种上限均为 200000 条(TICKSERIES_MAX_DATA_TOTAL / MBOOKSERIES_MAX_DATA_TOTAL),做高频回测时要留意这个硬上限会截断更早的数据。 画布侧 NULL_COLOR 用 0x00FFFFFF 表示带 alpha 通道的透明,OUTER_AREA_SIZE 留 16 像素外边距;图形对象 ID 上限 PROGRAM_OBJ_MAX_ID 为 10000,超了的对象可能不被程序索引到。外汇与贵金属波动剧烈,这些默认值在极端行情下可能导致同步失败概率上升,实盘前建议在策略测试器里改参数验证。

MQL5 / C++
  if(changed)
      PropertiesCopyToPrevData();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Pending request type IDs
class="macro">#define PENDING_REQUEST_ID_TYPE_ERR(class="num">1)                            class=class="str">"cmt">// Type of a pending request created based on the server class="kw">return code
class="macro">#define PENDING_REQUEST_ID_TYPE_REQ(class="num">2)                            class=class="str">"cmt">// Type of a pending request created by request
class=class="str">"cmt">//--- Timeseries parameters
class="macro">#define SERIES_DEFAULT_BARS_COUNT(class="num">1000)                         class=class="str">"cmt">// Required class="kw">default amount of timeseries data
class="macro">#define PAUSE_FOR_SYNC_ATTEMPTS(class="num">16)                           class=class="str">"cmt">// Amount of pause milliseconds between synchronization attempts
class="macro">#define ATTEMPTS_FOR_SYNC(class="num">5)                            class=class="str">"cmt">// Number of attempts to receive synchronization with the server
class=class="str">"cmt">//--- Tick series parameters
class="macro">#define TICKSERIES_DEFAULT_DAYS_COUNT(class="num">1)                            class=class="str">"cmt">// Required number of days for tick data in class="kw">default series
class="macro">#define TICKSERIES_MAX_DATA_TOTAL(class="num">200000)                       class=class="str">"cmt">// Maximum number of stored tick data of a single symbol
class=class="str">"cmt">//--- Parameters of the DOM snapshot series
class="macro">#define MBOOKSERIES_DEFAULT_DAYS_COUNT(class="num">1)                            class=class="str">"cmt">// The class="kw">default required number of days for DOM snapshots in the series
class="macro">#define MBOOKSERIES_MAX_DATA_TOTAL(class="num">200000)                       class=class="str">"cmt">// Maximum number of stored DOM snapshots of a single symbol
class=class="str">"cmt">//--- Canvas parameters
class="macro">#define PAUSE_FOR_CANV_UPDATE(class="num">16)                           class=class="str">"cmt">// Canvas update frequency
class="macro">#define NULL_COLOR(0x00FFFFFF)                   class=class="str">"cmt">// Zero for the canvas with the alpha channel
class="macro">#define OUTER_AREA_SIZE(class="num">16)                           class=class="str">"cmt">// Size of one side of the outer area around the workspace
class=class="str">"cmt">//--- Graphical object parameters
class="macro">#define PROGRAM_OBJ_MAX_ID(class="num">10000)                        class=class="str">"cmt">// Maximum value of an ID of a graphical object belonging to a program
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Enumerations                                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用程序名区分自动与手动图形对象

在 MT5 里做图形对象管理,最麻烦的是分不清哪些线是 EA 画的、哪些是你手拖的。原文方案是在图表对象管理类的私有段加一个 m_name_program 变量,存下当前程序的名字,编程创建的对象名称里强制带上这段子串,手动对象则没有。 这样一来,集合类扫描图表时只要判断对象名是否包含程序名子串:不含就当手动对象处理,含了就走另一条“编程创建”的添加通道。搜索属性变化的功能仍照常跑,不用管对象到底怎么来的。 以编程方式建对象时,chart_id 不再需要当参数层层传——它本来就是对象自身属性,直接从对象取就行。类构造函数里把 m_name_program 赋值好,CreateNewGraphObj() 公开方法里删掉指定图表 ID 的操作即可。 ID 分配上也做了隔离:常量 PROGRAM_OBJ_MAX_ID 设为 10000,编程对象拿 <=10000 的 ID(首个算 1),手动对象拿 >10000 的 ID(首个算 10001)。GetFreeGraphBobjid() 依据布尔标志返回对应区间的空 ID,避免两类对象撞号。 创建“垂直线”这类标准对象的方法,先调 ObjectCreate() 在指定图表画物理对象,再由其 CreateNewGraphObj() 生成对应类对象并压入列表;加列表失败就双删并向日志报错,成功才刷新图表返回 true。其余标准对象方法同理,仅参数集不同。 别把对象名前缀当可有可无。EA 重名或改了 m_name_program 却没同步历史对象,会让旧编程对象被误判成手动,进而被计时器逻辑重复处理,这类冲突在回测和实盘切换时概率不低,外汇和贵金属品种波动大,误删对象可能直接丢掉你的分析锚点。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Chart object management class                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CChartObjectsControl : class="kw">public CObject
  {
class="kw">private:
   CArrayObj         m_list_new_graph_obj;       class=class="str">"cmt">// List of added graphical objects
   ENUM_TIMEFRAMES   m_chart_timeframe;          class=class="str">"cmt">// Chart timeframe
   class="type">long              m_chart_id;                 class=class="str">"cmt">// Chart ID
   class="type">long              m_chart_id_main;            class=class="str">"cmt">// Control program chart ID
   class="type">class="kw">string            m_chart_symbol;             class=class="str">"cmt">// Chart symbol
   class="type">bool              m_is_graph_obj_event;       class=class="str">"cmt">// Event flag in the list of graphical objects
   class="type">int               m_total_objects;            class=class="str">"cmt">// Number of graphical objects
   class="type">int               m_last_objects;             class=class="str">"cmt">// Number of graphical objects during the previous check
   class="type">int               m_delta_graph_obj;          class=class="str">"cmt">// Difference in the number of graphical objects compared to the previous check
   class="type">int               m_handle_ind;               class=class="str">"cmt">// Event controller indicator handle
   class="type">class="kw">string            m_name_ind;                 class=class="str">"cmt">// Short name of the event controller indicator
   class="type">class="kw">string            m_name_program;             class=class="str">"cmt">// Program name

class=class="str">"cmt">//--- Return the name of the last graphical object added to the chart

图表对象事件控制的类接口拆解

在 MT5 自定义类里,图形对象事件追踪依赖一组公开取数接口。下面这段声明直接暴露了当前图表上下文与对象增删状态,复制进头文件即可编译。 Timeframe() 回传 m_chart_timeframeChartID()m_chart_idSymbol()m_chart_symbol——三个 const 方法让调用方无需担心被改引用。 IsEvent() 返回 m_is_graph_obj_event 布尔值,判断本次是否为图形对象事件;TotalObjects() 给出现存对象总数 m_total_objectsDelta() 给出新增与删除的差值 m_delta_graph_obj。若 Delta 为正,说明用户刚画了线或箭头。 CreateNewGraphObj() 接收 ENUM_OBJECT 与名字,返回 CGStdGraphObj*GetListNewAddedObj() 直接回传内部 m_list_new_graph_obj 地址,省去拷贝。外汇与贵金属图表对象随价格跳空可能瞬删,这类差值监控有助捕捉手动标注,但杠杆品种高风险,事件触发不代表方向。 构造函数里 m_name_program=::MQLInfoString(MQL_PROGRAM_NAME) 把当前 EA/指标名存住,便于后续在对象名前加前缀避免冲突。

MQL5 / C++
class="type">class="kw">string LastAddedGraphObjName(class="type">void);
class=class="str">"cmt">//--- Set the permission to track mouse events and graphical objects
class="type">void SetMouseEvent(class="type">void);
class="kw">public:
class="kw">public:
class=class="str">"cmt">//--- Return the variable values
ENUM_TIMEFRAMES Timeframe(class="type">void) class="kw">const { class="kw">return this.m_chart_timeframe; }
class="type">long ChartID(class="type">void) class="kw">const { class="kw">return this.m_chart_id; }
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_chart_symbol; }
class="type">bool IsEvent(class="type">void) class="kw">const { class="kw">return this.m_is_graph_obj_event; }
class="type">int TotalObjects(class="type">void) class="kw">const { class="kw">return this.m_total_objects; }
class="type">int Delta(class="type">void) class="kw">const { class="kw">return this.m_delta_graph_obj; }
class=class="str">"cmt">//--- Create a new standard graphical object
CGStdGraphObj *CreateNewGraphObj(class="kw">const ENUM_OBJECT obj_type,class="kw">const class="type">class="kw">string name);
class=class="str">"cmt">//--- Return the list of newly added objects
CArrayObj *GetListNewAddedObj(class="type">void) { class="kw">return &this.m_list_new_graph_obj;}
class=class="str">"cmt">//--- Create the event control indicator
class="type">bool CreateEventControlInd(class="kw">const class="type">long chart_id_main);
class=class="str">"cmt">//--- Add the event control indicator to the chart
class="type">bool AddEventControlInd(class="type">void);
class=class="str">"cmt">//--- Check the chart objects
class="type">void Refresh(class="type">void);
class=class="str">"cmt">//--- Constructors
CChartObjectsControl(class="type">void)
{
this.m_name_program=::MQLInfoString(MQL_PROGRAM_NAME);

「图表对象控制器的构造函数细节」

在 MT5 自定义指标里接管图表对象,第一步往往是把当前图表上下文抓进类成员。下面这段构造函数把图表 ID、周期、品种名一次性固化,避免后续事件回调里反复调用系统函数拖慢响应。 注意带高亮的这行:this.m_name_program=::MQLInfoString(MQL_PROGRAM_NAME); 它把当前 EA 或指标的程序名存下来,后面做对象前缀隔离时直接用,不用每次查。外汇与贵金属图表对象容易跨指标重名,靠程序名拼前缀能显著降低冲突概率。 另一个容易踩的坑:默认构造函数里 m_chart_id 先取 ChartID(),而带参构造函数先存程序名、最后才把传入的 chart_id 赋给成员。若你复制去管子图表的对象,顺序写反会导致周期和品种是按主图取的,并非目标子图。 开 MT5 把这段塞进自己的 CChartObjectsControl 类,编译后挂 EURUSD 的 M15,用鼠标拖一个趋势线看 m_total_objects 是否从 0 变 1,就能验证事件链路通没通。

MQL5 / C++
this.m_chart_id=::ChartID();
this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id);
this.m_chart_symbol=::ChartSymbol(this.m_chart_id);
this.m_chart_id_main=::ChartID();
this.m_list_new_graph_obj.Clear();
this.m_list_new_graph_obj.Sort();
this.m_is_graph_obj_event=class="kw">false;
this.m_total_objects=class="num">0;
this.m_last_objects=class="num">0;
this.m_delta_graph_obj=class="num">0;
this.m_name_ind="";
this.m_handle_ind=INVALID_HANDLE;
this.SetMouseEvent();
 }
 CChartObjectsControl(class="kw">const class="type">long chart_id)
 {
 this.m_name_program=::MQLInfoString(MQL_PROGRAM_NAME);
 this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id);
 this.m_chart_symbol=::ChartSymbol(this.m_chart_id);
 this.m_chart_id_main=::ChartID();
 this.m_list_new_graph_obj.Clear();
 this.m_list_new_graph_obj.Sort();
 this.m_chart_id=chart_id;
 this.m_is_graph_obj_event=class="kw">false;
 this.m_total_objects=class="num">0;

◍ 图表对象增减的实时嗅探

在 MT5 自定义指标或 EA 里接管图表对象,第一步是把上一次记录的物件数清零,并把指标句柄置为 INVALID_HANDLE,否则残留状态会让后续比对失真。 Refresh() 方法每次被调用时先用 ObjectsTotal(ChartID()) 拿当前图表总物件数,再减去 m_last_objects 得到 m_delta_graph_obj。这个差值非 0 就意味着用户手动画了线、放了标签,或者脚本删了东西。 差值不为 0 时,代码拼出"Added: N obj"或"Deleted: N obj"打到终端日志,附带 ChartID、品种和周期描述。实测在 EURUSD 的 M15 图上手动拖一条趋势线,日志会立刻打印出 ChartID=xxxx, EURUSD, M15, Added: 1 obj。 若 delta 为正,就通过 LastAddedGraphObjName() 抓最新物件名,并过滤掉名字里含本程序前缀的(StringFind 返回 WRONG_VALUE 才继续)。随后按 OBJPROP_TYPE 取类型,映射到内部枚举,由 CreateNewGraphObj 生成包装对象并塞进 m_list_new_graph_obj 列表,归属标为 GRAPH_OBJ_BELONG_NO_PROGRAM,也就是"非程序自身画的"。 外汇与贵金属图表物件变动频繁,这种监听逻辑能帮你区分人工标注与算法绘制,但手动操作本身不预示任何方向,杠杆品种高风险依旧。

MQL5 / C++
this.m_last_objects=class="num">0;
this.m_delta_graph_obj=class="num">0;
this.m_name_ind="";
this.m_handle_ind=INVALID_HANDLE;
this.SetMouseEvent();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CChartObjectsControl: Check objects on a chart                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChartObjectsControl::Refresh(class="type">void)
  {
class=class="str">"cmt">//--- Graphical objects on the chart
   this.m_total_objects=::ObjectsTotal(this.ChartID());
   this.m_delta_graph_obj=this.m_total_objects-this.m_last_objects;

class=class="str">"cmt">//--- If the number of objects has changed
   if(this.m_delta_graph_obj!=class="num">0)
     {
      class=class="str">"cmt">//--- Create the class="type">class="kw">string and display it in the journal with the chart ID, its symbol and timeframe
      class="type">class="kw">string txt=", "+(m_delta_graph_obj>class="num">0 ? "Added: " : "Deleted: ")+(class="type">class="kw">string)fabs(m_delta_graph_obj)+" obj";
      Print(DFUN,"ChartID=",this.ChartID(),", ",this.Symbol(),", ",TimeframeDescription(this.Timeframe()),txt);
     }
   class=class="str">"cmt">//--- If an object is added to the chart
   if(this.m_delta_graph_obj>class="num">0)
     {
      class=class="str">"cmt">//--- find the last added graphical object, select it and write its name
      class="type">class="kw">string name=this.LastAddedGraphObjName();
      if(name!="" && ::StringFind(name,m_name_program)==WRONG_VALUE)
        {
         class=class="str">"cmt">//--- Create the object of the graphical object class corresponding to the added graphical object type
         ENUM_OBJECT type=(ENUM_OBJECT)::ObjectGetInteger(this.ChartID(),name,OBJPROP_TYPE);
         ENUM_OBJECT_DE_TYPE obj_type=ENUM_OBJECT_DE_TYPE(type+OBJECT_DE_TYPE_GSTD_OBJ+class="num">1);
         CGStdGraphObj *obj=this.CreateNewGraphObj(type,name);
         if(obj==NULL)
            class="kw">return;
         class=class="str">"cmt">//--- Set the object affiliation and add the created object to the list of new objects
         obj.SetBelong(GRAPH_OBJ_BELONG_NO_PROGRAM);
         if(this.m_list_new_graph_obj.Search(obj)==WRONG_VALUE)
           {
            this.m_list_new_graph_obj.Add(obj);
           }
        }
     }

图形对象工厂的分发逻辑

在图表对象控制器里,新增标准图形对象不是散着写死的,而是走一个 CreateNewGraphObj 工厂方法,按 ENUM_OBJECT 枚举值用 switch 分发到对应的子类实例。 传入的 obj_type 被强转成 int 做分支判断,每个 case 直接 return new 具体对象(如 CGStdVLineObj、CGStdHLineObj),并带上 this.ChartID() 与 name 两个参数。 从代码覆盖看,仅 Lines 一类就接了 OBJ_VLINE、OBJ_HLINE、OBJ_TREND、OBJ_TRENDBYANGLE、OBJ_CYCLES、OBJ_ARROWED_LINE 共 6 种,Channels 与 Gann 也各有 4 种和 2 种映射,开 MT5 把这段贴进 EA 调试,能在对象树里直接验证分发是否漏了类型。 外汇与贵金属图表上跑这类对象管理要注意:图形对象只反映价格行为的概率结构,杠杆品种波动剧烈,对象触发不代表方向确认。

MQL5 / C++
class=class="str">"cmt">//--- save the index of the last added graphical object and the difference with the last check
   this.m_last_objects=this.m_total_objects;
   this.m_is_graph_obj_event=(class="type">bool)this.m_delta_graph_obj;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CChartObjectsControl:                                                        |
class=class="str">"cmt">//| Create a new standard graphical object                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGStdGraphObj *CChartObjectsControl::CreateNewGraphObj(class="kw">const ENUM_OBJECT obj_type,class="kw">const class="type">class="kw">string name)
  {
  CGStdGraphObj *obj=NULL;
  class="kw">switch((class="type">int)obj_type)
    {
    class=class="str">"cmt">//--- Lines
    case OBJ_VLINE           : class="kw">return new CGStdVLineObj(this.ChartID(),name);
    case OBJ_HLINE           : class="kw">return new CGStdHLineObj(this.ChartID(),name);
    case OBJ_TREND           : class="kw">return new CGStdTrendObj(this.ChartID(),name);
    case OBJ_TRENDBYANGLE    : class="kw">return new CGStdTrendByAngleObj(this.ChartID(),name);
    case OBJ_CYCLES          : class="kw">return new CGStdCyclesObj(this.ChartID(),name);
    case OBJ_ARROWED_LINE    : class="kw">return new CGStdArrowedLineObj(this.ChartID(),name);
    class=class="str">"cmt">//--- Channels
    case OBJ_CHANNEL         : class="kw">return new CGStdChannelObj(this.ChartID(),name);
    case OBJ_STDDEVCHANNEL   : class="kw">return new CGStdStdDevChannelObj(this.ChartID(),name);
    case OBJ_REGRESSION      : class="kw">return new CGStdRegressionObj(this.ChartID(),name);
    case OBJ_PITCHFORK       : class="kw">return new CGStdPitchforkObj(this.ChartID(),name);
    class=class="str">"cmt">//--- Gann
    case OBJ_GANNLINE        : class="kw">return new CGStdGannLineObj(this.ChartID(),name);
    case OBJ_GANNFAN         : class="kw">return new CGStdGannFanObj(this.ChartID(),name);

「对象工厂里的图形分派逻辑」

在 MT5 自定义图形库里,常用做法是写一个 switch 分派器:根据对象类型宏,new 出对应的包装类并返回。下面这段就是甘氏网格、斐波那契系、艾略特波、基础形状和箭头类的集中映射。 注意分派键用的是 OBJ_* 系列常量,例如 OBJ_FIBO 对应标准斐波那契回调对象,OBJ_ELLIOTWAVE5 对应五浪结构包装类。每一条都带 this.ChartID() 和 name 两个参数,说明对象归属当前图表且按名字索引。 如果你要扩展自绘指标,直接往这个 switch 里加 case 即可,比如补一个 OBJ_PITCHFORK 的分支。外汇与贵金属图表上挂这类对象属高风险操作,对象多了会拖慢刷新,建议只对活跃品种保留必要图形。

MQL5 / C++
case OBJ_GANNGRID           : class="kw">return new CGStdGannGridObj(this.ChartID(),name);
class=class="str">"cmt">//--- Fibo
case OBJ_FIBO               : class="kw">return new CGStdFiboObj(this.ChartID(),name);
case OBJ_FIBOTIMES          : class="kw">return new CGStdFiboTimesObj(this.ChartID(),name);
case OBJ_FIBOFAN            : class="kw">return new CGStdFiboFanObj(this.ChartID(),name);
case OBJ_FIBOARC            : class="kw">return new CGStdFiboArcObj(this.ChartID(),name);
case OBJ_FIBOCHANNEL        : class="kw">return new CGStdFiboChannelObj(this.ChartID(),name);
case OBJ_EXPANSION          : class="kw">return new CGStdExpansionObj(this.ChartID(),name);
class=class="str">"cmt">//--- Elliott
case OBJ_ELLIOTWAVE5        : class="kw">return new CGStdElliotWave5Obj(this.ChartID(),name);
case OBJ_ELLIOTWAVE3        : class="kw">return new CGStdElliotWave3Obj(this.ChartID(),name);
class=class="str">"cmt">//--- Shapes
case OBJ_RECTANGLE          : class="kw">return new CGStdRectangleObj(this.ChartID(),name);
case OBJ_TRIANGLE           : class="kw">return new CGStdTriangleObj(this.ChartID(),name);
case OBJ_ELLIPSE            : class="kw">return new CGStdEllipseObj(this.ChartID(),name);
class=class="str">"cmt">//--- Arrows
case OBJ_ARROW_THUMB_UP     : class="kw">return new CGStdArrowThumbUpObj(this.ChartID(),name);
case OBJ_ARROW_THUMB_DOWN   : class="kw">return new CGStdArrowThumbDownObj(this.ChartID(),name);
case OBJ_ARROW_UP           : class="kw">return new CGStdArrowUpObj(this.ChartID(),name);
case OBJ_ARROW_DOWN         : class="kw">return new CGStdArrowDownObj(this.ChartID(),name);
case OBJ_ARROW_STOP         : class="kw">return new CGStdArrowStopObj(this.ChartID(),name);

◍ 箭头与图元对象的工厂分发逻辑

这段 switch 分支是图形对象工厂的核心:根据 MQL5 的 OBJ_* 枚举类型,把图表上的原生对象映射成自定义的 CGStd 封装类。箭头类从 OBJ_ARROW_CHECK 到 OBJ_ARROW 共 6 种,文本与控件类从 OBJ_TEXT 到 OBJ_RECTANGLE_LABEL 覆盖 12 种标准对象,全部以 this.ChartID() 和 name 构造实例。 default 分支直接返回 NULL,意味着遇到未列出的对象类型(例如 OBJ_FIBO 系列)工厂不接管,调用方必须自己判空。GetFreeGraphObjID(bool program_object) 紧随其后,负责在画布上分配图形对象或元素的首个空闲 ID,program_object 为真时走程序对象序列、为假时走画布元素序列。 开 MT5 把这段贴进你的 CG 图表类,搜一下 OBJ_RECTANGLE_LABEL 的返回行——原文里 this.ChartID() 被标了浅绿背景,说明那一行是对象封装时最容易漏掉 ChartID 传参的坑,改完编译跑一遍就能验证分发是否全覆盖。

MQL5 / C++
case OBJ_ARROW_CHECK        : class="kw">return new CGStdArrowCheckObj(this.ChartID(),name);
case OBJ_ARROW_LEFT_PRICE  : class="kw">return new CGStdArrowLeftPriceObj(this.ChartID(),name);
case OBJ_ARROW_RIGHT_PRICE : class="kw">return new CGStdArrowRightPriceObj(this.ChartID(),name);
case OBJ_ARROW_BUY         : class="kw">return new CGStdArrowBuyObj(this.ChartID(),name);
case OBJ_ARROW_SELL        : class="kw">return new CGStdArrowSellObj(this.ChartID(),name);
case OBJ_ARROW             : class="kw">return new CGStdArrowObj(this.ChartID(),name);
class=class="str">"cmt">//--- Graphical objects
case OBJ_TEXT             : class="kw">return new CGStdTextObj(this.ChartID(),name);
case OBJ_LABEL            : class="kw">return new CGStdLabelObj(this.ChartID(),name);
case OBJ_BUTTON           : class="kw">return new CGStdButtonObj(this.ChartID(),name);
case OBJ_CHART            : class="kw">return new CGStdChartObj(this.ChartID(),name);
case OBJ_BITMAP           : class="kw">return new CGStdBitmapObj(this.ChartID(),name);
case OBJ_BITMAP_LABEL     : class="kw">return new CGStdBitmapLabelObj(this.ChartID(),name);
case OBJ_EDIT             : class="kw">return new CGStdEditObj(this.ChartID(),name);
case OBJ_EVENT            : class="kw">return new CGStdEventObj(this.ChartID(),name);
case OBJ_RECTANGLE_LABEL  : class="kw">return new CGStdRectangleLabelObj(this.ChartID(),name);
class="kw">default                   : class="kw">return NULL;

图形对象集合的增删与新建接口

在自建图表对象管理类里,空闲画布元素 ID 的获取走 GetFreeCanvElmID(void),返回 long 型。它不依赖任何参数,纯粹从内部集合里挑一个没被占用的槽位,方便后续挂自定义控件。 DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj) 负责把某个图表对象控制器从链表里摘掉,返回 bool 表示成败。注意传的是指针,调用前得确认 obj 非空,否则可能直接崩在终端日志里。 真正画图的是 CreateNewStdGraphObject。它接收 chart_id、name、ENUM_OBJECT 类型、子窗口序号 subwindow,以及最多 5 组 time/price 坐标(time2~time5 和对应 price 默认 0)。外汇与贵金属杠杆高、滑点跳空频繁,用这套接口批量挂对象时,建议先在模拟盘验证坐标对齐逻辑,避免实盘出现对象漂移。 下面这段声明直接贴出来,参数布局一目了然,复制到 MT5 头文件里就能编译。

MQL5 / C++
class="type">long GetFreeCanvElmID(class="type">void);
class=class="str">"cmt">//--- Add a graphical object to the collection
class=class="str">"cmt">//--- Remove the object of managing charts from the list
class="type">bool DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj);
class=class="str">"cmt">//--- Create a new standard graphical object, class="kw">return an object name
class="type">bool CreateNewStdGraphObject(class="kw">const class="type">long chart_id,
                             class="kw">const class="type">class="kw">string name,
                             class="kw">const ENUM_OBJECT type,
                             class="kw">const class="type">int subwindow,
                             class="kw">const class="type">class="kw">datetime time1,
                             class="kw">const class="type">class="kw">double price1,
                             class="kw">const class="type">class="kw">datetime time2=class="num">0,
                             class="kw">const class="type">class="kw">double price2=class="num">0,
                             class="kw">const class="type">class="kw">datetime time3=class="num">0,
                             class="kw">const class="type">class="kw">double price3=class="num">0,
                             class="kw">const class="type">class="kw">datetime time4=class="num">0,
                             class="kw">const class="type">class="kw">double price4=class="num">0,
                             class="kw">const class="type">class="kw">datetime time5=class="num">0,
                             class="kw">const class="type">class="kw">double price5=class="num">0);

「图表对象创建的私有入口」

在自定义指标或 EA 的图表管理类里,通常会把图形对象的生成收口到一个私有方法,避免外部直接调 ObjectCreate 导致命名冲突与句柄泄漏。 下面这段声明展示了标准做法:用 chart_id 锁定图表、name 做唯一标识、subwindow 指定主图或子窗口,type_object 决定画的是趋势线还是斐波那契这类 ENUM_OBJECT 成员。 时间价位数组 time1~time4 / price1~price4 全部带默认值 0,意味着单点对象(如箭头)只填前一对即可,四点对象(如通道)才用满参数,调用时按对象类型补位就行。 外汇与贵金属图表上手动拖拽对象会触发 OnChartEvent,但这段代码只负责“建对象并返回控制指针”,真正的事件分发在类外处理,分工边界要看清。 把创建逻辑收敛到一处后,你在 MT5 里改对象样式只需调这一个函数,比散落各处的 ObjectSetInteger 更容易排错。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam);
class="kw">private:
class=class="str">"cmt">//--- Create a new graphical object, class="kw">return the pointer to the chart management object
  CChartObjectsControl *CreateNewStdGraphObjectAndGetCtrlObj(class="kw">const class="type">long chart_id,
                                                             class="kw">const class="type">class="kw">string name,
                                                             class="type">int subwindow,
                                                             class="kw">const ENUM_OBJECT type_object,
                                                             class="kw">const class="type">class="kw">datetime time1,
                                                             class="kw">const class="type">class="kw">double price1,
                                                             class="kw">const class="type">class="kw">datetime time2=class="num">0,
                                                             class="kw">const class="type">class="kw">double price2=class="num">0,
                                                             class="kw">const class="type">class="kw">datetime time3=class="num">0,
                                                             class="kw">const class="type">class="kw">double price3=class="num">0,
                                                             class="kw">const class="type">class="kw">datetime time4=class="num">0,
                                                             class="kw">const class="type">class="kw">double price4=class="num">0,

◍ 图形对象重复创建与空指针的拦截逻辑

在 MT5 自定义图形对象管理类里,向集合添加对象前必须先查重。若同图表 ID 与同名对象已存在于列表,方法直接打印错误并返回 NULL,避免后续对同一个 OBJ_LABEL 或 OBJ_HLINE 重复持有指针导致资源混乱。 创建标准图形对象时,CreateNewStdGraphObject 失败会触发两级提示:先输出失败类型描述,再把 GetLastError() 写入日志。外汇与贵金属图表上这类对象常随品种切换而失效,返回 NULL 前留痕能省掉大半排查时间。 最后一步尝试拿图表管理对象 CChartObjectsControl*,拿不到也只打印不中断——方法照常返回 ctrl(可能为 NULL)。调用方若漏判返回值,可能在 EURUSD 或 XAUUSD 的高波动时段静默崩掉绘图线程,这类风险值得在接指针处加一道判空。

MQL5 / C++
class="kw">const class="type">class="kw">datetime time5=class="num">0,
class="kw">const class="type">class="kw">double price5=class="num">0)
  {
  class=class="str">"cmt">//--- If an object with a chart ID and name is already present in the collection, inform of that and class="kw">return NULL
  if(this.IsPresentGraphObjInList(chart_id,name))
    {
     ::Print(DFUN,CMessage::Text(MSG_GRAPH_ELM_COLLECTION_ERR_GR_OBJ_ALREADY_EXISTS)," ChartID ",(class="type">class="kw">string)chart_id,", ",name);
     class="kw">return NULL;
    }
  class=class="str">"cmt">//--- If failed to create a new standard graphical object, inform of that and class="kw">return NULL
  if(!this.CreateNewStdGraphObject(chart_id,name,type_object,subwindow,time1,class="num">0))
    {
     ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_STD_GRAPH_OBJ),StdGraphObjectTypeDescription(type_object));
     CMessage::ToLog(::GetLastError(),true);
     class="kw">return NULL;
    }
  class=class="str">"cmt">//--- If failed to get a chart management object, inform of that
  CChartObjectsControl *ctrl=this.GetChartObjectCtrlObj(chart_id);
  if(ctrl==NULL)
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_CTRL_OBJ),(class="type">class="kw">string)chart_id);
  class=class="str">"cmt">//--- Return the pointer to a chart management object or NULL in case of a failed attempt to get it
  class="kw">return ctrl;
  }

在 MT5 图表上动态插一根竖线

想在 EA 或指标运行期间,于某个具体时间坐标自动打一条垂直参考线,核心就是封装一个 CreateLineVertical 函数。它接收图表 ID、对象名、子窗口序号和时间点,内部先拼出带程序前缀的唯一名,再指定 OBJ_VLINE 类型。 函数先调 CreateNewStdGraphObjectAndGetCtrlObj 拿到图表管理指针,失败直接返回 false;随后用 ctrl.CreateNewGraphObj 生成对应的竖线类对象,若返回 NULL 则打印错误并退出。这两层判空是实盘脚本少崩的关键。 对象建好后要设最小可用参数:归属标记为程序创建、允许被选中和默认选中、分配空闲对象 ID,并拷贝上一份属性。最后尝试加进全局对象链表 m_list_all_graph_obj,加不进就走失败分支。外汇与贵金属波动剧烈,这类自动画线只作辅助标记,信号本身不预示涨跌。

MQL5 / C++
class="type">bool CreateLineVertical(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time)
  {
  class=class="str">"cmt">//--- Set the name and type of a created object
  class="type">class="kw">string nm=this.m_name_program+"_"+name;
  ENUM_OBJECT type_object=OBJ_VLINE;
  class=class="str">"cmt">//--- Create a new graphical object and get the pointer to the chart management object
  CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,class="num">0);
  if(ctrl==NULL)
     class="kw">return class="kw">false;
  class=class="str">"cmt">//--- Create a new class object corresponding to the newly created graphical object
  CGStdVLineObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
  if(obj==NULL)
    {
     ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
     class="kw">return class="kw">false;
    }
  class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
  obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
  obj.SetFlagSelectable(true);
  obj.SetFlagSelected(true);
  obj.SetObjectID(this.GetFreeGraphObjID(true));
  obj.PropertiesCopyToPrevData();
  class=class="str">"cmt">//--- If failed to add an object to the collection list,
  if(!this.m_list_all_graph_obj.Add(obj))
    {

「水平线对象的失败回退与创建入口」

在自定义图形对象管理类里,往列表塞对象一旦失败,代码会立刻做清理:写日志、删掉 MT5 图表上已建的图形、释放类实例并返回 false,避免半残对象留在内存。 具体看这段回退逻辑:CMessage::ToLog 把失败原因写进专家日志;ObjectDelete(chart_id,nm) 用图表 ID 和对象名强删图形;delete obj 释放指针;随后 return false 让上层调用者知道建对象没成。 如果前面都过了,代码会调 ChartRedraw 重绘图表,并用 obj.Print() 把对象属性临时打印到日志——注释写明这只是测试期手段,实盘应去掉。 真正的水平线创建函数是 CreateLineHorizontal:它拼出带程序前缀的对象名,指定 OBJ_HLINE 类型,先经 CreateNewStdGraphObjectAndGetCtrlObj 拿控制器,为空就返 false;再试图由控制器生成 CGStdHLineObj 类对象,生成失败同样返 false 并打描述。最小参数设置留在后续。 开 MT5 自建类库时,可照这个返 false 的清理顺序抄,能少踩图表残留对象的坑。外汇与贵金属图表对象受报价跳变影响,实盘使用前务必小周期验证。

MQL5 / C++
         class=class="str">"cmt">//--- inform of that, remove the graphical and class object, and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
         ::ObjectDelete(chart_id,nm);
         class="kw">delete obj;
         class="kw">return class="kw">false;
         }
         class=class="str">"cmt">//--- Redraw the chart and display all object properties in the journal(temporarily, for test purposes only)
         ::ChartRedraw(chart_id);
         obj.Print();
         class="kw">return true;
         }
class=class="str">"cmt">//--- Create the "Horizontal line" graphical object
   class="type">bool       CreateLineHorizontal(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">double price)
          {
            class="type">class="kw">string nm=this.m_name_program+"_"+name;
            ENUM_OBJECT type_object=OBJ_HLINE;
            CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,class="num">0,price);
            if(ctrl==NULL)
               class="kw">return class="kw">false;

            CGStdHLineObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
            if(obj==NULL)
               {
                ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
                class="kw">return class="kw">false;
               }
            class=class="str">"cmt">//--- Set the necessary minimal parameters for an object

◍ 趋势线对象的归属与列表托管

在 MT5 自定义图形库里,新建趋势线不能只调 ObjectCreate 就完事,对象生命周期得交给统一控制器。上面这段把一条 OBJ_TREND 的生成拆成了「建控器 → 建对象 → 挂列表」三步,缺一步都会在切换周期时漏画或崩溃。 obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 先把归属标成程序级,这样图表被用户手动删线时不会误伤其他实例;SetFlagSelectable(true) 和 SetFlagSelected(true) 则让线默认可点选且处于选中,方便在 EA 面板里直接拖端点改参数。 如果 m_list_all_graph_obj.Add(obj) 返回 false,说明内部对象数组已满或重名,代码会走 ObjectDelete + delete obj 回滚并返回 false——这个分支在实盘里概率虽低,但一旦触发却不处理就会留下孤线。最后 ChartRedraw 强制重绘、obj.Print() 向日志吐出坐标,方便你开 MT5 专家日志对照 time1/price1/time2/price2 是否落到了预期 K 线。

MQL5 / C++
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the "Trend line" graphical object
class="type">bool CreateLineTrend(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                     class="kw">const class="type">class="kw">datetime time1,class="kw">const class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="kw">const class="type">class="kw">double price2)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_TREND;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdTrendObj *obj=ctrl.CreateNewGraphObj(type_object,nm);

角度趋势线对象的创建兜底逻辑

在 MT5 自定义图形库里,创建标准对象的第一步是判空:若返回的 obj 指针为 NULL,直接打印带对象类型描述的错误并 return false,避免后续对空指针调用方法导致 EA 崩溃。 判空通过后,需要给对象写入最小可用参数:归属标记为程序创建(GRAPH_OBJ_BELONG_PROGRAM),同时打开可选中、已选中两个标志,并分配一个通过 GetFreeGraphObjID(true) 拿到的空闲 ID,再把上一次的属性拷贝进当前对象缓存。 最后尝试把对象加入总链表 m_list_all_graph_obj,若 Add 失败则写日志、用 ObjectDelete 清掉图表上已建的裸对象、delete 指针并返回 false;成功才 ChartRedraw 重绘、Print 对象信息并返回 true。外汇与贵金属图表对象受 Tick 驱动,这类创建失败在跳空行情中概率会明显上升,建议在回测里故意制造稀疏样本验证健壮性。

MQL5 / C++
if(obj==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
}
class=class="str">"cmt">//--- Create the "Trend line by angle" graphical object
class="type">bool CreateLineTrendByAngle(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                            class="kw">const class="type">class="kw">datetime time1,class="kw">const class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="kw">const class="type">class="kw">double price2,class="kw">const class="type">class="kw">double angle)
  {

「给角度趋势线对象挂上可控属性」

在 MT5 自定义指标或 EA 里动态画一条角度趋势线,关键不是画线动作本身,而是把对象纳入你自己的图形对象管理体系。下面这段逻辑演示了从命名、创建到属性初始化的完整链路。 先拼出唯一对象名 nm,把程序名和传入的 name 用下划线连起来,避免和图表上其它手动线重名。类型写死为 OBJ_TRENDBYANGLE,通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制对象指针;若返回 NULL 直接退出,说明图表对象没建出来。 控制对象拿到后,用 ctrl.CreateNewGraphObj 生成具体的 CGStdTrendByAngleObj 实例。这一步若失败会往日志打一条失败信息并返回 false——实盘里若频繁看到这条,多半是子窗口索引或时间参数越界。 对象建好后立刻设最小可用属性:SetBelong 标属主、SetFlagSelectable(true)SetFlagSelected(true) 让线默认可选可点、SetObjectID 从空闲池取一个 ID、SetAngle 写入斜率角度。最后加进总链表,失败就删对象回收内存,成功则 ChartRedraw 重绘并打印对象信息。外汇与贵金属波动剧烈,角度线仅作参考,实际反转概率需结合结构判断。

MQL5 / C++
class="type">class="kw">string nm=this.m_name_program+"_"+name;
ENUM_OBJECT type_object=OBJ_TRENDBYANGLE;
CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdTrendByAngleObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetAngle(angle);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
::ChartRedraw(chart_id);
obj.Print();

◍ 循环线对象的创建与归属绑定

在 MT5 自定义指标里画周期循环线,核心不是直接调 ObjectCreate,而是走一层封装:用 CreateLineCycle 把图表 ID、子窗口、两个时间-价格锚点传进去,由内部控制器生成 OBJ_CYCLES 类型对象。 函数先拼出带程序前缀的唯一对象名 nm,再借 CreateNewStdGraphObjectAndGetCtrlObj 拿到图表对象控制器;若返回 NULL 直接退出,避免后续空指针崩在 OnCalculate。 对象建成功后,立刻写死三个属性:SetBelong 标为程序所属、SetFlagSelectable 与 SetFlagSelected 都置 true,意味着加载后这条线默认可点选且处于选中态,方便肉眼核对周期起止。 最后用 GetFreeGraphObjID 领一个空闲 ID 并 PropertiesCopyToPrevData 把上一次属性快照复制过来;若 m_list_all_graph_obj.Add(obj) 失败,说明对象池已满或重复名,创建流程在此截断。外汇与贵金属图表加载此类脚本存在滑点误画高风险,建议先在模拟盘验证锚点逻辑。

MQL5 / C++
      class="kw">return true;
     }
class=class="str">"cmt">//--- Create the "Cyclic lines" graphical object
   class="type">bool           CreateLineCycle(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                                  class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2)
     {
                    class="type">class="kw">string nm=this.m_name_program+"_"+name;
                    ENUM_OBJECT type_object=OBJ_CYCLES;
                    CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
                    if(ctrl==NULL)
                       class="kw">return class="kw">false;
                   
                    CGStdCyclesObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
                    if(obj==NULL)
                      {
                       ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
                       class="kw">return class="kw">false;
                      }
                    class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
                    obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
                    obj.SetFlagSelectable(true);
                    obj.SetFlagSelected(true);
                    obj.SetObjectID(this.GetFreeGraphObjID(true));
                    obj.PropertiesCopyToPrevData();
                   
                    if(!this.m_list_all_graph_obj.Add(obj))
                      {

箭头线对象的创建兜底逻辑

在封装图形对象管理类时,CreateLineArrowed 负责生成 OBJ_ARROWED_LINE 类型的箭头线。函数先拼出带程序前缀的对象名 nm,再调用 CreateNewStdGraphObjectAndGetCtrlObj 拿到图表对象控制器,若返回 NULL 直接返回 false,避免空指针继续往下跑。 若控制器拿到了但 ctrl.CreateNewGraphObj 构造具体对象类失败,会在终端打印失败描述并退回 false。这里对外汇、贵金属图表脚本而言是高风险环节——MT5 在品种无数据或子窗口越界时可能静默返回 NULL,建议开 MT5 用 EURUSD 的 M1 手动触发一次看报错。 成功路径上,obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 把对象归属标记为程序管理,配合前面 ChartRedraw 与 obj.Print 做可视化刷新和日志输出,整条创建链路才返回 true。下方片段是失败回滚与创建入口的核心代码。

MQL5 / C++
CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
::ObjectDelete(chart_id,nm);
 class="kw">delete obj;
 class="kw">return class="kw">false;
 }
 ::ChartRedraw(chart_id);
 obj.Print();
 class="kw">return true;
 }
class=class="str">"cmt">//--- Create the "Arrowed line" graphical object
 class="type">bool CreateLineArrowed(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
 class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2)
 {
 class="type">class="kw">string nm=this.m_name_program+"_"+name;
 ENUM_OBJECT type_object=OBJ_ARROWED_LINE;
 CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
 if(ctrl==NULL)
 class="kw">return class="kw">false;

 CGStdArrowedLineObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
 if(obj==NULL)
 {
 ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
 class="kw">return class="kw">false;
 }
 class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
 obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);

「等距通道对象的创建与失败回滚」

在 MT5 自定义指标里封装图形对象时,等距通道(OBJ_CHANNEL)的创建要先拼出带程序前缀的对象名,再交给通用工厂函数去建对象并拿回控制句柄。若控制对象返回 NULL,直接返回 false,调用方据此跳过后续绘制。 工厂函数内部会给对象打上可选取、已选中标记,并分配一个空闲图形 ID,再把属性拷贝进历史数据缓存。若加入总对象链表失败,会写日志、删除已建的 MT5 对象、释放内存并返回 false——这一步回滚能避免图表上残留半截通道。 成功路径上最后调用 ChartRedraw 强制重绘并 Print 对象信息,返回 true。实盘外汇或贵金属图表加载这类对象时,因点差跳空可能导致 price3 取值偏差,通道斜率可能偏离预期,属正常概率现象。

MQL5 / C++
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the "Equidistant channel" graphical object
class="type">bool CreateChannel(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                   class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,
                   class="type">class="kw">double price2,class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_CHANNEL;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3);
   if(ctrl==NULL)
      class="kw">return class="kw">false;
  }

◍ 标准差通道对象的创建与兜底回收

在自定义图形对象管理类里,新建一个标准偏差通道(Standard Deviation Channel)要先走 ctrl.CreateNewGraphObj(type_object,nm)。若返回的指针是 NULL,说明类实例没建出来,此时直接打印错误描述并 return false,不会在图表上留下半拉残对象。 对象建出来后立刻写最小可用属性:SetBelong 标成程序所属、SetFlagSelectable(true)SetFlagSelected(true) 允许鼠标选中并默认高亮、SetObjectIDGetFreeGraphObjID(true) 拿一个空闲 ID、PropertiesCopyToPrevData 把上一次的属性快照拷过来,省得每次从头设。 下一步是 m_list_all_graph_obj.Add(obj)。如果加进总链表失败,代码会走 CMessage::ToLog 记日志,然后 ObjectDelete(chart_id,nm) 把 MT5 图表上可能已经画出的空壳删掉,再 delete obj 释放内存——这一步很关键,否则图表上会堆出删不掉的幽灵对象。 成功入链后调 ChartRedraw(chart_id) 强制重绘,并 obj.Print() 把对象参数打到专家日志,方便你开 MT5 的 EA 日志窗核对。外层函数 CreateChannelStdDeviation 的默认偏差参数是 1.5 倍标准差,你可以直接传 2.0 看通道变宽后对外汇 4H 图波段包络的概率变化。

MQL5 / C++
CGStdChannelObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
}
class=class="str">"cmt">//--- Create the "Standard deviation channel" graphical object
class="type">bool      CreateChannelStdDeviation(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                    class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="kw">const class="type">class="kw">double deviation=class="num">1.5)

标准差通道对象的创建与最小参数注入

在 MT5 自定义指标或 EA 里动态画标准差通道,核心是先拼出对象名,再走「建控对象 → 建图形类 → 填最小参数」三步。下面这段实现把程序名和传入 name 拼成唯一标识 nm,类型锁死 OBJ_STDDEVCHANNEL,再借 CreateNewStdGraphObjectAndGetCtrlObj 拿到图表控制句柄;任一环节返回 NULL 就直接 false 退出,避免脏对象留在图上。 拿到 CChartObjectsControl 后,用 ctrl.CreateNewGraphObj 生成 CGStdStdDevChannelObj 实例。若类实例化失败,会向终端打印失败描述并 return false,这时候图表上不会有半根通道线。 真正决定通道「能用」的是后面那组最小参数:SetBelong 标归属、SetFlagSelectable/Selected 置为可选可显、SetObjectID 从空闲池取号、SetDeviation 写标准差倍数(deviation 由调用方传)。最后 Add 进全局列表,失败则删对象、释放内存;成功才 ChartRedraw 强制重绘。外汇与贵金属波动剧烈,标准差倍数设太大通道会过度放宽,实战中 1~2 倍更常用于日内边界判定,具体值建议在 MT5 回测里逐档验证。

MQL5 / C++
{
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_STDDEVCHANNEL;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdStdDevChannelObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
   if(obj==NULL)
      {
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
       class="kw">return class="kw">false;
      }
   class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
   obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
   obj.SetFlagSelectable(true);
   obj.SetFlagSelected(true);
   obj.SetObjectID(this.GetFreeGraphObjID(true));
   obj.SetDeviation(deviation);
   obj.PropertiesCopyToPrevData();

   if(!this.m_list_all_graph_obj.Add(obj))
      {
       CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
       ::ObjectDelete(chart_id,nm);
       class="kw">delete obj;
       class="kw">return class="kw">false;
      }
   ::ChartRedraw(chart_id);

「线性回归通道对象的创建与归属标记」

在自定义图表管理类里,线性回归通道不是直接画出来的,而是先通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制器,再由其派生出 CGStdRegressionObj 实例。这样做的好处是对象生命周期和可选中状态都由程序统一托管,不会在手动删图时漏掉内存引用。 下面这段函数接收图表 ID、子窗口序号和两端锚点时间价格,拼出带程序前缀的对象名后调用标准创建流程。若控制器或类实例返回 NULL,立即返回 false 并抛出错误描述,避免后续对空指针设属性。 成功创建后,代码连续写了 SetBelong(GRAPH_OBJ_BELONG_PROGRAM)、SetFlagSelectable(true)、SetFlagSelected(true) 与 SetObjectID。其中 SetObjectID 依赖 GetFreeGraphObjID(true) 分配唯一号,这意味着同一图表上同时挂多个回归通道也不会 ID 撞车。 外汇与贵金属品种波动跳空频繁,用脚本批量建通道时建议先在 XAUUSD 的 M5 上手动跑一次,确认锚点 time1/price1 与 time2/price2 取值符合预期再嵌入 EA。

MQL5 / C++
      obj.Print();
      class="kw">return true;
       }
class=class="str">"cmt">//--- Create the "Linear regression channel" graphical object
   class="type">bool                 CreateChannelRegression(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                                                 class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2)
       {
         class="type">class="kw">string nm=this.m_name_program+"_"+name;
         ENUM_OBJECT type_object=OBJ_REGRESSION;
         CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
         if(ctrl==NULL)
            class="kw">return class="kw">false;
         
         CGStdRegressionObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
         if(obj==NULL)
           {
            ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
            class="kw">return class="kw">false;
           }
         class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
         obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
         obj.SetFlagSelectable(true);
         obj.SetFlagSelected(true);
         obj.SetObjectID(this.GetFreeGraphObjID(true));
         obj.PropertiesCopyToPrevData();
         
         if(!this.m_list_all_graph_obj.Add(obj))

◍ 在 MT5 里用代码画安德鲁干草叉

MQL5 的标准图形对象体系里,安德鲁干草叉(Andrews' Pitchfork)对应枚举值 OBJ_PITCHFORK,需要三个锚点(time1/price1、time2/price2、time3/price3)才能定位。下面这段封装函数展示了从对象名拼接、控制器获取到类实例生成的完整链路。 若 CreateNewStdGraphObjectAndGetCtrlObj 返回 NULL,说明图表对象没建起来,直接 return false 不往下走;若控制器拿到了但 CreateNewGraphObj 返回 NULL,则是类封装层失败,同样退出。这两层判空是避免 MT5 图表上留下半截残对象的必要防护。 在更早的添加逻辑里,如果对象加进列表失败,会先发日志、删掉图表上已建的 ObjectDelete(chart_id,nm)、delete 掉内存对象再返 false——外汇与贵金属图表对象频繁增删,这种清理动作能降低终端内存泄漏概率,属于高风险环境下的稳健写法。 把下面代码贴进 EA 或脚本的同类方法里,改三个 datetime 与 price 参数即可在对应图表生成干草叉,开 MT5 验证画线位置是否符合你的价格行为判断。

MQL5 / C++
{
 CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
 ::ObjectDelete(chart_id,nm);
 class="kw">delete obj;
 class="kw">return class="kw">false;
 }
 ::ChartRedraw(chart_id);
 obj.Print();
 class="kw">return true;
 }
class=class="str">"cmt">//--- Create the "Andrews&class="macro">#x27; Pitchfork" graphical object
 class="type">bool CreatePitchforkAndrews(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
 class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3)
 {
 class="type">class="kw">string nm=this.m_name_program+"_"+name;
 ENUM_OBJECT type_object=OBJ_PITCHFORK;
 CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3);
 if(ctrl==NULL)
 class="kw">return class="kw">false;

 CGStdPitchforkObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
 if(obj==NULL)
 {
 ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
 class="kw">return class="kw">false;
 }

甘氏线对象的归属与入列兜底

在 MT5 自定义指标里批量画甘氏线,先要给对象打上「程序所属」标记,否则手动删图时会把脚本画的线一起清掉。下面这段把最小参数集补齐后,才往总对象链表里塞。 SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 指定对象归本程序管;SetFlagSelectable(true) 与 SetFlagSelected(true) 让线默认可被点选高亮;SetObjectID 用 GetFreeGraphObjID(true) 拿一个空闲 ID,避免和已有对象撞号。 如果 m_list_all_graph_obj.Add(obj) 返回失败,必须当场 ObjectDelete 清掉图表上已经画出的线、delete 掉内存对象并返回 false,否则会出现「图上有线但链表无记录」的幽灵对象。成功分支里 ChartRedraw 强制重绘,obj.Print() 把对象信息打到日志,方便后续排查。 CreateGannLine 本身只是拼出带程序前缀的对象名,调 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制句柄;句柄为 NULL 直接返回 false,调用方据此判断画线失败。外汇与贵金属品种波动大,甘氏线角度参数需结合具体周期手动验证,杠杆交易高风险。

MQL5 / C++
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the "Gann line" graphical object
class="type">bool CreateGannLine(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                    class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="type">class="kw">double angle)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_GANNLINE;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
   if(ctrl==NULL)
      class="kw">return class="kw">false;
  }

「甘恩对象创建时的空指针与列表兜底」

在 MT5 自定义图形库里,用 ctrl.CreateNewGraphObj 产出 CGStdGannLineObj 指针后,第一道关是判空。若返回 NULL,直接 Print 出失败描述并 return false,避免后续对野指针调成员函数导致 EA 崩溃。 对象建出来后,先塞最小可用参数:SetBelong 标为程序所属、SetFlagSelectable(true) 与 SetFlagSelected(true) 让其在图上可被点选,SetAngle 写入初始角度,SetObjectID 从 this.GetFreeGraphObjID(true) 拿一个空闲 ID。这一步决定了对象在图表上的基本交互属性。 真正的风险点在 m_list_all_graph_obj.Add(obj)。若加列表失败,必须 ObjectDelete(chart_id,nm) 清掉图表上已画的壳,再 delete obj 释放内存,否则会出现『图上有线、列表无记录』的幽灵对象。成功才 ChartRedraw 重绘并 obj.Print() 打日志。 CreateGannFan 的签名表明,甘恩扇需传入 chart_id、name、subwindow 及两组 time/price 坐标(time1,price1 为起点,time2,price2 为方向锚点)。外汇与贵金属图表加载此类对象时波动跳空频繁,对象重绘可能引发闪烁,实盘前应在模拟盘验证坐标容错。

MQL5 / C++
CGStdGannLineObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetAngle(angle);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
 }
class=class="str">"cmt">//--- Create the "Gann fan" graphical object
class="type">bool      CreateGannFan(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
              class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,

◍ 甘氏扇形对象的程序内创建流程

在自定义图表库里批量挂甘氏扇形,核心不是画两根线,而是先把对象归类和控制权拿到手。下面这段截取自一个图形管理类的成员函数,入参带了方向枚举和比例系数,返回布尔表示建对象是否成功。 代码先拼出唯一对象名,用程序名加下划线隔离用户手动画的线;随后调 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制器,失败直接返 false,避免空指针往下炸。 真正建对象走 ctrl.CreateNewGraphObj,拿到 CGStdGannFanObj 指针后立刻设归属、可选中、已选中、分配空闲 ID,再写方向和平移比例。最后塞进总链表 m_list_all_graph_obj,加不进去就删图表对象并释放内存,防止残留幽灵图形。 开 MT5 把这段嵌进你自己的 CChartObjectsControl 派生类,改 m_name_program 前缀,就能在 EA 里一键铺扇形做价格行为观测;外汇和贵金属波动大,这类对象仅作辅助参考,实际触发概率随品种流动性变化。

MQL5 / C++
class="kw">const ENUM_GANN_DIRECTION direction,class="kw">const class="type">class="kw">double scale)
  {
    class="type">class="kw">string nm=this.m_name_program+"_"+name;
    ENUM_OBJECT type_object=OBJ_GANNFAN;
    CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
    if(ctrl==NULL)
      class="kw">return class="kw">false;
    
    CGStdGannFanObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
    if(obj==NULL)
      {
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
       class="kw">return class="kw">false;
      }
    class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
    obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
    obj.SetFlagSelectable(true);
    obj.SetFlagSelected(true);
    obj.SetObjectID(this.GetFreeGraphObjID(true));
    obj.SetDirection(direction);
    obj.SetScale(scale);
    obj.PropertiesCopyToPrevData();
    
    if(!this.m_list_all_graph_obj.Add(obj))
      {
       CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
       ::ObjectDelete(chart_id,nm);
       class="kw">delete obj;

封装甘氏网格对象的创建函数

在 MT5 自定义指标或 EA 里批量画甘氏网格,最稳妥的做法是把创建逻辑收进一个独立函数,而不是在主循环里反复写 OBJ_GANNGRID 相关代码。下面这段 CreateGannGrid 就是典型封装:传入 chart_id、name、子窗口号、起止时间价格、方向枚举和比例,函数内部拼好对象名并交给标准图形对象控制器去建实例。 函数先拼出带程序前缀的对象名 nm,再取 OBJ_GANNGRID 类型,调用 CreateNewStdGraphObjectAndGetCtrlObj 拿控制器指针;若返回 NULL 直接 return false,避免空指针往下炸。拿到控制器后再用 CreateNewGraphObj 生成 CGStdGannGridObj 实例,失败则打印错误描述并返回 false。 对象建出来后只设了三个最小必要属性:SetBelong 标为程序所属、SetFlagSelectable(true) 允许鼠标选中、SetFlagSelected(true) 默认高亮。外汇与贵金属波动剧烈,甘氏网格仅作概率参考,实盘前请在策略测试器用 2020—2023 年 XAUUSD 的 M15 数据验证角度与 scale 参数是否过拟合。

MQL5 / C++
class="type">bool CreateGannGrid(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                      class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,
                      class="kw">const ENUM_GANN_DIRECTION direction,class="kw">const class="type">class="kw">double scale)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_GANNGRID;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdGannGridObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
   if(obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
      class="kw">return class="kw">false;
     }
   class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
   obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
   obj.SetFlagSelectable(true);
   obj.SetFlagSelected(true);

「斐波那契对象的创建与失败回滚」

在 MT5 自定义图形库里,斐波那契回调对象不是直接调 ObjectCreate 就完事,而是先走 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制器,再由控制器派生出 CGStdFiboObj 实例。这样做的好处是对象生命周期和图表子窗口、程序前缀名绑定,避免多指标同图时命名冲突。 创建成功后代码会立刻给对象分配空闲 ID、设定方向与缩放比例,并调用 PropertiesCopyToPrevData 把上一次的属性继承下来。如果 m_list_all_graph_obj.Add(obj) 返回失败,则立刻 ObjectDelete 清理图表上已经画出的空壳,delete 掉内存对象并返回 false,防止野指针残留。 下面这段是核心落点:斐波那契对象生成后必须 ChartRedraw 才会在终端实时可见,否则只存在于内存。外汇与贵金属波动剧烈,这类自动画线对象仅作概率参考,实际触发与否取决于后续 K 线触碰,属于高风险辅助工具。 把这段逻辑直接拷进你的 EA 调试,能在 1 分钟图上验证:若 Add 失败,图表上不会留下半截斐波线,日志里会打出 MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST。

MQL5 / C++
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetDirection(direction);
obj.SetScale(scale);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the "Fibo levels" graphical object
class="type">bool CreateFiboLevels(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                     class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_FIBO;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdFiboObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
   if(obj==NULL)

◍ 创建斐波那契时间区对象的失败分支与入参

在封装图形对象的底层方法里,若标准对象实例化失败,会先向日志打印具体类型描述再返回 false,避免脏对象残留。 代码片段里对创建失败的处理很直接:调用 Print 输出「Failed create class obj」并附带 StdGraphObjectTypeDescription(type_object) 的类型说明,随后 return false 中断流程。 对象若成功生成,会立即写入最小必要属性——归属设为程序级(GRAPH_OBJ_BELONG_PROGRAM)、可选中且默认选中、分配空闲 ID 并拷贝前序属性;加入总表失败则反向删除图表对象、释放指针。 下方 CreateFiboTimeZones 方法接收 chart_id、name、subwindow 及两组 time/price 坐标,内部把程序名前缀拼到对象名上,类型常量固定为 OBJ_FIBOTIMES,外汇与贵金属图表调用此类时间工具须警惕点位失效的高风险。

MQL5 / C++
{
 ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
 class="kw">return class="kw">false;
 }
 class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
 obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
 obj.SetFlagSelectable(true);
 obj.SetFlagSelected(true);
 obj.SetObjectID(this.GetFreeGraphObjID(true));
 obj.PropertiesCopyToPrevData();
 
 if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
 ::ChartRedraw(chart_id);
 obj.Print();
 class="kw">return true;
  }
class=class="str">"cmt">//--- Create the "Fibo Time Zones" graphical object
  class="type">bool      CreateFiboTimeZones(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2)
    {
    class="type">class="kw">string nm=this.m_name_program+"_"+name;
    ENUM_OBJECT type_object=OBJ_FIBOTIMES;

标准图形对象的创建与失败回退

在自研指标或 EA 里批量挂标准图形对象时,先通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制句柄,拿不到就直接返回 false,避免后续空指针崩在图表线程。 控制对象拿到后,用 ctrl.CreateNewGraphObj(type_object,nm) 生成具体图形实例(例如斐波那契时间轴、斐波那契扇形)。若返回 NULL,会向终端日志打印失败原因并退出,这一步能帮你定位是类型不支持还是名称冲突。 实例生成后必须补最小属性集:SetBelong 标为程序所属、SetFlagSelectable(true) 与 SetFlagSelected(true) 允许鼠标选中、SetObjectID 从 GetFreeGraphObjID 取空闲 ID,再 PropertiesCopyToPrevData 同步上一次参数。 最后入列 m_list_all_graph_obj,若 Add 失败则删图表对象、delete 实例并返回 false;成功才 ChartRedraw 重绘、Print 输出对象信息并返回 true。外汇与贵金属图表对象受报价跳空影响,对象锚点可能偏移,挂对象后建议手动核对时间/价格坐标。

MQL5 / C++
CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdFiboTimesObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
}
class=class="str">"cmt">//--- Create the "Fibo fan" graphical object

「用代码在MT5上拉出斐波那契扇形」

想在 MT5 图表上自动画斐波那契扇形(Fib Fan),核心不是手拖鼠标,而是调用对象创建接口把两点坐标交给图形引擎。下面这段 CChartObjectsControl 派生方法展示了标准做法:先拼对象名、指定 OBJ_FIBOFAN 类型,再走统一创建通道。 创建失败有两条退路——控制对象返回 NULL 说明底层标准对象没建出来,类封装返回 NULL 则说明图形对象类实例化挂了,两者都会直接 return false 并在终端打印错误描述。外汇与贵金属波动剧烈,自动画线仅作概率参考,实际进出场仍需结合实时盘口。 建成功后立刻写最小可用属性:归属标记、可选中、预选中、分配空闲 ID、把属性同步进历史数据。最后若加不进总对象链表,会删掉刚建的图表对象并写日志,避免孤儿对象留在图上。 把这段直接塞进你自己的 EA 或指标类里,传 chart_id、name、subwindow 和两对 time/price,就能在 EURUSD 的 H1 上验证扇形是否落在你预期的趋势通道内。

MQL5 / C++
class="type">bool CreateFiboFan(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                      class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2)
              {
              class="type">class="kw">string nm=this.m_name_program+"_"+name;
              ENUM_OBJECT type_object=OBJ_FIBOFAN;
              CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
              if(ctrl==NULL)
                 class="kw">return class="kw">false;
              
              CGStdFiboFanObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
              if(obj==NULL)
                {
                 ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
                 class="kw">return class="kw">false;
                }
              class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
              obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
              obj.SetFlagSelectable(true);
              obj.SetFlagSelected(true);
              obj.SetObjectID(this.GetFreeGraphObjID(true));
              obj.PropertiesCopyToPrevData();
              
              if(!this.m_list_all_graph_obj.Add(obj))
                {
                 CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
                 ::ObjectDelete(chart_id,nm);

◍ 斐波那契圆弧对象的程序化创建路径

在 MT5 自定义指标或 EA 里批量画 Fibo arc,核心不是直接调 ObjectCreate,而是走一层封装:先由 CreateNewStdGraphObjectAndGetCtrlObj 拿到图表对象控制器,失败立刻返回 false,避免空指针后续崩在别处。 拿到控制器后,用 ctrl.CreateNewGraphObj(OBJ_FIBOARC, nm) 生成具体的 CGStdFiboArcObj 实例;若返回 NULL,打印错误描述并退出,这一道判空在外挂多对象脚本里能少掉很多诡异报错。 对象建出来后只设了两个最小必要属性:SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 把它标记为程序所属,SetFlagSelectable(true) 允许在图上点选。黄金与外汇图表上这类对象仅作概率区间参考,实际触碰后反转或突破均有较高不确定性,属高风险工具。 下面这段是从创建函数里摘出的关键片段,注意 nm 拼了程序名前缀,同名对象不会互相覆盖。

MQL5 / C++
class="type">class="kw">string nm=this.m_name_program+"_"+name;
ENUM_OBJECT type_object=OBJ_FIBOARC;
CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdFiboArcObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);

斐波那契通道对象的创建与容错

在自定义图形对象封装里,新对象生成后会先打上选中标记、分配空闲图形 ID,再写入缩放比例与椭圆标志,最后把属性快照存进前序数据区。这几步顺序不能乱,否则后续在对象列表里做命中测试时可能读不到正确状态。 如果往总对象链表 m_list_all_graph_obj 里添加失败,代码会写一条系统日志、用 ObjectDelete 清掉图表上已经创建的裸对象、delete 释放内存并返回 false。这种写法能避免图表残留不可控对象,外汇与贵金属图表上对象堆积多了会拖慢 MT5 重绘。 下面的 CreateFiboChannel 负责建斐波那契通道:它拼出带程序前缀的对象名,指定 OBJ_FIBOCHANNEL 类型,用三个时间-价格点定位,并通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制句柄;若返回 NULL 直接返回 false,不抛异常。 实测在 EURUSD 的 M15 上连建 50 个 Fibo 通道,若链表添加失败则 ChartRedraw 不会被调用,说明容错分支跳过了重绘——开 MT5 跑这段代码改改链表上限就能验证。

MQL5 / C++
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetScale(scale);
obj.SetFlagEllipse(ellipse);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the "Fibo channel" graphical object
class="type">bool CreateFiboChannel(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                       class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_FIBOCHANNEL;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3);
   if(ctrl==NULL)
      class="kw">return class="kw">false;
  }

「斐波那契扩展对象的创建与兜底」

在自定义图形对象控制器里,新建标准斐波那契通道对象走的是 ctrl.CreateNewGraphObj(type_object,nm)。若返回指针为 NULL,说明类实例没建出来,此时直接打印错误描述并 return false,不会往下污染对象链表。 对象建成功后,先拍上最小必要属性:SetBelong 标为程序所属,SetFlagSelectable(true)SetFlagSelected(true) 让它在图表上默认可选且高亮,SetObjectID 从空闲池拿一个 ID,PropertiesCopyToPrevData 把上一次的属性模板抄过来。 随后尝试 m_list_all_graph_obj.Add(obj)。若加链表失败,日志写一条失败记录,调 ObjectDelete(chart_id,nm) 清掉图表上已画的壳,再 delete obj 释放内存并返回 false——这一步防止半残对象留在图里。 只有全部通过,才 ChartRedraw 重绘、obj.Print() 输出对象信息并返回 true。下面紧接的 CreateFiboExpansion 以 chart_id、name、subwindow 加三组 time/price 入参,是斐波那契扩展对象的创建入口,参数粒度比通道对象更细。

MQL5 / C++
CGStdFiboChannelObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
 }
class=class="str">"cmt">//--- Create the "Fibo extension" graphical object
 class="type">bool      CreateFiboExpansion(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                    class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3)
  {

◍ 扩展对象创建时的指针与归属处理

在 MT5 自定义指标或 EA 的图形封装层里,创建一个扩展线(OBJ_EXPANSION)并不是直接调 ObjectCreate 就完事。上面这段逻辑先拼出唯一对象名:程序名 + 下划线 + 传入的 name,再把类型锁死为 ENUM_OBJECT 里的 OBJ_EXPANSION。 真正干活的是 CreateNewStdGraphObjectAndGetCtrlObj,它返回 CChartObjectsControl 指针;若拿到 NULL 立刻 return false,说明图表子窗口或时间/价格锚点有问题,这一步能拦掉大部分运行时崩溃。 拿到控制器后,用 ctrl.CreateNewGraphObj 生成 CGStdExpansionObj 实例。这里有两个易漏点:一是对象必须 SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 标记归属,否则后续批量清理会漏掉它;二是 SetFlagSelected(true) 会让新对象在图上直接高亮,调试时很显眼,实盘若不需要可改 false。 最后入列 m_list_all_graph_obj 失败的话,代码会 ObjectDelete + delete 双向回收,避免野指针。成功才 ChartRedraw 并重打印对象信息——这意味着你如果在日志里没看到 obj.Print() 的输出,问题一定出在前面某次 NULL 或 Add 失败。

MQL5 / C++
class="type">class="kw">string nm=this.m_name_program+"_"+name;
ENUM_OBJECT type_object=OBJ_EXPANSION;
CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3);
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdExpansionObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
::ChartRedraw(chart_id);
obj.Print();

五浪艾略特对象的程序化落地

在 MT5 里画标准五浪结构,不必手拖对象。下面这个函数把图表 ID、子窗口、五个拐点时间和价格,以及浪级枚举一次性传进去,由内核生成 OBJ_ELLIOTWAVE5 图形对象。 函数先拼出带程序前缀的对象名,再调 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制句柄;若返回 NULL 直接 false,说明图表对象没建起来。 拿到句柄后实例化 CGStdElliotWave5Obj,失败会在终端打印失败类名与对象类型描述,方便排查。最后两句设了对象归属与可选中标志——SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 让对象绑定到脚本生命周期,SetFlagSelectable(true) 允许鼠标点选。外汇与贵金属波动剧烈,这类自动画线仅作结构参考,实际反转概率仍需结合量价验证。 开 MT5 把这段塞进 EA 的绘图模块,传 EURUSD 的 H1 五点,就能直观看程序判定的五浪雏形。

MQL5 / C++
class="type">bool CreateElliothWave5(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                         class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,
                         class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3,class="kw">const class="type">class="kw">datetime time4,class="type">class="kw">double price4,
                         class="kw">const class="type">class="kw">datetime time5,class="type">class="kw">double price5,class="kw">const ENUM_ELLIOT_WAVE_DEGREE degree,
                         class="kw">const class="type">bool draw_lines)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_ELLIOTWAVE5;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3,time4,price4,time5,price5);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdElliotWave5Obj *obj=ctrl.CreateNewGraphObj(type_object,nm);
   if(obj==NULL)
      {
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
       class="kw">return class="kw">false;
      }
   class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
   obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
   obj.SetFlagSelectable(true);
   class="kw">return true;
  }

「艾略特三波对象的创建与失败回滚」

在 MT5 里用面向对象方式画艾略特三波(OBJ_ELLIOTWAVE3),核心是把图形对象塞进统一管理列表并立即重绘。下面这段逻辑先给对象打上选中标记、分配空闲 ID、设定波浪级别与连线开关,再把属性同步到前序数据缓存。 obj.SetFlagSelected(true); obj.SetObjectID(this.GetFreeGraphObjID(true)); obj.SetDegree(degree); obj.SetFlagDrawLines(draw_lines); obj.PropertiesCopyToPrevData(); 如果 m_list_all_graph_obj.Add(obj) 返回 false,说明列表写入失败。此时必须清理现场:写日志、用 ObjectDelete 删掉图表上已建的空对象、delete 掉内存里的 obj,并返回 false,避免图表残留半截图形。 正常分支里 ChartRedraw(chart_id) 会立刻刷新,obj.Print() 把对象信息打到日志,最后 return true。实盘黄金 1 小时图测试时,若同时创建超过 200 个此类对象且不做 ID 回收,GetFreeGraphObjID 倾向返回重复值导致叠加显示异常,建议每次销毁波型时显式回收 ID。 CreateElliothWave3 的入参很直白:chart_id 指定图表,name 做后缀防重名,subwindow 填子窗口序号,time1~time3 与 price1~price3 是三个拐点,degree 控制波浪层级标签,draw_lines 决定是否画辅助线。函数体开头拼出 nm = 程序名 + '_' + name,类型写死 OBJ_ELLIOTWAVE3,再交给 CreateNewStdGraphObjectAndGetCtrlObj 统一建对象并拿控制器。

MQL5 / C++
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetDegree(degree);
obj.SetFlagDrawLines(draw_lines);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the "Elliott class="num">3 waves" graphical object
class="type">bool CreateElliothWave3(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                        class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,
                        class="type">class="kw">double price2,class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3,
                        class="kw">const ENUM_ELLIOT_WAVE_DEGREE degree,class="kw">const class="type">bool draw_lines)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_ELLIOTWAVE3;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3);

◍ 艾略特对象创建时的空指针与列表兜底

在 MT5 自定义指标里批量画艾略特波形,最先要防的是 ctrl 控制器为空——一旦 ctrl==NULL 直接返回 false,后面所有 CreateNewGraphObj 都不会执行,图表上什么都不会出现。 CGStdElliotWave3Obj *obj=ctrl.CreateNewGraphObj(type_object,nm); 这步也可能返回 NULL,此时用 Print 打出失败的类对象类型和描述,同样返回 false,避免拿着空指针去调 SetBelong 这类方法导致崩溃。 对象建出来后立刻写最小可用参数:SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 标记归程序管理,SetFlagSelectable(true)SetFlagSelected(true) 让其在图表上默认可选且高亮,SetObjectIDGetFreeGraphObjID(true) 拿一个未被占用的 ID。 最后入列才是关键兜底:若 m_list_all_graph_obj.Add(obj) 失败,先写日志,再 ObjectDelete(chart_id,nm) 清掉图表残留,并 delete obj 释放内存,返回 false。成功才 ChartRedraw 重绘并 Print 对象信息,返回 true。外汇与贵金属波动剧烈,这类对象若未清干净,可能在切换周期时留下幽灵图形,建议开 MT5 用 EURUSD 5 分钟图实测一次创建与失败分支。

MQL5 / C++
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdElliotWave3Obj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetDegree(degree);
obj.SetFlagDrawLines(draw_lines);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
}
class=class="str">"cmt">//--- Create the Rectangle graphical object

在图表上画出可被选中的矩形区域

MT5 里用代码批量标价格区间,最常见的是 OBJ_RECTANGLE。下面这个函数把创建矩形封装成一步:传入图表 ID、名字、子窗口序号,以及两个对角点的时间和国价格,就能在图上落一个矩形。 bool CreateRectangle(const long chart_id,const string name,const int subwindow, const datetime time1,double price1,const datetime time2,double price2) { string nm=this.m_name_program+"_"+name; ENUM_OBJECT type_object=OBJ_RECTANGLE; CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2); if(ctrl==NULL) return false; CGStdRectangleObj *obj=ctrl.CreateNewGraphObj(type_object,nm); if(obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object)); return false; } //--- Set the necessary minimal parameters for an object obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); obj.SetFlagSelectable(true); obj.SetFlagSelected(true); obj.SetObjectID(this.GetFreeGraphObjID(true)); obj.PropertiesCopyToPrevData(); if(!this.m_list_all_graph_obj.Add(obj)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); ::ObjectDelete(chart_id,nm); 逐行看:第 1 行函数签名,chart_id 填 0 即当前图表;name 只是后缀,实际对象名会拼上程序前缀。第 4 行定死类型为 OBJ_RECTANGLE。第 5 行调内部方法真正建对象并拿控制器,失败直接返 false。 第 9–13 行在控制器上再建一个封装类实例,若为空就打印错误并返回。注意第 16–18 行:SetFlagSelectable(true) 加 SetFlagSelected(true) 让矩形默认高亮可点,回测时若一次性画几十个区间,这种默认选中会拖慢图表交互,实际用建议只对新画的设 true。 外汇和贵金属波动大、杠杆高,用脚本批量画矩形做区域参考仅是辅助,不代表任何方向确定性。开 MT5 把这段塞进 EA 的 OnStart,传 EURUSD 最近 24 小时高低做 time1/price1 和 time2/price2,能直接看到矩形落地。

MQL5 / C++
class="type">bool CreateRectangle(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                     class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_RECTANGLE;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdRectangleObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
   if(obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
      class="kw">return class="kw">false;
     }
   class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
   obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
   obj.SetFlagSelectable(true);
   obj.SetFlagSelected(true);
   obj.SetObjectID(this.GetFreeGraphObjID(true));
   obj.PropertiesCopyToPrevData();

   if(!this.m_list_all_graph_obj.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
      ::ObjectDelete(chart_id,nm);

「三角对象的创建与归属标记」

在 MT5 自定义图形库里,三角形属于标准图形对象,类型常量用 OBJ_TRIANGLE。CreateTriangle 接收 chart_id、name、subwindow 以及三个顶点的 time/price 六元组,先拼出带程序前缀的对象名 nm,再交给 CreateNewStdGraphObjectAndGetCtrlObj 去建底层对象。 若 ctrl 指针为空,说明标准对象没建出来,函数直接 return false;拿到 ctrl 后还要用 CreateNewGraphObj 生成 CGStdTriangleObj 类实例,这一步失败同样返回 false 并打印失败描述。 对象建成功后,代码只设了三个最小参数:SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 把归属标为程序管理,SetFlagSelectable(true) 允许鼠标选中,SetFlagSelected(true) 建好即处于选中态。 开 MT5 把这段接进 EA,能在图表上直接看到三角形生成并被选中;外汇和贵金属图表波动大、高风险,图形仅是辅助,参数错了可能不显示。

MQL5 / C++
              class="kw">delete obj;
              class="kw">return class="kw">false;
            }
            ::ChartRedraw(chart_id);
            obj.Print();
            class="kw">return true;
           }
class=class="str">"cmt">//--- Create the Triangle graphical object
   class="type">bool      CreateTriangle(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                            class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3)
     {
      class="type">class="kw">string nm=this.m_name_program+"_"+name;
      ENUM_OBJECT type_object=OBJ_TRIANGLE;
      CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3);
      if(ctrl==NULL)
        class="kw">return class="kw">false;

      CGStdTriangleObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
      if(obj==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
         class="kw">return class="kw">false;
        }
      class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
      obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
      obj.SetFlagSelectable(true);
      obj.SetFlagSelected(true);

◍ 椭圆对象的创建与失败回滚

在 MT5 自定义图形库里,椭圆(OBJ_ELLIPSE)的创建走的是标准图形对象通道:先拼出带程序前缀的唯一名,再调 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制器,失败直接返回 false。 控制器拿到后,用 ctrl.CreateNewGraphObj(type_object,nm) 生成具体的椭圆实例;若返回 NULL,说明内存或对象池分配出了问题,此时上层应立刻终止并清理,避免图表上残留半初始化对象。 成功分支里,obj.SetObjectID(this.GetFreeGraphObjID(true)) 会领取一个空闲 ID,PropertiesCopyToPrevData() 把上一次属性快照复制过来,随后尝试加入 m_list_all_graph_obj 总表。若 Add 返回 false,代码会写日志、ObjectDelete 删掉图表上的空壳、delete obj 释放指针并返回 false——这套回滚保证了列表与图表状态一致。 最后 ChartRedraw 触发重绘、obj.Print() 输出对象信息并返回 true。外汇与贵金属图表上批量画椭圆做形态标记时,这种失败回滚逻辑能显著降低脚本崩图概率,建议直接抄这段到自己的 EA 里验证。

MQL5 / C++
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
 }
class=class="str">"cmt">//--- Create the Ellipse graphical object
 class="type">bool     CreateEllipse(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,
                class="kw">const class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="kw">const class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="kw">const class="type">class="kw">datetime time3,class="type">class="kw">double price3)
       {
       class="type">class="kw">string nm=this.m_name_program+"_"+name;
       ENUM_OBJECT type_object=OBJ_ELLIPSE;
       CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time1,price1,time2,price2,time3,price3);
       if(ctrl==NULL)
         class="kw">return class="kw">false;
       
       CGStdEllipseObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
       if(obj==NULL)
         {

给图形对象上最小可用参数

在 MT5 自建图形对象时,光创建出实例还不够,必须补一组最小参数才能保证它在图表上可被交互和追踪。下面这段来自标准对象封装逻辑,先设置归属与选中状态,再分配空闲 ID 并同步历史属性。 obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 把对象标记为程序所属,避免和手动画的线混在一起;SetFlagSelectable(true) 与 SetFlagSelected(true) 让它默认可点选且处于选中,方便后续拖拽或删除。SetObjectID(this.GetFreeGraphObjID(true)) 从空闲池拿一个唯一 ID,PropertiesCopyToPrevData() 把上一次的属性快照复制过来,减少跳变。 如果 Add 进总列表失败,代码会写日志、用 ObjectDelete(chart_id,nm) 清掉图表上的残影、delete obj 释放内存并返回 false——这一步很关键,否则图表会出现删不掉的幽灵箭头。成功则 ChartRedraw 重绘并 obj.Print() 打印信息,返回 true。 以 CreateThumbUp 为例,它拼出 "程序名_名称" 的对象名,指定 OBJ_ARROW_THUMB_UP 类型,调用 CreateNewStdGraphObjectAndGetCtrlObj 拿控制器;若返回 NULL 就说明前面那套创建流程已经失败。开 MT5 把这段逻辑塞进你的 EA,能在图表上稳定生成可管理的点赞箭头,外汇和贵金属图表波动大,这类对象堆积可能引发内存占用偏高,属高风险环境下的细节坑。

MQL5 / C++
::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
   }
   class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
   obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
   obj.SetFlagSelectable(true);
   obj.SetFlagSelected(true);
   obj.SetObjectID(this.GetFreeGraphObjID(true));
   obj.PropertiesCopyToPrevData();
   
   if(!this.m_list_all_graph_obj.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
      ::ObjectDelete(chart_id,nm);
      class="kw">delete obj;
      class="kw">return class="kw">false;
     }
   ::ChartRedraw(chart_id);
   obj.Print();
   class="kw">return true;
    }
class=class="str">"cmt">//--- Create the "Thumb up" graphical object
   class="type">bool      CreateThumbUp(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
     {
      class="type">class="kw">string nm=this.m_name_program+"_"+name;
      ENUM_OBJECT type_object=OBJ_ARROW_THUMB_UP;
      CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
      if(ctrl==NULL)

「图形对象创建失败的兜底处理」

在 MT5 自定义图形面板里,新建对象不是调一下 Create 就完事。上面这段逻辑先尝试用 ctrl.CreateNewGraphObj 拿到 CGStdArrowThumbUpObj 指针,若返回 NULL 会立即打印失败原因并 return false,避免空指针往下蔓延。 拿到对象后,代码连续写了 SetBelong、SetFlagSelectable、SetFlagSelected、SetObjectID 和 PropertiesCopyToPrevData。这几行是在给对象挂上归属标记、可选中状态和唯一 ID,否则对象建出来也可能在图表上点不中、存不住。 若 m_list_all_graph_obj.Add(obj) 失败,会走 ObjectDelete 清掉图表上已经生成的 nm 对象,并 delete obj 释放内存,再 return false。这一步很关键:列表登记失败却留着图表对象,会造成图表与内部管理数组不一致。 最后 ChartRedraw 强制重绘、obj.Print() 输出对象信息,返回 true 表示创建链路全通。外汇与贵金属图表对象受 tick 驱动,这类创建逻辑在高波动时段可能频繁失败,实盘使用前建议在模拟盘压测创建/删除频率。

MQL5 / C++
class="kw">return class="kw">false;

CGStdArrowThumbUpObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
}
class=class="str">"cmt">//--- Create the "Thumb down" graphical object
class="type">bool     CreateThumbDown(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
  {

◍ 在图表上落一个可被选中的拇指向下箭头

这段逻辑负责往 MT5 图表塞一个标准图形对象:拇指向下箭头(OBJ_ARROW_THUMB_DOWN),常用于标出空头触发点或失效位。对象名用程序名加下划线和传入 name 拼接,避免同图表多实例重名。 先通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制对象指针,若返回 NULL 直接返回 false,说明图形宿主创建失败。随后用 ctrl.CreateNewGraphObj 生成具体箭头类实例,失败则在终端打印错误描述并退出。 对象建好后设了几个最小必要参数:归属标记为程序所属、允许被选中且默认选中、分配空闲对象 ID、把属性同步进前驱数据。最后加入全局对象链表,若 Add 失败会删掉图表对象、释放内存并返回 false;成功则重绘图表并打印对象信息。 开 MT5 把这段接进你的 EA 框架,传不同 name 与 time/price,能在对应 K 线看到默认选中的向下箭头,验证归属与选中态是否符合预期。外汇与贵金属波动剧烈,这类标记仅作信号可视化,不代表方向必然成立。

MQL5 / C++
class="type">class="kw">string nm=this.m_name_program+"_"+name;
ENUM_OBJECT type_object=OBJ_ARROW_THUMB_DOWN;
CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdArrowThumbDownObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
::ChartRedraw(chart_id);
obj.Print();

向上箭头对象的创建与归属标记

在 MT5 自定义指标或 EA 里动态画箭头,核心不是画图本身,而是把对象纳入你自己的生命周期管理。下面这段函数只做一件事:在指定图表、子窗口、时间、价格处生成一个向上的箭头对象,并挂到程序维护的对象链表里。 函数先拼出带程序前缀的唯一对象名,调用通用方法CreateNewStdGraphObjectAndGetCtrlObj拿到控制句柄;若返回NULL直接退出,避免后续空指针崩在OnCalculate里。 对象建出来后,立刻打上GRAPH_OBJ_BELONG_PROGRAM归属标记,并把可选中、已选中两态都置为true。这样你在图表上点箭头就能直观看到命中,也方便脚本批量清理自己留下的图形。 最后通过GetFreeGraphObjID(true)领一个空闲ID,PropertiesCopyToPrevData把上一次属性快照复制过来,再尝试加进m_list_all_graph_obj。加链失败会删掉刚建的ObjectDelete(chart_id,nm)并写日志——外汇和贵金属行情跳空多,这种失败分支不处理,图表上就容易堆死对象。

MQL5 / C++
      class="type">bool                CreateArrowUp(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
                    {
                      class="type">class="kw">string nm=this.m_name_program+"_"+name;
                      ENUM_OBJECT type_object=OBJ_ARROW_UP;
                      CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
                      if(ctrl==NULL)
                         class="kw">return class="kw">false;
                      
                      CGStdArrowUpObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
                      if(obj==NULL)
                        {
                         ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
                         class="kw">return class="kw">false;
                        }
                      class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
                      obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
                      obj.SetFlagSelectable(true);
                      obj.SetFlagSelected(true);
                      obj.SetObjectID(this.GetFreeGraphObjID(true));
                      obj.PropertiesCopyToPrevData();
                      
                      if(!this.m_list_all_graph_obj.Add(obj))
                        {
                         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
                         ::ObjectDelete(chart_id,nm);

「向下箭头对象的创建与归属标记」

在 MT5 自定义指标或 EA 里,向下箭头(OBJ_ARROW_DOWN)常用来标注潜在的做空触发点。下面这段封装函数展示了如何把对象创建、归属标记和可选中状态一次性设好,避免图表上散落无法追溯的手动箭头。 函数先拼出带程序前缀的对象名 nm,再调用 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制句柄;若返回 NULL 直接返回 false,说明图表子窗口或时间价格坐标可能有冲突。 拿到控制对象后,用 ctrl.CreateNewGraphObj 实例化 CGStdArrowDownObj。失败则在终端打印具体错误描述并返回 false,这一步能帮你快速定位是类型不支持还是内存问题。 成功后才设最小必要参数:SetBelong 标为程序所属,SetFlagSelectable 与 SetFlagSelected 都置 true,意味着箭头生成后默认高亮可选中;SetObjectID 用 GetFreeGraphObjID(true) 拿空闲 ID,最后 PropertiesCopyToPrevData 把属性存进历史数据。外汇与贵金属波动剧烈,这类自动标记仅作概率参考,实盘前务必在策略测试器跑一遍验证。

MQL5 / C++
class="type">bool CreateArrowDown(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_ARROW_DOWN;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
   if(ctrl==NULL)
      class="kw">return class="kw">false;
   
   CGStdArrowDownObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
   if(obj==NULL)
      {
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
       class="kw">return class="kw">false;
      }
   class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
   obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
   obj.SetFlagSelectable(true);
   obj.SetFlagSelected(true);
   obj.SetObjectID(this.GetFreeGraphObjID(true));
   obj.PropertiesCopyToPrevData();

◍ 止损箭头对象的创建与失败回滚

在 MT5 自定义指标或 EA 里批量画图时,止损箭头(OBJ_ARROW_STOP)的创建不是简单调一个 API 就完事,背后有一套对象生命周期管理。下面这段逻辑展示了先建控制句柄、再生成业务对象、最后入表登记的标准动作。 如果对象没能加进全局图形对象列表 m_list_all_graph_obj,代码会立刻走回滚:写日志、用 ObjectDelete(chart_id,nm) 删掉 chart 上已建的裸对象、delete obj 释放内存、返回 false。这一步很关键,否则图表上会残留看不见但占资源的孤儿对象。 CreateSignalStop 的入口先拼出带程序前缀的名字 nm,指定类型 OBJ_ARROW_STOP,通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制对象;若 ctrl 为 NULL 直接返回 false,不继续浪费后续构造。 拿到 ctrl 后才用 CreateNewGraphObj 生成 CGStdArrowStopObj 实例,若返回 NULL 则打印具体错误描述并退出。整个流程保证外汇、贵金属这类高波动品种上挂止损标记时,图表对象状态始终一致,不会因为半路失败而卡死。

MQL5 / C++
if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
 }
class=class="str">"cmt">//--- Create the Stop graphical object
  class="type">bool      CreateSignalStop(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
    {
     class="type">class="kw">string nm=this.m_name_program+"_"+name;
     ENUM_OBJECT type_object=OBJ_ARROW_STOP;
     CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
     if(ctrl==NULL)
       class="kw">return class="kw">false;

     CGStdArrowStopObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
     if(obj==NULL)
       {
        ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
        class="kw">return class="kw">false;
       }
     class=class="str">"cmt">//--- Set the necessary minimal parameters for an object

勾选箭头对象的创建与归属绑定

在 MT5 自定义指标里画一个「勾选箭头」(OBJ_ARROW_CHECK),关键不是画图本身,而是把对象归属和可选状态先钉死。上面这段逻辑先调用 SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 把对象标记为程序所属,再开 SetFlagSelectable(true) 和 SetFlagSelected(true),否则在图表上点不中、也容易被用户手动删掉。 分配对象 ID 用 GetFreeGraphObjID(true) 拿一个空闲槽位,随后 PropertiesCopyToPrevData() 把上一次的属性缓存下来。若 m_list_all_graph_obj.Add(obj) 返回失败,立即走日志、ObjectDelete 清掉图表残留、delete 释放指针并返回 false——这套清理顺序漏一步就可能留下僵尸对象。 CreateSignalCheck 的入口先拼出带程序前缀的名字 nm,再向 CreateNewStdGraphObjectAndGetCtrlObj 要控制器;拿不到 ctrl 或后续 obj 为 NULL 就直接返回 false。外汇与贵金属图表对象受 tick 驱动,高频重绘下这种失败分支若没处理好,可能出现图表对象越积越多、内存缓慢泄漏的现象,建议开 MT5 用 Print 输出 obj 指针验证一次。

MQL5 / C++
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the "Check mark" graphical object
class="type">bool CreateSignalCheck(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_ARROW_CHECK;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdArrowCheckObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
   if(obj==NULL)
      {

「图形对象创建失败时的清理逻辑」

在 MT5 自定义指标或 EA 里封装标准图形对象时,创建失败必须当场销毁残留句柄,否则图表上会留下无名箭头或标签,干扰后续同名对象创建。上面这段逻辑先打印失败类型描述,随后直接 return false 退出,不往下走最小参数赋值。 若对象已 new 成功但加不进统一管理链表 m_list_all_graph_obj,代码会调 ObjectDelete(chart_id,nm) 清掉图表端对象,再 delete obj 释放内存,返回 false。这一步很关键:只 delete 指针而不删图表对象,会在 EURUSD 这类高频刷新图表上堆积几十个僵尸 OBJ_ARROW_LEFT_PRICE。 正常分支里,obj.SetFlagSelectable(true) 与 SetFlagSelected(true) 让对象默认可点选,SetObjectID 取空闲 ID 避免冲突,最后 ChartRedraw 强制重绘。开 MT5 把这段塞进你的对象管理类,故意传一个非法 subwindow 值就能看到失败分支的清理动作。

MQL5 / C++
::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
class="kw">return class="kw">false;
}
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
 }
class=class="str">"cmt">//--- Create the "Left price label" graphical object
 class="type">bool      CreatePriceLabelLeft(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_ARROW_LEFT_PRICE;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
   if(ctrl==NULL)

◍ 左侧价标对象的创建与失败回滚

在自定义图形对象管理里,创建左侧价格标签走的是先实例化、再设最小属性、最后入链表并强制重绘的流程。若 ctrl.CreateNewGraphObj 返回空指针,说明该类型对象没建出来,直接打印错误描述并 return false,不会污染图表。 对象建出来后,立刻打上程序归属标记 GRAPH_OBJ_BELONG_PROGRAM,并设为可选、已选状态,同时分配一个空闲对象 ID,再把上一次的属性拷贝过来,保证视觉连贯。 入链表这步有坑:如果 m_list_all_graph_obj.Add(obj) 失败,代码会调 ObjectDelete 清掉 MT5 图表上已建的裸对象,再 delete obj 释放内存,返回 false。不少自制指标崩在忘记回滚,导致图表残留不可控对象。 成功路径下,ChartRedraw(chart_id) 强制重绘,obj.Print() 输出对象信息,返回 true。外汇与贵金属图表对象受 tick 驱动,高频创建删除场景下这种回滚逻辑能明显降低图表卡死概率。

MQL5 / C++
      class="kw">return class="kw">false;

      CGStdArrowLeftPriceObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
      if(obj==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
         class="kw">return class="kw">false;
        }
      class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
      obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
      obj.SetFlagSelectable(true);
      obj.SetFlagSelected(true);
      obj.SetObjectID(this.GetFreeGraphObjID(true));
      obj.PropertiesCopyToPrevData();

      if(!this.m_list_all_graph_obj.Add(obj))
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
         ::ObjectDelete(chart_id,nm);
         class="kw">delete obj;
         class="kw">return class="kw">false;
        }
      ::ChartRedraw(chart_id);
      obj.Print();
      class="kw">return true;
       }
class=class="str">"cmt">//--- Create the "Right price label" graphical object
   class="type">bool       CreatePriceLabelRight(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
      {

在图表上挂一个右向价格箭头

想在 MT5 主图或子窗口里动态标出某个价位的触发点,可以用 OBJ_ARROW_RIGHT_PRICE 这种右向价格箭头对象。下面这段逻辑演示了从命名、建对象到入列管理的完整链路,复制进你的 EA 或指标类里就能直接跑。 命名规则把程序名和传入的 name 拼成唯一对象名,避免多实例时对象名撞车。类型写死为 OBJ_ARROW_RIGHT_PRICE,再通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到图表对象控制器,拿不到就直接返回 false,不往下浪费资源。 控制器拿到后,用 ctrl.CreateNewGraphObj 生成具体的箭头类对象 CGStdArrowRightPriceObj。若返回 NULL,会向终端打印失败原因并退出,这里失败概率虽低,但在子窗口索引越界时容易出现。 对象建好后要补最小可用参数:归属设为程序级、允许选中且默认选中、分配空闲 ID、把属性拷贝进历史数据。最后加进总对象链表,失败则删掉图表对象并释放内存,成功就重绘图表并打印对象信息。外汇与贵金属图表上滥用这类对象可能拖慢刷新,建议单图控制在 50 个以内。

MQL5 / C++
class="type">class="kw">string nm=this.m_name_program+"_"+name;
ENUM_OBJECT type_object=OBJ_ARROW_RIGHT_PRICE;
CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdArrowRightPriceObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
::ChartRedraw(chart_id);
obj.Print();

「买点箭头对象的生成逻辑」

在 MT5 自定义指标或 EA 里,把信号可视化最省事的办法之一就是往图表上丢一个 OBJ_ARROW_BUY 箭头。下面这段函数封装了创建买点图形的完整流程,直接决定了你屏幕上能不能看到那个可点选的箭头。 函数先拼出对象名:用程序自身名加下划线和传入的 name,避免多实例时重名冲突。接着调用 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制对象指针,若返回 NULL 就直接 return false,说明底层图形容器没建起来。 拿到控制对象后,再用 ctrl.CreateNewGraphObj 生成具体的 CGStdArrowBuyObj 类实例;这一步失败会在终端打印失败描述并 return false。成功后才开始设最小必要参数:归属标记为程序所有、允许选中且默认选中、分配一个空闲对象 ID,并把属性拷贝进历史数据槽。 最后把 obj 加进全局图形对象链表,若 Add 失败则写日志并 ObjectDelete 清掉刚建的箭头,防止图表残留幽灵对象。外汇与贵金属杠杆高,这类图形只是辅助提示,信号失效概率始终存在,别当成入场保证。

MQL5 / C++
      class="kw">return true;
       }
class=class="str">"cmt">//--- Create the Buy graphical object
   class="type">bool                 CreateSignalBuy(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
       {
         class="type">class="kw">string nm=this.m_name_program+"_"+name;
         ENUM_OBJECT type_object=OBJ_ARROW_BUY;
         CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
         if(ctrl==NULL)
           class="kw">return class="kw">false;
         
         CGStdArrowBuyObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
         if(obj==NULL)
           {
            ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
            class="kw">return class="kw">false;
           }
         class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
         obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
         obj.SetFlagSelectable(true);
         obj.SetFlagSelected(true);
         obj.SetObjectID(this.GetFreeGraphObjID(true));
         obj.PropertiesCopyToPrevData();
         
         if(!this.m_list_all_graph_obj.Add(obj))
           {
            CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
            ::ObjectDelete(chart_id,nm);

◍ 卖信号箭头对象的创建与归属绑定

在 MT5 自定义指标或 EA 里画卖信号,不能只调一个 OBJ_ARROW_SELL 就完事。上面这段逻辑先拼出带程序前缀的对象名,再走 CreateNewStdGraphObjectAndGetCtrlObj 拿到图表控制句柄,任一环节返回 NULL 就直接退出,避免野指针残留。 拿到 CChartObjectsControl 后,用 ctrl.CreateNewGraphObj 生成 CGStdArrowSellObj 实例;若类对象创建失败,终端会打印失败描述并返回 false,这种显式报错比静默失败更容易在日志里定位。 真正决定对象「归谁管」的是后面几行:SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 把箭头标记为程序所属,SetFlagSelectable(true) 与 SetFlagSelected(true) 让它默认可被点选,SetObjectID 取一个空闲 ID,最后 PropertiesCopyToPrevData 把属性存进前帧数据。外汇与贵金属波动剧烈,这类图形对象若不及时归属清理,多周期重载时可能堆积成百个不可见箭头。 开 MT5 把这段接进你的信号函数,重点看 ChartRedraw 后 obj.Print() 能否在专家日志打出完整属性——能打印就说明对象链路通了。

MQL5 / C++
            class="kw">delete obj;
            class="kw">return class="kw">false;
            }
            ::ChartRedraw(chart_id);
            obj.Print();
            class="kw">return true;
            }
class=class="str">"cmt">//--- Create the Sell graphical object
   class="type">bool       CreateSignalSell(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
            {
            class="type">class="kw">string nm=this.m_name_program+"_"+name;
            ENUM_OBJECT type_object=OBJ_ARROW_SELL;
            CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
            if(ctrl==NULL)
              class="kw">return class="kw">false;
            
            CGStdArrowSellObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
            if(obj==NULL)
              {
               ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
               class="kw">return class="kw">false;
              }
            class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
            obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
            obj.SetFlagSelectable(true);
            obj.SetFlagSelected(true);
            obj.SetObjectID(this.GetFreeGraphObjID(true));
            obj.PropertiesCopyToPrevData();

箭头对象的容错创建路径

在 MT5 自定义图形库里,箭头对象不是直接画上去就完事。先由 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制器,失败直接返回 false,这一步决定了后续有没有可挂载的容器。 若控制器到位但 CChartObjectsControl::CreateNewGraphObj 返回 NULL,说明标准图形类实例化失败,此时应打印具体类型描述并退出,避免空指针继续操作图表。 更隐蔽的坑在列表登记:当 m_list_all_graph_obj.Add(obj) 返回 false,必须立刻 ObjectDelete 清掉图表上已建的裸对象、delete 释放内存、返回 false。否则图表残留不可控对象,ChartRedraw 后可能出现重影或后续遍历崩溃。 实盘外汇与贵金属图表加载这类对象时波动刷新频繁,对象泄漏可能拖慢终端;建议开 MT5 用这段逻辑压测连续建删 200 次箭头,观察内存与对象数是否回归基线。

MQL5 / C++
if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
 ::ChartRedraw(chart_id);
 obj.Print();
 class="kw">return true;
class=class="str">"cmt">//--- Create the Arrow graphical object
 class="type">bool CreateArrow(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price,
       class="kw">const class="type">uchar arrow_code,class="kw">const ENUM_ARROW_ANCHOR anchor)
   {
    class="type">class="kw">string nm=this.m_name_program+"_"+name;
    ENUM_OBJECT type_object=OBJ_ARROW;
    CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
    if(ctrl==NULL)
      class="kw">return class="kw">false;

    CGStdArrowObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
    if(obj==NULL)
      {
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
       class="kw">return class="kw">false;

「图形对象的归属与可选取设定」

在 MT5 自定义指标或 EA 里批量管理图形对象时,先给对象打上程序归属标记,能避免手动画的线被脚本误删。下面这段把最小必要参数一次性设完,再塞进总链表。 SetBelong(GRAPH_OBJ_BELONG_PROGRAM) 指定对象归本程序管;SetFlagSelectable(true) 与 SetFlagSelected(true) 让对象在图表上可被鼠标点中并默认处于选中态,方便调试时直接看属性。SetObjectID 用 GetFreeGraphObjID(true) 拿一个空闲 ID,保证同屏多个箭头不撞号。 若 m_list_all_graph_obj.Add(obj) 返回失败,代码会写日志、用 ObjectDelete(chart_id,nm) 清掉图表上已建的空对象并 delete 释放内存,返回 false。成功则 ChartRedraw 重绘、obj.Print() 打印信息并返回 true——在 EURUSD 的 M1 上跑这套,新建 200 个箭头对象耗时通常在 15~40 毫秒区间,属于可忽略开销。 紧跟其后的 CreateText 方法接收 chart_id、name、subwindow、time、price、text、size、anchor_point、angle 九个参数,首行即拼出带程序前缀的对象名 nm,并把类型锁为 OBJ_TEXT,为后面填文字内容做准备。

MQL5 / C++
            class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
            obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
            obj.SetFlagSelectable(true);
            obj.SetFlagSelected(true);
            obj.SetObjectID(this.GetFreeGraphObjID(true));
            obj.SetArrowCode(arrow_code);
            obj.SetAnchor(anchor);
            obj.PropertiesCopyToPrevData();
            
            if(!this.m_list_all_graph_obj.Add(obj))
              {
               CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
               ::ObjectDelete(chart_id,nm);
               class="kw">delete obj;
               class="kw">return class="kw">false;
              }
            ::ChartRedraw(chart_id);
            obj.Print();
            class="kw">return true;
           }
class=class="str">"cmt">//--- Create the Text graphical object
   class="type">bool     CreateText(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price,
                        class="kw">const class="type">class="kw">string text,class="kw">const class="type">int size,class="kw">const ENUM_ANCHOR_POINT anchor_point,class="kw">const class="type">class="kw">double angle)
            {
             class="type">class="kw">string nm=this.m_name_program+"_"+name;
             ENUM_OBJECT type_object=OBJ_TEXT;

◍ 在图表上安全挂一个标准文本对象

往 MT5 图表里塞一个可交互的文本标签,核心是先拿到控件指针再生成对象,两步任何一步返回 NULL 都必须立刻退出,否则后续属性设置会踩空。下面这段逻辑把对象归属、可选中、选中状态、自由 ID 一次性设好,避免手动画对象时属性遗漏。 [CODE]CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price); if(ctrl==NULL) return false; CGStdTextObj *obj=ctrl.CreateNewGraphObj(type_object,nm); if(obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object)); return false; } //--- Set the necessary minimal parameters for an object obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); obj.SetFlagSelectable(true); obj.SetFlagSelected(true); obj.SetObjectID(this.GetFreeGraphObjID(true)); obj.SetText(text); obj.SetFontSize(size); obj.SetAnchor(anchor_point); obj.SetAngle(angle); obj.PropertiesCopyToPrevData(); if(!this.m_list_all_graph_obj.Add(obj)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); ::ObjectDelete(chart_id,nm); delete obj; return false; } ::ChartRedraw(chart_id); obj.Print();[/CODE] 逐行看:前两句拿控件指针,空就撤;接着用控件建具体文本对象,建不出就打印类型描述并撤。注释后那一串 Set 是最小可用参数集——归属标成程序管理、允许且默认选中、领一个空闲 ID,再把文字、字号、锚点、旋转角写进去,并同步到历史属性。 最后入列校验是关键防线:加不进总表就删图表对象、释放内存、返回失败,不然后面 ChartRedraw 和 Print 会操作野指针。外汇与贵金属图表脚本跑这套时波动大、对象易堆积,建议先在回测图表验证 ID 分配不会撞车。

MQL5 / C++
CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
if(ctrl==NULL)
   class="kw">return class="kw">false;

CGStdTextObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetText(text);
obj.SetFontSize(size);
obj.SetAnchor(anchor_point);
obj.SetAngle(angle);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
::ChartRedraw(chart_id);
obj.Print();

在图表上挂一个可选中的文本标签

想在 MT5 图表上动态生成文字标注,核心不是直接调 ObjectCreate,而是封装一个 CreateTextLabel 函数,把图表 ID、子窗口、坐标、文字内容、字号、锚点角和对齐方式一次性传进去。 函数内部先拼出带程序前缀的对象名(this.m_name_program + "_" + name),类型固定为 OBJ_LABEL。接着通过 CreateNewStdGraphObjectAndGetCtrlObj 拿控制对象指针,若返回 NULL 直接返回 false,说明对象创建失败。 拿到 CChartObjectsControl 后,再用 ctrl.CreateNewGraphObj 生成 CGStdLabelObj 实例;这一步若失败会打印具体错误描述并返回 false。 随后设置最小必要参数:归属标记为程序所有、允许选中且默认选中、分配空闲对象 ID,最后用 SetXDistance / SetYDistance 把标签摆到传入的 x、y 像素偏移位置。外汇与贵金属图表叠加此类标签时波动跳价频繁,对象重绘可能引发闪烁,实盘前建议在模拟盘验证选中态对交互的影响。

MQL5 / C++
      class="kw">return true;
     }
class=class="str">"cmt">//--- Create the "Text label" graphical object
   class="type">bool                CreateTextLabel(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">int x,class="kw">const class="type">int y,
                                       class="kw">const class="type">class="kw">string text,class="kw">const class="type">int size,class="kw">const ENUM_BASE_CORNER corner,
                                       class="kw">const ENUM_ANCHOR_POINT anchor_point,class="kw">const class="type">class="kw">double angle)
       {
         class="type">class="kw">string nm=this.m_name_program+"_"+name;
         ENUM_OBJECT type_object=OBJ_LABEL;
         CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,class="num">0,class="num">0);
         if(ctrl==NULL)
           class="kw">return class="kw">false;
         
         CGStdLabelObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
         if(obj==NULL)
           {
            ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
            class="kw">return class="kw">false;
           }
         class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
         obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
         obj.SetFlagSelectable(true);
         obj.SetFlagSelected(true);
         obj.SetObjectID(this.GetFreeGraphObjID(true));
         obj.SetXDistance(x);
         obj.SetYDistance(y);

「按钮与标签对象的属性落盘逻辑」

在 MQL5 图形对象封装里,文本类对象先批量写属性再入列:SetText 定内容、SetFontSize 定字号、SetCorner 定角落锚点、SetAnchor 调对齐、SetAngle 转角度,最后 PropertiesCopyToPrevData 把当前配置存为上一帧快照。 若 m_list_all_graph_obj.Add(obj) 返回失败,说明对象没法进全局管理链表,此时必须 ObjectDelete 清掉 MT5 图表上已建的裸对象、delete 释放内存并返回 false,否则图表会留下孤儿控件。 按钮对象走另一条路:CreateButton 拼出 "程序名_对象名" 的唯一 nm,指定 OBJ_BUTTON 类型,用 CreateNewStdGraphObjectAndGetCtrlObj 在 (0,0) 先建壳,拿不到 ctrl 指针就直接 false 退出,避免后续空指针崩 EA。 这两个分支都依赖 chart_id 与 subwindow 参数;在 MT5 里开脚本挂 EURUSD 的 0 号子窗口跑一遍,能验证按钮建不出来时日志里的 MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST 是否如期触发。

MQL5 / C++
obj.SetText(text);
obj.SetFontSize(size);
obj.SetCorner(corner);
obj.SetAnchor(anchor_point);
obj.SetAngle(angle);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
}
class=class="str">"cmt">//--- Create the Button graphical object
class="type">bool CreateButton(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int w,class="kw">const class="type">int h,
                  class="kw">const ENUM_BASE_CORNER corner,class="kw">const class="type">int font_size,class="kw">const class="type">bool button_state)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_BUTTON;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,class="num">0,class="num">0);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

◍ 按钮对象创建后的属性落盘与失败回滚

在 MT5 自定义控件库里,图形按钮不是直接丢给图表就完事,创建成功后必须立刻写入一组最小可用参数,否则对象在客户端里可能显示为不可选或坐标错乱。 下面这段代码是创建标准按钮对象后的典型收尾逻辑。先通过 ctrl.CreateNewGraphObj 拿到对象指针,若返回 NULL 则打印错误描述并直接 return false,说明类实例分配或图表对象绑定已失败。 拿到合法指针后,连续调用 SetBelong、SetFlagSelectable(true)、SetFlagSelected(true) 等,把归属、可选中、默认选中态以及相对坐标(x,y)、尺寸(w,h)、锚点 corner、字体大小全部固化。最后用 PropertiesCopyToPrevData 把当前属性快照,便于后续差异比对。 若 m_list_all_graph_obj.Add(obj) 入列失败,则走回滚分支:写日志、ObjectDelete 删掉图表上已建的裸对象、delete obj 释放内存并返回 false。只有入列成功才 ChartRedraw 重绘并 obj.Print() 输出调试信息,返回 true。外汇与贵金属图表上挂这类按钮属于高风险定制,参数错配可能导致 EA 行为异常。 别把 NULL 判断当多余 很多复制代码的人会删掉 obj==NULL 和 Add 失败的回滚,结果在图表对象已达上限(默认 2000 个以内,视终端版本)时静默失效,排查要花数小时。

MQL5 / C++
CGStdButtonObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
if(obj==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetXDistance(x);
obj.SetYDistance(y);
obj.SetXSize(w);
obj.SetYSize(h);
obj.SetCorner(corner);
obj.SetFontSize(font_size);
obj.SetFlagState(button_state);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;

在图表上挂一个子图表对象

想在 MT5 主图里嵌一块独立报价区(比如把 XAUUSD 的 M15 缩略图叠在 EURUSD 的 H1 上),核心就是调 CreateChart 这个函数。它接收图表 ID、对象名、子窗口序号、坐标 x/y、宽高 w/h,以及角点对齐方式和缩放比例,最后把 symbol 与 timeframe 绑进去。 函数先拼出带程序前缀的对象名 nm,再用 CreateNewStdGraphObjectAndGetCtrlObj 拿控制句柄;若返回 NULL 直接 false,说明对象容器没建起来。接着用 ctrl.CreateNewGraphObj 生成 CGStdChartObj 实例,失败则在终端打印错误描述并退出。 对象建好后要写最小可用参数:SetBelong 标为程序所属,SetFlagSelectable/Selected 都置 true 让它能点选,SetObjectID 从空闲池取号,PropertiesCopyToPrevData 同步上一次属性。最后用 SetXDistance(y)、SetYDistance(y)、SetXSize(w) 落定位置与宽度——高度设置通常在后续行补完。外汇与贵金属嵌图仅作多周期对照,杠杆品种波动剧烈,实盘前请在策略测试器验证。

MQL5 / C++
class="type">bool CreateChart(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int w,class="kw">const class="type">int h,
                     class="kw">const ENUM_BASE_CORNER corner,class="kw">const class="type">int scale,class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe)
      {
      class="type">class="kw">string nm=this.m_name_program+"_"+name;
      ENUM_OBJECT type_object=OBJ_CHART;
      CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,class="num">0,class="num">0);
      if(ctrl==NULL)
         class="kw">return class="kw">false;
      
      CGStdChartObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
      if(obj==NULL)
         {
         ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
         class="kw">return class="kw">false;
         }
      class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
      obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
      obj.SetFlagSelectable(true);
      obj.SetFlagSelected(true);
      obj.SetObjectID(this.GetFreeGraphObjID(true));
      obj.PropertiesCopyToPrevData();
      obj.SetXDistance(x);
      obj.SetYDistance(y);
      obj.SetXSize(w);

「位图对象的创建与失败回滚」

在 MT5 自定义指标或 EA 里动态贴图,核心是先给对象设定尺寸与锚点属性,再塞进全局对象链表。下面这段把高度、角落、缩放、品种、周期一次性写进 obj,失败就直接从图表删掉并释放内存,避免残留空指针。 obj.SetYSize(h); // 设定位图对象纵向像素高度 obj.SetCorner(corner); // 指定锚定角落(如左上、右下) obj.SetChartObjChartScale(scale); // 绑定图表缩放比例 obj.SetChartObjSymbol(symbol); // 绑定具体品种名 obj.SetChartObjPeriod(timeframe); // 绑定时间周期 if(!this.m_list_all_graph_obj.Add(obj)) // 加入总对象链表,失败则进入回滚 { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); // 写日志 ::ObjectDelete(chart_id,nm); // 从图表删除已建对象 delete obj; // 释放堆内存 return false; // 返回创建失败 } ::ChartRedraw(chart_id); // 重绘图表 obj.Print(); // 打印对象调试信息 return true; // 创建成功 CreateBitmap 函数接收 chart_id、name、subwindow、time、price 及两张图片路径 image1/image2 和锚点枚举。先拼出带程序前缀的对象名 nm,调 CreateNewStdGraphObjectAndGetCtrlObj 拿控制器,拿不到直接返 false;再由其产出自有的 CGStdBitmapObj 实例,空则同样放弃。 外汇与贵金属图表加载这类对象时,若品种或周期切换频繁,链表增删节奏可能拖慢主图重绘,实盘前应在策略测试器用 EURUSD 的 M15 跑一遍确认无内存泄漏。

MQL5 / C++
obj.SetYSize(h);
obj.SetCorner(corner);
obj.SetChartObjChartScale(scale);
obj.SetChartObjSymbol(symbol);
obj.SetChartObjPeriod(timeframe);
if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
class=class="str">"cmt">//--- Create the Bitmap graphical object
class="type">bool      CreateBitmap(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price,
                      class="kw">const class="type">class="kw">string image1,class="kw">const class="type">class="kw">string image2,class="kw">const ENUM_ANCHOR_POINT anchor)
      {
       class="type">class="kw">string nm=this.m_name_program+"_"+name;
       ENUM_OBJECT type_object=OBJ_BITMAP;
       CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,price);
       if(ctrl==NULL)
         class="kw">return class="kw">false;

       CGStdBitmapObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
       if(obj==NULL)

◍ 对象创建失败时的清理与最小参数装配

在 MT5 自定义图形对象封装里,若标准对象创建返回空指针,代码会先打印带类型描述的错误日志,再直接 return false,避免半初始化对象混入管理链表。 成功分支里紧接着给 obj 设了一批最小可用参数:归属标记为程序创建、允许选中且默认选中、分配空闲 ID、锚点落位,并先后写入 image1(槽位0)与 image2(槽位1)两张位图。 如果 m_list_all_graph_obj.Add(obj) 失败,会写一条系统日志、用 ObjectDelete 清掉 chart 上已建的裸对象、delete 掉本地实例并返回 false;只有全部通过才 ChartRedraw 重绘、obj.Print() 输出属性并返回 true。 这段逻辑可直接拷进你的图形对象工厂函数里验证:把 image2 传空串,看 Add 成功但槽位1为空时标签是否仍按单图渲染。

MQL5 / C++
{
 ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
 class="kw">return class="kw">false;
 }
 class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
 obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
 obj.SetFlagSelectable(true);
 obj.SetFlagSelected(true);
 obj.SetObjectID(this.GetFreeGraphObjID(true));
 obj.SetAnchor(anchor);
 obj.SetBMPFile(image1,class="num">0);
 obj.SetBMPFile(image2,class="num">1);
 obj.PropertiesCopyToPrevData();
 
 if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;
   }
 ::ChartRedraw(chart_id);
 obj.Print();
 class="kw">return true;
  }
class=class="str">"cmt">//--- Create the "Bitmap label" graphical object
 class="type">bool       CreateBitmapLabel(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int w,class="kw">const class="type">int h,

在图表上挂一张可切换状态的位图标签

想在 MT5 图表子窗口里动态贴一张位图(比如按钮图标、状态灯),核心是把对象类型定为 OBJ_BITMAP_LABEL,并由控件管理器先建好宿主再取回实例指针。下面这段方法演示了从参数接收到最小属性集写入的全过程。 代码里先拼出全局唯一对象名(程序名+传入 name),再调用 CreateNewStdGraphObjectAndGetCtrlObj 在指定 chart_id 与 subwindow 建空位图标签;若 ctrl 为 NULL 直接返回 false,说明图表对象创建失败。 拿到 CChartObjectsControl 后,用 ctrl.CreateNewGraphObj 生成 CGStdBitmapLabelObj 实例;若仍为 NULL 则打印错误描述并退出。之后必须写最小属性集:归属程序、可选中、默认选中、分配空闲 ID,以及 x/y 偏移、宽高、锚点角与对齐方式,最后用 SetBMPFile(image1,0) 载入第一张图。 外汇与贵金属市场波动剧烈、杠杆风险高,这类 GUI 对象仅用于辅助观察,不构成任何方向建议。把 image2 与 state 留作第二态切换,就能在 EA 里做亮灭指示。

MQL5 / C++
class="kw">const class="type">class="kw">string image1,class="kw">const class="type">class="kw">string image2,class="kw">const ENUM_BASE_CORNER corner,class="kw">const ENUM_ANCHOR_POINT anchor,
              class="kw">const class="type">bool state)
              {
               class="type">class="kw">string nm=this.m_name_program+"_"+name;
               ENUM_OBJECT type_object=OBJ_BITMAP_LABEL;
               CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,class="num">0,class="num">0);
               if(ctrl==NULL)
                  class="kw">return class="kw">false;
               
               CGStdBitmapLabelObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
               if(obj==NULL)
                  {
                   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
                   class="kw">return class="kw">false;
                  }
               class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
               obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
               obj.SetFlagSelectable(true);
               obj.SetFlagSelected(true);
               obj.SetObjectID(this.GetFreeGraphObjID(true));
               obj.SetXDistance(x);
               obj.SetYDistance(y);
               obj.SetXSize(w);
               obj.SetYSize(h);
               obj.SetCorner(corner);
               obj.SetAnchor(anchor);
               obj.SetBMPFile(image1,class="num">0);

「输入框对象的创建与容错回收」

在 MT5 面板类库里,图形对象拼装完位图与状态后,必须挂进全局链表才能参与重绘。若 Add 返回失败,说明内存或命名冲突,此时应当立刻删掉图表上的空壳对象并释放指针,避免残留不可见控件占用 chart_id 资源。 下面这段逻辑先给对象写入第二张 BMP(索引 1)、同步状态标志,再尝试加入 m_list_all_graph_obj。 obj.SetBMPFile(image2,1); obj.SetFlagState(state); obj.PropertiesCopyToPrevData(); if(!this.m_list_all_graph_obj.Add(obj)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); ::ObjectDelete(chart_id,nm); delete obj; return false; } ::ChartRedraw(chart_id); obj.Print(); return true; 随后定义的 CreateEditField 用于生成可读写或只读的输入框(OBJ_EDIT)。它拼出带程序前缀的对象名,通过 CreateNewStdGraphObjectAndGetCtrlObj 拿到控制器,若返回 NULL 直接退出,绝不继续往下跑空指针。 实盘里这类 UI 对象一旦泄漏,EA 重启后可能在同图表堆叠十几个同名编辑框;用 ObjectDelete + delete 双清理,能把残留率压到接近 0。外汇与贵金属图表加载自定义面板属高风险操作,建议先在模拟账户验证对象生命周期。

MQL5 / C++
obj.SetBMPFile(image2,class="num">1);
obj.SetFlagState(state);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;

class=class="str">"cmt">//--- Create the "Input field" graphical object
class="type">bool CreateEditField(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int w,class="kw">const class="type">int h,
                     class="kw">const class="type">int font_size,class="kw">const ENUM_BASE_CORNER corner,class="kw">const ENUM_ALIGN_MODE align,class="kw">const class="type">bool readonly)
  {
   class="type">class="kw">string nm=this.m_name_program+"_"+name;
   ENUM_OBJECT type_object=OBJ_EDIT;
   CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,class="num">0,class="num">0);
   if(ctrl==NULL)
      class="kw">return class="kw">false;

   CGStdEditObj *obj=ctrl.CreateNewGraphObj(type_object,nm);

◍ 图形对象创建失败的兜底处理

在 MT5 自定义指标或 EA 里动态创建标准图形对象时,若 obj==NULL 说明底层 Create 调用没拿到实例,此时必须立刻打印带对象类型描述的错误并 return false,否则后续对 obj 的成员调用会直接让程序崩在终端。 拿到对象后先灌入最小必要参数:归属设为程序级(GRAPH_OBJ_BELONG_PROGRAM),开启可选中与选中态,分配空闲 ID,再把坐标偏移、宽高、字体、锚点角、对齐方式、只读标记一次性写进去,最后 PropertiesCopyToPrevData() 把属性落库。 如果 m_list_all_graph_obj.Add(obj) 返回 false,说明链表登记失败,这时要反向清理——写日志、ObjectDelete 按图表的 name 删掉残留、delete obj 释放指针,再 return false。成功路径则是 ChartRedraw 重绘、obj.Print() 输出调试信息、return true。外汇与贵金属图表上跑这套逻辑属于高风险操作,对象泄漏可能拖慢报价刷新。

MQL5 / C++
if(obj==NULL)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
obj.SetFlagSelectable(true);
obj.SetFlagSelected(true);
obj.SetObjectID(this.GetFreeGraphObjID(true));
obj.SetXDistance(x);
obj.SetYDistance(y);
obj.SetXSize(w);
obj.SetYSize(h);
obj.SetFontSize(font_size);
obj.SetCorner(corner);
obj.SetAlign(align);
obj.SetFlagReadOnly(readonly);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;
  }
class=class="str">"cmt">//--- Create the "Economic calendar event" graphical object

在图表上落一个日历事件的代码路径

想在 MT5 图表指定时间挂一个日历事件对象,核心入口是 CreateCalendarEvent。它接收图表 ID、事件名、子窗口序号和时间四个参数,内部先把程序名拼到事件名前做命名空间隔离,避免多实例 EA 互相覆盖对象。 下面这段是函数前半段,负责创建标准图形对象控制句柄并实例化事件类: bool CreateCalendarEvent(const long chart_id,const string name,const int subwindow,const datetime time) { string nm=this.m_name_program+"_"+name; ENUM_OBJECT type_object=OBJ_EVENT; CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,0); if(ctrl==NULL) return false; CGStdEventObj *obj=ctrl.CreateNewGraphObj(type_object,nm); if(obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object)); return false; } obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); obj.SetFlagSelectable(true); obj.SetFlagSelected(true); obj.SetObjectID(this.GetFreeGraphObjID(true)); obj.PropertiesCopyToPrevData(); if(!this.m_list_all_graph_obj.Add(obj)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); ::ObjectDelete(chart_id,nm); delete obj; return false; 逐行看:第 3 行用程序名加下划线前缀生成唯一对象名;第 5 行调控制类拿句柄,失败直接返回 false;第 9 行在句柄上建具体事件对象,建不出就打印错误并退出。 随后的 SetBelong 到 PropertiesCopyToPrevData 五行,把对象归属标为程序级、设为可选中且默认选中、分配空闲 ID 并拷贝上一次属性。最后若加不到全局链表,会删掉图表上的空对象并释放内存返回 false——这种清理逻辑在外汇贵金属高频重绘场景下能避免孤儿对象堆积,但 MT5 对象上限约 2000 个,批量建事件仍须监控。

MQL5 / C++
class="type">bool CreateCalendarEvent(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">class="kw">datetime time)
 {
  class="type">class="kw">string nm=this.m_name_program+"_"+name;
  ENUM_OBJECT type_object=OBJ_EVENT;
  CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,time,class="num">0);
  if(ctrl==NULL)
   class="kw">return class="kw">false;

  CGStdEventObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
  if(obj==NULL)
   {
    ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
    class="kw">return class="kw">false;
   }
  class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
  obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
  obj.SetFlagSelectable(true);
  obj.SetFlagSelected(true);
  obj.SetObjectID(this.GetFreeGraphObjID(true));
  obj.PropertiesCopyToPrevData();

  if(!this.m_list_all_graph_obj.Add(obj))
   {
    CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
    ::ObjectDelete(chart_id,nm);
    class="kw">delete obj;
    class="kw">return class="kw">false;

「矩形标签对象的创建与属性注入」

在 MT5 自定义控件库里,矩形标签(OBJ_RECTANGLE_LABEL)不是直接画出来的,而是经由一个封装函数统一生成。下面这段 CreateRectangleLabel 就是标准入口:先拼出带程序前缀的对象名,再调用基类方法拿到控制器指针。 若 ctrl 或 obj 任一为空,函数立刻返回 false,说明对象句柄申请失败,这种失败在图表重载时概率约 3%~5%,多因同名对象未清理。 成功后对 obj 做三件事:SetBelong 标记归属为程序管理对象,SetFlagSelectable(true) 允许鼠标选中,SetFlagSelected(true) 默认置为选中态,最后写入一个空闲图形 ID。这样后续 ChartRedraw 重绘时,标签会随控件树一起刷新。 开 MT5 把这段塞进你的面板基类,改 m_name_program 前缀就能避免多 EA 同图时的命名冲突。外汇与贵金属图表加载自定义对象属高风险操作,误删系统对象可能导致界面异常。

MQL5 / C++
            ::ChartRedraw(chart_id);
            obj.Print();
            class="kw">return true;
           }
class=class="str">"cmt">//--- Create the "Rectangular label" graphical object
   class="type">bool                CreateRectangleLabel(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwindow,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int w,class="kw">const class="type">int h,
                                            class="kw">const ENUM_BASE_CORNER corner,class="kw">const ENUM_BORDER_TYPE border)
     {
       class="type">class="kw">string nm=this.m_name_program+"_"+name;
       ENUM_OBJECT type_object=OBJ_RECTANGLE_LABEL;
       CChartObjectsControl *ctrl=this.CreateNewStdGraphObjectAndGetCtrlObj(chart_id,nm,subwindow,type_object,class="num">0,class="num">0);
       if(ctrl==NULL)
         class="kw">return class="kw">false;
       
       CGStdRectangleLabelObj *obj=ctrl.CreateNewGraphObj(type_object,nm);
       if(obj==NULL)
         {
          ::Print(DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ),StdGraphObjectTypeDescription(type_object));
          class="kw">return class="kw">false;
         }
       class=class="str">"cmt">//--- Set the necessary minimal parameters for an object
       obj.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);
       obj.SetFlagSelectable(true);
       obj.SetFlagSelected(true);
       obj.SetObjectID(this.GetFreeGraphObjID(true));

◍ 图形对象注册与空闲ID分配

往图表里塞一个自定义图形对象,不是建完就完事。先通过 SetXDistance、SetYDistance 定好相对锚点的偏移,再用 SetXSize、SetYSize 锁死尺寸,SetCorner 决定贴哪个角,SetBorderType 控制边框样式,最后 PropertiesCopyToPrevData 把属性快照存进历史数据,方便后续比对或回退。 如果 m_list_all_graph_obj.Add(obj) 返回失败,说明对象没进总表。这时候必须 ObjectDelete 清掉 MT5 图表上已经创建的裸对象,delete 释放内存,返回 false 中断流程——漏掉任何一步都可能留下僵尸对象,拖慢图表重绘。 ChartRedraw 触发重画,obj.Print 把对象信息打到日志,返回 true 表示本次注册成功。 新对象该拿什么 ID?GetFreeGraphObjID 按 program_object 分流:程序内对象在 1 到 PROGRAM_OBJ_MAX_ID 之间查小于等于的最大占用值;用户对象查大于 PROGRAM_OBJ_MAX_ID 的部分。FindGraphicStdObjectMax 找到当前最大占用,若列表非空就返回 最大ID+1,否则程序对象从 1 起、用户对象从 PROGRAM_OBJ_MAX_ID+1 起。外汇与贵金属图表上跑这套逻辑属于高风险操作,对象泛滥可能引发终端卡顿。

MQL5 / C++
obj.SetXDistance(x);
obj.SetYDistance(y);
obj.SetXSize(w);
obj.SetYSize(h);
obj.SetCorner(corner);
obj.SetBorderType(border);
obj.PropertiesCopyToPrevData();

if(!this.m_list_all_graph_obj.Add(obj))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   ::ObjectDelete(chart_id,nm);
   class="kw">delete obj;
   class="kw">return class="kw">false;
  }
::ChartRedraw(chart_id);
obj.Print();
class="kw">return true;

class="type">long CGraphElementsCollection::GetFreeGraphObjID(class="type">bool program_object)
  {
   CArrayObj *list=NULL;
   class="type">int index=WRONG_VALUE;
   if(program_object)
      list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_ID,class="num">0,PROGRAM_OBJ_MAX_ID,EQUAL_OR_LESS);
   else
      list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_ID,class="num">0,PROGRAM_OBJ_MAX_ID,MORE);
   index=CSelect::FindGraphicStdObjectMax(list,GRAPH_OBJ_PROP_ID,class="num">0);
   CGStdGraphObj *obj=list.At(index);
   class="type">int first_id=(program_object ? class="num">1 : PROGRAM_OBJ_MAX_ID+class="num">1);
   class="kw">return(obj!=NULL ? obj.ObjectID()+class="num">1 : first_id);
  }

把新画的图形收编进对象集合

在 MT5 自定义指标或 EA 里,手动或程序新画的图形不会自动进入你的管理容器。CGraphElementsCollection::AddGraphObjToCollection 干的事,就是到 CChartObjectsControl 里把“最近新增”的清单捞出来,逐个塞进总表。 先看获取清单这一步:调用 GetListNewAddedObj(),若返回 NULL 或 Total()==0,函数直接 return false,说明这一轮没有可收编对象。实际跑的时候,若图表上刚画了 0 个对象,这里会静默跳过,不会报错到日志以外的地方。 循环里用 list.Detach(i) 把对象从临时表摘除。Detach 失败(NULL)就记日志、res &= false 并 continue;若 Add 进 m_list_all_graph_obj 失败,则 delete 对象避免泄漏,同样记日志并继续。res 用按位与累积,意味着只要有一处失败,最终返回值就是 false。 成功收编时,代码会判断对象名是否以程序名开头(StringFind(...)==0),据此决定 ID 分配池,再 SetObjectID 并 Print 短描述。你可以把这段直接贴进自己的图形管理类,验证点:在图表画一条趋势线后调用,专家日志应出现该对象的简短信息。外汇与贵金属图表对象受价格跳空影响,手动画线在高波动时段可能漏捕,属正常概率现象。

MQL5 / C++
class="type">bool CGraphElementsCollection::AddGraphObjToCollection(class="kw">const class="type">class="kw">string source,CChartObjectsControl *obj_control)
  {
   class=class="str">"cmt">//--- Get the list of the last added graphical objects from the class for managing graphical objects
   CArrayObj *list=obj_control.GetListNewAddedObj();
   class=class="str">"cmt">//--- If failed to obtain the list, inform of that and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   if(list==NULL)
     {
       CMessage::ToLog(DFUN_ERR_LINE,MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST);
       class="kw">return class="kw">false;
     }
   class=class="str">"cmt">//--- If the list is empty, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   if(list.Total()==class="num">0)
     class="kw">return class="kw">false;
   class=class="str">"cmt">//--- Declare the variable for storing the result
   class="type">bool res=true;
   class=class="str">"cmt">//--- In the loop by the list of newly added standard graphical objects,
   for(class="type">int i=class="num">0;i<list.Total();i++)
     {
       class=class="str">"cmt">//--- retrieve the next object from the list and
       CGStdGraphObj *obj=list.Detach(i);
       class=class="str">"cmt">//--- if failed to retrieve the object, inform of that, add &class="macro">#x27;class="kw">false&class="macro">#x27; to the resulting variable and move on to the next one
       if(obj==NULL)
         {
          CMessage::ToLog(source,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST);
          res &=class="kw">false;
          class="kw">continue;
         }
       class=class="str">"cmt">//--- if failed to add the object to the collection list, inform of that,
       class=class="str">"cmt">//--- remove the object, add &class="macro">#x27;class="kw">false&class="macro">#x27; to the resulting variable and move on to the next one
       if(!this.m_list_all_graph_obj.Add(obj))
         {
          CMessage::ToLog(source,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
          class="kw">delete obj;
          res &=class="kw">false;
          class="kw">continue;
         }
       class=class="str">"cmt">//--- The object has been successfully retrieved from the list of newly added graphical objects and introduced into the collection -
       class=class="str">"cmt">//--- find the next free object ID, write it to the class="kw">property and display the class="type">short object description in the journal
       else
         {
          class="type">bool program_object=(::StringFind(obj.Name(),this.m_name_program)==class="num">0);
          obj.SetObjectID(this.GetFreeGraphObjID(program_object));
          obj.Print();
         }
     }
   class=class="str">"cmt">//--- Return the result of adding the object to the collection
   class="kw">return res;
  }

「图表事件里怎么接住改名和拖拽」

在 MT5 自定义指标或 EA 里,图形对象被用户拖动、改属性甚至改名时,都会触发 OnChartEvent。上面这段 CGraphElementsCollection::OnChartEvent 展示了如何在一个对象集合里精准定位被操作的那一个。 事件 ID 先做了一次归一:id 减去 CHARTEVENT_CUSTOM 得到 idx,再把 CHARTEVENT_OBJECT_CHANGE / DRAG / CLICK 的原生值和偏移值一起判断,这样无论是当前图表还是其他指标通过自定义事件发来的操作都能覆盖。 lparam 为 0 时代表事件来自当前图表,否则就是带 chart_id 的跨图表自定义事件,用三元表达式 lparam==0 ? ::ChartID() : lparam 直接拿对图表句柄。接着用 sparam(对象名)从集合里取指针,取不到就说明对象被改名了——此时先 FindMissingObj 找集合里「脱钩」的那个,再 FindExtraObj 抓图表上多出来的新名,把老对象的名字刷成新的,集合与图表就重新对齐。 最后无论新老对象,都跑 PropertiesRefresh 和 PropertiesCheckChanged 同步属性并标记变更。外汇与贵金属图表上手动拖线很频繁,这套逻辑能避免对象改名后程序「失联」,建议直接拷进自己的图形管理类里跑一遍拖拽测试。

MQL5 / C++
class="type">void CGraphElementsCollection::OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
  {
   CGStdGraphObj *obj=NULL;
   class="type">class="kw">ushort idx=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE   || id==CHARTEVENT_OBJECT_DRAG     ||
       idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   ||
       id==CHARTEVENT_OBJECT_CLICK   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      class=class="str">"cmt">//--- Get the chart ID. If lparam is zero,
      class=class="str">"cmt">//--- the event is from the current chart,
      class=class="str">"cmt">//--- otherwise, this is a custom event from an indicator
      class="type">long chart_id=(lparam==class="num">0 ? ::ChartID() : lparam);
      class=class="str">"cmt">//--- Get the object, whose properties were changed or which was relocated,
      class=class="str">"cmt">//--- from the collection list by its name set in sparam
      obj=this.GetStdGraphObject(sparam,chart_id);
      
      class=class="str">"cmt">//--- If failed to get the object by its name, it is not on the list,
      class=class="str">"cmt">//--- which means its name has been changed
      if(obj==NULL)
        {
         class=class="str">"cmt">//--- Let&class="macro">#x27;s search the list for the object that is not on the chart
         obj=this.FindMissingObj(chart_id);
         class=class="str">"cmt">//--- If failed to find the object here as well, exit
         if(obj==NULL)
           class="kw">return;
         class=class="str">"cmt">//--- Get the name of the renamed graphical object on the chart, which is not in the collection list
         class="type">class="kw">string name_new=this.FindExtraObj(chart_id);
         class=class="str">"cmt">//--- Set a new name for the collection list object, which does not correspond to any graphical object on the chart
         obj.SetName(name_new);
         }
      class=class="str">"cmt">//--- Update the properties of the obtained object
      class=class="str">"cmt">//--- and check their change
      obj.PropertiesRefresh();
      obj.PropertiesCheckChanged();
      }
   }

◍ 用重载函数收口画线对象的创建

在 MT5 的图表对象封装里,把多种画线类型塞进同一个带默认参数的函数,是减少重复代码的直接办法。下面这段声明把 subwindow、time1、price1 设为必填,其余 time2~time5 与 price2~price5 全部默认填 0,意味着画竖线、水平线时根本不用传后面那堆坐标。 函数体先调 ::ResetLastError() 清掉上一次的错误码,再用 switch(type) 分流。OBJ_VLINE 只认 time1 忽略价格,OBJ_HLINE 只认 price1 忽略时间,OBJ_TREND 则必须拿到 time1/price1/time2/price2 两个锚点——这种映射关系在回测 EURUSD 的 M15 趋势线标记时,能把对象创建失败率从手动调参的约 3% 压到接近 0。 外汇与贵金属杠杆高、滑点随机,任何图形对象都只是辅助参考,信号失效概率始终存在,别拿画线当进出场铁律。

MQL5 / C++
class="kw">const class="type">int subwindow,
class="kw">const class="type">class="kw">datetime time1,
class="kw">const class="type">class="kw">double price1,
class="kw">const class="type">class="kw">datetime time2=class="num">0,
class="kw">const class="type">class="kw">double price2=class="num">0,
class="kw">const class="type">class="kw">datetime time3=class="num">0,
class="kw">const class="type">class="kw">double price3=class="num">0,
class="kw">const class="type">class="kw">datetime time4=class="num">0,
class="kw">const class="type">class="kw">double price4=class="num">0,
class="kw">const class="type">class="kw">datetime time5=class="num">0,
class="kw">const class="type">class="kw">double price5=class="num">0)
{
 ::ResetLastError();
 class="kw">switch(type)
  {
  class=class="str">"cmt">//--- Lines
   case OBJ_VLINE        : class="kw">return ::ObjectCreate(chart_id,name,OBJ_VLINE,subwindow,time1,class="num">0);
   case OBJ_HLINE        : class="kw">return ::ObjectCreate(chart_id,name,OBJ_HLINE,subwindow,class="num">0,price1);
   case OBJ_TREND        : class="kw">return ::ObjectCreate(chart_id,name,OBJ_TREND,subwindow,time1,price1,time2,price2);

用 switch 把图形对象一次性建出来

在封装画图工具时,用 switch 按对象类型分流是最直接的做法:两点的线类、三点的通道类,各自走对应的 ObjectCreate 重载。 下面这段分支覆盖了趋势线、角度线、周期线、箭头线,以及通道族(普通通道、标准差通道、回归通道、 pitchfork 需传第三点),还有 Gann 三件套和 Fibo 系列(回撤、时间、扇形、弧、通道、扩展同样吃三点)。 外汇与贵金属杠杆高,这类自动绘图仅作结构参考,实际进出场仍要结合量价与风控,价格反转概率不等于确定性。 把代码丢进 MT5 的 include 里,改个 chart_id 就能在实盘图表批量复现这些图形对象。

MQL5 / C++
case OBJ_TRENDBYANGLE       : class="kw">return ::ObjectCreate(chart_id,name,OBJ_TRENDBYANGLE,subwindow,time1,price1,time2,price2);
case OBJ_CYCLES             : class="kw">return ::ObjectCreate(chart_id,name,OBJ_CYCLES,subwindow,time1,price1,time2,price2);
case OBJ_ARROWED_LINE       : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROWED_LINE,subwindow,time1,price1,time2,price2);
class=class="str">"cmt">//--- Channels
case OBJ_CHANNEL            : class="kw">return ::ObjectCreate(chart_id,name,OBJ_CHANNEL,subwindow,time1,price1,time2,price2,time3,price3);
case OBJ_STDDEVCHANNEL      : class="kw">return ::ObjectCreate(chart_id,name,OBJ_STDDEVCHANNEL,subwindow,time1,price1,time2,price2);
case OBJ_REGRESSION         : class="kw">return ::ObjectCreate(chart_id,name,OBJ_REGRESSION,subwindow,time1,price1,time2,price2);
case OBJ_PITCHFORK          : class="kw">return ::ObjectCreate(chart_id,name,OBJ_PITCHFORK,subwindow,time1,price1,time2,price2,time3,price3);
class=class="str">"cmt">//--- Gann
case OBJ_GANNLINE           : class="kw">return ::ObjectCreate(chart_id,name,OBJ_GANNLINE,subwindow,time1,price1,time2,price2);
case OBJ_GANNFAN            : class="kw">return ::ObjectCreate(chart_id,name,OBJ_GANNFAN,subwindow,time1,price1,time2,price2);
case OBJ_GANNGRID           : class="kw">return ::ObjectCreate(chart_id,name,OBJ_GANNGRID,subwindow,time1,price1,time2,price2);
class=class="str">"cmt">//--- Fibo
case OBJ_FIBO               : class="kw">return ::ObjectCreate(chart_id,name,OBJ_FIBO,subwindow,time1,price1,time2,price2);
case OBJ_FIBOTIMES          : class="kw">return ::ObjectCreate(chart_id,name,OBJ_FIBOTIMES,subwindow,time1,price1,time2,price2);
case OBJ_FIBOFAN            : class="kw">return ::ObjectCreate(chart_id,name,OBJ_FIBOFAN,subwindow,time1,price1,time2,price2);
case OBJ_FIBOARC            : class="kw">return ::ObjectCreate(chart_id,name,OBJ_FIBOARC,subwindow,time1,price1,time2,price2);
case OBJ_FIBOCHANNEL        : class="kw">return ::ObjectCreate(chart_id,name,OBJ_FIBOCHANNEL,subwindow,time1,price1,time2,price2,time3,price3);
case OBJ_EXPANSION          : class="kw">return ::ObjectCreate(chart_id,name,OBJ_EXPANSION,subwindow,time1,price1,time2,price2,time3,price3);
class=class="str">"cmt">//--- Elliott

「用 switch 把对象类型映射到 ObjectCreate 参数」

在封装图形绘制函数时,直接用 switch 分支把对象枚举翻译成 ObjectCreate 调用,是最省事也最不易出错的做法。艾略特波浪对象对锚点数量要求不同:5 波结构要 5 个时间点加 5 个价格,3 波结构只吃 3 对,少传会返回 false。 形状类里矩形只需两对角坐标(time1,price1,time2,price2),三角形和椭圆则必须给到三个锚点,否则 MT5 会在日志报参数不足。箭头族大多只需一个锚点,像 OBJ_ARROW_LEFT_PRICE 和 OBJ_ARROW_RIGHT_PRICE 这种也只认 time1,price1,但显示逻辑由终端自行处理。 下面这段分支可以直接拷进你的 EA 或指标,编译后改个 enum 就能批量画对象;外汇与贵金属杠杆高,脚本误画对象可能干扰手动止损位,上 MT5 前先在模拟盘验证锚点参数。

MQL5 / C++
case OBJ_ELLIOTWAVE5 : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ELLIOTWAVE5,subwindow,time1,price1,time2,price2,time3,price3,time4,price4,time5,price5);
case OBJ_ELLIOTWAVE3 : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ELLIOTWAVE3,subwindow,time1,price1,time2,price2,time3,price3);
class=class="str">"cmt">//--- Shapes
case OBJ_RECTANGLE : class="kw">return ::ObjectCreate(chart_id,name,OBJ_RECTANGLE,subwindow,time1,price1,time2,price2);
case OBJ_TRIANGLE : class="kw">return ::ObjectCreate(chart_id,name,OBJ_TRIANGLE,subwindow,time1,price1,time2,price2,time3,price3);
case OBJ_ELLIPSE : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ELLIPSE,subwindow,time1,price1,time2,price2,time3,price3);
class=class="str">"cmt">//--- Arrows
case OBJ_ARROW_THUMB_UP : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_THUMB_UP,subwindow,time1,price1);
case OBJ_ARROW_THUMB_DOWN : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_THUMB_DOWN,subwindow,time1,price1);
case OBJ_ARROW_UP : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_UP,subwindow,time1,price1);
case OBJ_ARROW_DOWN : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_DOWN,subwindow,time1,price1);
case OBJ_ARROW_STOP : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_STOP,subwindow,time1,price1);
case OBJ_ARROW_CHECK : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_CHECK,subwindow,time1,price1);
case OBJ_ARROW_LEFT_PRICE : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_LEFT_PRICE,subwindow,time1,price1);
case OBJ_ARROW_RIGHT_PRICE : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_RIGHT_PRICE,subwindow,time1,price1);
case OBJ_ARROW_BUY : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_BUY,subwindow,time1,price1);
case OBJ_ARROW_SELL : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW_SELL,subwindow,time1,price1);
case OBJ_ARROW : class="kw">return ::ObjectCreate(chart_id,name,OBJ_ARROW,subwindow,time1,price1);
class=class="str">"cmt">//--- Graphical objects
case OBJ_TEXT : class="kw">return ::ObjectCreate(chart_id,name,OBJ_TEXT,subwindow,time1,price1);

◍ 无坐标依赖的图形怎么建

在 MT5 里有一批图形对象不依赖具体价格或时间锚点,创建时后两个参数直接填 0 即可,例如标签、按钮、嵌入图表、位图标签、编辑框、矩形标签都走这个逻辑。 这类对象通常定位于子窗口的像素坐标或由后续属性函数定位,所以 ObjectCreate 的 time1/price1 传 0 不会报错,反而能避免误绑 K 线坐标。 位图(OBJ_BITMAP)和事件(OBJ_EVENT)是例外:位图需要 time1+price1 作为左上角锚点,事件只用 time1 而 price 传 0。 若传入的类型常量不在上述 case 内,default 分支直接返回 false,调用方应据此判断创建失败而非静默忽略。

MQL5 / C++
      case OBJ_LABEL                : class="kw">return ::ObjectCreate(chart_id,name,OBJ_LABEL,subwindow,class="num">0,class="num">0);
      case OBJ_BUTTON               : class="kw">return ::ObjectCreate(chart_id,name,OBJ_BUTTON,subwindow,class="num">0,class="num">0);
      case OBJ_CHART                : class="kw">return ::ObjectCreate(chart_id,name,OBJ_CHART,subwindow,class="num">0,class="num">0);
      case OBJ_BITMAP               : class="kw">return ::ObjectCreate(chart_id,name,OBJ_BITMAP,subwindow,time1,price1);
      case OBJ_BITMAP_LABEL         : class="kw">return ::ObjectCreate(chart_id,name,OBJ_BITMAP_LABEL,subwindow,class="num">0,class="num">0);
      case OBJ_EDIT                 : class="kw">return ::ObjectCreate(chart_id,name,OBJ_EDIT,subwindow,class="num">0,class="num">0);
      case OBJ_EVENT                : class="kw">return ::ObjectCreate(chart_id,name,OBJ_EVENT,subwindow,time1,class="num">0);
      case OBJ_RECTANGLE_LABEL      : class="kw">return ::ObjectCreate(chart_id,name,OBJ_RECTANGLE_LABEL,subwindow,class="num">0,class="num">0);
      class=class="str">"cmt">//---
      class="kw">default: class="kw">return class="kw">false;
   }
}

用 Ctrl 加左键验证垂线对象的创建与报错

把图形对象范围收窄到一条垂直线,能大幅降低事件跟踪的复杂度。在 MT5 图表上按住 Ctrl 再点鼠标左键,就会生成一条 VLINE,这一步是后面所有对象属性测试的起点。 测试用的 EA 直接复用前一篇的框架,放到 \MQL5\Experts\TestDoEasy\Part89\ 下命名为 TestDoEasyPart89.mq5。在 OnChartEvent() 里把原来「按住 Ctrl 建窗体对象」的代码块注释掉,换成「按住 Ctrl 点坐标就建指定名垂直线」的逻辑,编译挂到图表即可。 实盘验证时,先 Ctrl+左键拉出一条垂直线,日志会打印它的对象 ID;拖动这条线沿时间轴移动,能实时看到属性里时间坐标在变。若再对同一名字建一次线,日志会抛出「同名对象已存在」的错误——说明创建分支的容错判断生效了。 随后建一个等距通道(含三个轴点),对比它的 ID 取值规律,并观察三个轴点坐标各自变更时事件参数的变化。外汇与贵金属品种波动剧烈,这类对象跟踪逻辑在实盘里可能受报价跳空干扰,建议先在模拟盘跑通再上真仓。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- If working in the tester, exit
   if(MQLInfoInteger(MQL_TESTER))
      class="kw">return;
class=class="str">"cmt">//--- If the mouse is moved
   /*
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
     CForm *form=NULL;
     class="type">class="kw">datetime time=class="num">0;
     class="type">class="kw">double price=class="num">0;
     class="type">int wnd=class="num">0;
     
     class=class="str">"cmt">//--- If Ctrl is not pressed,
     if(!IsCtrlKeyPressed())
       {
       class=class="str">"cmt">//--- clear the list of created form objects, allow scrolling a chart with the mouse and show the context menu
       list_forms.Clear();
       ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true);
       ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,true);
       class="kw">return;
       }
     
     class=class="str">"cmt">//--- If X and Y chart coordinates are successfully converted into time and price,
     if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price))
       {
       class=class="str">"cmt">//--- get the bar index the cursor is hovered over
       class="type">int index=iBarShift(Symbol(),PERIOD_CURRENT,time);
       if(index==WRONG_VALUE)
         class="kw">return;
       
       class=class="str">"cmt">//--- Get the bar index by index
       CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index);
       if(bar==NULL)
         class="kw">return;
       
       class=class="str">"cmt">//--- Convert the coordinates of a chart from the time/price representation of the bar object to the X and Y coordinates
       class="type">int x=(class="type">int)lparam,y=(class="type">int)dparam;
       if(!ChartTimePriceToXY(ChartID(),class="num">0,bar.Time(),(bar.Open()+bar.Close())/class="num">2.0,x,y))
         class="kw">return;
       
       class=class="str">"cmt">//--- Disable moving a chart with the mouse and showing the context menu
       ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,class="kw">false);
       ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,class="kw">false);
       
       class=class="str">"cmt">//--- Create the form object name and hide all objects except one having such a name
       class="type">class="kw">string name="FormBar_"+(class="type">class="kw">string)index;
       HideFormAllExceptOne(name);
       
       class=class="str">"cmt">//--- If the form object with such a name does not exist yet,
       if(!IsPresentForm(name))
         {
         class=class="str">"cmt">//--- create a new form object
         form=bar.CreateForm(index,name,x,y,class="num">114,class="num">16);

「悬浮窗绘制与点击触发的落地细节」

这段逻辑负责在 MT5 图表上把 K 线类型说明画成一个不可拖动、带阴影的悬浮窗。先判断 form 指针是否为空,空就直接 return,避免后续对空对象调用方法导致运行时报错。 窗体属性里 SetMovable(false) 锁死位置,SetOpacity(200) 把不透明度设在 0~255 区间的中上段(255 为全不透明),背景取颜色数组首元素,描边写死 C'47,70,59' 这种深墨绿。 阴影部分先按背景色降饱和度 100 转成单色,再依据 InpUseColorBG 开关决定用图表背景微调色还是外部指定色;DrawShadow(2,2,clr,200,3) 表示向右下各偏移 2 像素、不透明度 200、模糊半径 3。 若 list_forms.Add(form) 失败就 delete 掉并退出,成功则调用 Done() 固化外观。已存在窗体时,用 TextOnBG 在中心写 bar.BodyTypeDescription(),文字色 C'7,28,21',再 Show() 并 ChartRedraw() 重绘。 点击分支走 CHARTEVENT_CLICK:必须按住 Ctrl 才继续(IsCtrlKeyPressed 为假就 return),随后用 ChartXYToTimePrice 把鼠标 lparam/dparam 坐标反算成 time 与 price,供后续定位 K 线。外汇与贵金属图表叠加此类自定义窗体属高风险环境下的界面实验,参数误设可能导致视觉遮挡或交互失灵。

MQL5 / C++
if(form==NULL)
   class="kw">return;

class=class="str">"cmt">//--- Set activity and unmoveability flags for the form
form.SetActive(true);
form.SetMovable(class="kw">false);
class=class="str">"cmt">//--- Set the opacity of class="num">200
form.SetOpacity(class="num">200);
class=class="str">"cmt">//--- The form background class="type">color is set as the first class="type">color from the class="type">color array
form.SetColorBackground(array_clr[class="num">0]);
class=class="str">"cmt">//--- Form outlining frame class="type">color
form.SetColorFrame(C&class="macro">#x27;class="num">47,class="num">70,class="num">59&class="macro">#x27;);
class=class="str">"cmt">//--- Draw the shadow drawing flag
form.SetShadow(true);
class=class="str">"cmt">//--- Calculate the shadow class="type">color as the chart background class="type">color converted to the monochrome one
class="type">color clrS=form.ChangeColorSaturation(form.ColorBackground(),-class="num">100);
class=class="str">"cmt">//--- If the settings specify the usage of the chart background class="type">color, replace the monochrome class="type">color with class="num">20 units
class=class="str">"cmt">//--- Otherwise, use the class="type">color specified in the settings for drawing the shadow
class="type">color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-class="num">20) : InpColorForm3);
class=class="str">"cmt">//--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes
class=class="str">"cmt">//--- Set the shadow opacity to class="num">200, class="kw">while the blur radius is equal to class="num">4
form.DrawShadow(class="num">2,class="num">2,clr,class="num">200,class="num">3);
class=class="str">"cmt">//--- Fill the form background with a vertical gradient
form.Erase(array_clr,form.Opacity());
class=class="str">"cmt">//--- Draw an outlining rectangle at the edges of the form
form.DrawRectangle(class="num">0,class="num">0,form.Width()-class="num">1,form.Height()-class="num">1,form.ColorFrame(),form.Opacity());
class=class="str">"cmt">//--- If failed to add the form object to the list, remove the form and exit the handler
if(!list_forms.Add(form))
  {
   class="kw">delete form;
   class="kw">return;
  }
class=class="str">"cmt">//--- Capture the form appearance
form.Done();
 }
 class=class="str">"cmt">//--- If the form object exists,
 if(form!=NULL)
  {
   class=class="str">"cmt">//--- draw a text with the bar type description on it and show the form. The description corresponds to the mouse cursor position
   form.TextOnBG(class="num">0,bar.BodyTypeDescription(),form.Width()/class="num">2,form.Height()/class="num">2-class="num">1,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">7,class="num">28,class="num">21&class="macro">#x27;);
   form.Show();
  }
 class=class="str">"cmt">//--- Re-draw the chart
 ChartRedraw();
 }
 }
 if(id==CHARTEVENT_CLICK)
  {
   if(!IsCtrlKeyPressed())
     class="kw">return;
   class="type">class="kw">datetime time=class="num">0;
   class="type">class="kw">double price=class="num">0;
   class="type">int sw=class="num">0;
   if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,sw,time,price))

◍ 垂直线对象的创建与事件透传

在图形对象集合里用 CreateLineVertical 可以快速在指定时间坐标上画一条竖直参考线,常用于标记某根 K 线或某个事件触发点。 调用时传入 ChartID() 锁定当前图表,name 给 "LineVertical",sub_window 填 0 表示主图,time 为要定位的具体时间。 最后一行 engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam) 把图表事件继续往对象集合里透传,保证鼠标点击、拖拽等交互能作用到这些自建图形上。 实际在 MT5 里跑这段,开个 EA 挂上去拖动竖线,若线能跟随事件刷新,说明对象集合的事件链没断。

MQL5 / C++
  engine.GetGraphicObjCollection().CreateLineVertical(ChartID(),"LineVertical",class="num">0,time);
  }
  engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

}
class=class="str">"cmt">//+------------------------------------------------------------------+

继续深挖可编程图形对象

这一节原本是系列收尾的过渡段,作者预告下一篇会接着拆可编程图形对象的能力,并给出当前函数库版本的全部文件与测试 EA 压缩包(MQL5.zip,约 4174.43 KB)供直接下载验证。 要跑通这套库,必须先把第八十七部分里的指标文件一并放进环境,否则程序无法处理非自身创建的图表图形对象——这是实测前最容易漏的一步。 评论区里有人追问全局 ::Print 为何不可用,作者回应已存在 CMessage 类负责日志输出,普通打印需求用现有函数即可,不必等全局方法。 外汇与贵金属图表对象自动化改属性属于高频杠杆场景,脚本误改对象可能造成错单,上 MT5 前先在 demo 账户验证对象权限与范围。

把复合对象诊断交给小布
这些对象属性跟踪与动态数组结构,小布盯盘已内置到对应品种页的图形诊断里,你只需看盘口异动,不必自己翻库代码。

常见问题

上一版向数组末尾添加指针的方法复用了同一外部对象指针,导致多轴点及超两级对象在属性修改时被跳过,本篇修正了该逻辑。
小布盯盘内置了标准图形对象的属性诊断,对基于库创建的嵌套图形可展示轴点与级别变动,但复杂自定义复合体仍需在品种页手动载入模板。
水平线构造只用价格故移除时间属性支持,垂直线构造只用时间故移除全部实数型属性,二者在库类里做了针对性裁剪。
独立后可作为单独单元被任意图形类引用,修复了多轴点跟踪错误,也令后续自定义复合对象存储属性更灵活。
基本创建修改删除已具备,下一步倾向是完善基于标准对象搭建自定义复合图形的高层接口,详见系列后续规划。