DoEasy 函数库中的图形(第九十一部分):标准图形对象事件。 对象名称更改历史记录·综合运用
🧠

DoEasy 函数库中的图形(第九十一部分):标准图形对象事件。 对象名称更改历史记录·综合运用

(3/3)·还在日志里盲猜哪个画线被改名?本篇把对象重命名轨迹直接存进属性数组

含代码示例实战向 第 3/3 篇
很多交易者只在对象消失后才发现改名打乱了 EA 的锚定逻辑。其实标准图形对象事件里就带着前后名称,只是多数库没把它留下来。先把重命名历史存进属性,后面复原状态才不用翻图表。

「图形对象集合类的构造与尺寸读取」

在 MT5 自定义图形库里,CGraphElementsCollection 负责统管图表上所有图形对象。它的构造函数不只设类型标识,还顺手打开了鼠标移动与滚轮事件监听,方便后续交互捕捉。 读取某个对象当前尺寸时,下面这个函数先按名和图表 ID 拿到标准图形对象指针,空指针就返回 0,否则取对应属性尺寸。 需要注意,Refresh 分两种重载:无参刷新全部图表对象,带 chart_id 的只刷指定图表,并会统计新增数量、置事件旗。外汇与贵金属图表对象密集时,频繁 Refresh 可能拖慢主线程,建议按图表粒度调用。 构造函数里还把总对象链表按画布元素 ID 排序并清空,控制图表链表也清空待填,这意味着每次新建集合都是从干净状态起步。

MQL5 / C++
class="type">int GetSizeProperty(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id,class="kw">const class="type">int prop)
  {
   CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id);
   class="kw">return(obj!=NULL ? obj.Properties().CurrSize(prop) : class="num">0);
  }

图表对象集合的刷新与增删追踪

图形对象集合类在 Refresh 里先调用 RefreshForExtraObjects 处理额外对象,随后用 ChartNext 遍历终端内所有已开图表,MT5 常量 CHARTS_MAX 上限为 100,因此循环最多跑 100 次,避免无边界扫描拖慢 EA。 每次拿到 chart_id 后,RefreshByChartID 返回 CChartObjectsControl 指针并就地更新该图表的对象清单;若返回 NULL 直接 continue 跳到下一张图,不阻塞后续刷新。 真正的变化判定靠 obj_ctrl.IsEvent():当 Delta() 大于 0 说明有对象被画上图表,走 AddGraphObjToCollection 搬进集合;小于 0 则说明被删,按 -Delta() 的次数循环,用 FindMissingObj 在旧列表里定位消失的对象并提取其 ChartID 等参数。 删除分支里 m_list_deleted_obj 先 Clear 再 Sort,意味着每次刷新前清空已删记录、重新排序待处理列表;这套机制让集合在任何图表对象变动后都能在下次 tick 前保持一致状态,外汇与贵金属图表多对象高频重绘时尤其容易触发,属于高波动环境下的常见技术风险点。

MQL5 / C++
this.m_total_objects=class="num">0;
this.m_is_graph_obj_event=class="kw">false;
this.m_list_deleted_obj.Clear();
this.m_list_deleted_obj.Sort();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update the list of all graphical objects                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGraphElementsCollection::Refresh(class="type">void)
  {
   this.RefreshForExtraObjects();
class=class="str">"cmt">//--- Declare variables to search for charts
   class="type">long chart_id=class="num">0;
   class="type">int i=class="num">0;
class=class="str">"cmt">//--- In the loop by all open charts in the terminal(no more than class="num">100)
   class="kw">while(i<CHARTS_MAX)
     {
      class=class="str">"cmt">//--- Get the chart ID
      chart_id=::ChartNext(chart_id);
      if(chart_id<class="num">0)
         class="kw">break;
      class=class="str">"cmt">//--- Get the pointer to the object for managing graphical objects
      class=class="str">"cmt">//--- and update the list of graphical objects by chart ID
      CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id);
      class=class="str">"cmt">//--- If failed to get the pointer, move on to the next chart
      if(obj_ctrl==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- If the number of objects on the chart changes
      if(obj_ctrl.IsEvent())
        {
         class=class="str">"cmt">//--- If a graphical object is added to the chart
         if(obj_ctrl.Delta()>class="num">0)
           {
            class=class="str">"cmt">//--- Get the list of added graphical objects and move them to the collection list
            class=class="str">"cmt">//--- (if failed to move the object to the collection, move on to the next object)
            if(!this.AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl))
               class="kw">continue;
           }
         class=class="str">"cmt">//--- If the graphical object has been removed
         else if(obj_ctrl.Delta()<class="num">0)
           {
            class="type">int index=WRONG_VALUE;
            class=class="str">"cmt">//--- In the loop by the number of removed graphical objects
            for(class="type">int j=class="num">0;j<-obj_ctrl.Delta();j++)
              {
               class=class="str">"cmt">// Find an extra object in the list
               CGStdGraphObj *obj=this.FindMissingObj(chart_id,index);
               if(obj!=NULL)
                 {
                  class=class="str">"cmt">//--- Get the removed object parameters
                  class="type">long   lparam=obj.ChartID();

◍ 图表子窗口被删时的对象回收逻辑

在 MT5 的自定义图形对象管理类里,当某个受控图表窗口被交易者手动关闭或程序卸载时,主图控件必须感知这一事件并把残留对象清理掉,否则集合类会一直持有悬空指针。 RefreshForExtraObjects 方法从后往前遍历 m_list_charts_control,用 IsPresentChartWindow 比对窗口是否还在终端里。若不在,就先记下 chart_id 与 symbol,再调用 DeleteGraphObjCtrlObjFromList 把该窗口的控制器移出列表。 关键的回收动作是 MoveGraphObjectsToDeletedObjList(chart_id)——它会把该图表下所有标准图形对象转存到“已删除”列表,随后通过 EventChartCustom 向主图抛 GRAPH_OBJ_EVENT_DEL_CHART 事件,参数里带了 del_obj 数量,方便上层统计一次清理掉了多少对象。 FindMissingObj 则反向排查:从总对象列表里按 GRAPH_OBJ_PROP_CHART_ID 筛出属于某 chart_id 的项,若图表已失联就能定位到“在集合里、不在图上”的孤儿对象。实盘跑这套代码时,建议你在 EURUSD 的 M5 上开两个离线子窗口画线,关掉其中一个,观察主图自定义事件里的 del_obj 是否等于你画的数量。外汇与贵金属杠杆高,这类对象泄漏在长时间挂 EA 时可能引发内存漂移,需自行验证稳定性。

MQL5 / C++
class="type">void CGraphElementsCollection::RefreshForExtraObjects(class="type">void)
  {
   for(class="type">int i=this.m_list_charts_control.Total()-class="num">1;i>WRONG_VALUE;i--)
     {
      CChartObjectsControl *obj_ctrl=this.m_list_charts_control.At(i);
      if(obj_ctrl==NULL)
         class="kw">continue;
      if(!this.IsPresentChartWindow(obj_ctrl.ChartID()))
        {
         class="type">long chart_id=obj_ctrl.ChartID();
         class="type">class="kw">string chart_symb=obj_ctrl.Symbol();
         class="type">int total_ctrl=this.m_list_charts_control.Total();
         this.DeleteGraphObjCtrlObjFromList(obj_ctrl);
         class="type">int total_obj=this.m_list_all_graph_obj.Total();
         this.MoveGraphObjectsToDeletedObjList(chart_id);
         class="type">int del_ctrl=total_ctrl-this.m_list_charts_control.Total();
         class="type">int del_obj=total_obj-this.m_list_all_graph_obj.Total();
         ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DEL_CHART,chart_id,del_obj,chart_symb);
        }
     }
  }

CGStdGraphObj *CGraphElementsCollection::FindMissingObj(class="kw">const class="type">long chart_id,class="type">int &index)
  {
   CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,class="num">0,chart_id,EQUAL);
   if(list==NULL)
      class="kw">return NULL;
   index=WRONG_VALUE;

「图表对象回收时的索引与内存处理」

在图形对象集合类里,查找第一个不在当前图表上的对象时,用正向循环从 0 扫到 list.Total(),一旦 IsPresentGraphObjOnChart 返回 false 就记下 index 并直接 return obj,平均查找复杂度与残留对象数线性相关。 按图表 ID 批量迁移到已删除列表时,代码反过来用 for(int i=list.Total()-1;i>WRONG_VALUE;i--) 倒序遍历,避免 Detach 改变数组下标后漏删。这一处若改成正向循环,删除第 i 个后后面的元素前移,会跳过相邻对象。 单个对象迁移靠 Detach 先从总表剥离指针,再尝试 Add 进删除表;任意一步失败都写日志并返回 false,Add 失败还会 delete obj 防止野指针。外汇与贵金属图表上脚本频繁增删对象时,这种显式回收能降低 MT5 终端内存泄漏概率,但高频调用仍属高风险操作,建议开 MT5 用 Experts 日志验证 Detach/Add 的返回路径。

MQL5 / C++
for(class="type">int i=class="num">0;i<list.Total();i++)
  {
    CGStdGraphObj *obj=list.At(i);
    if(obj==NULL)
      class="kw">continue;
    if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name()))
      {
       index=i;
       class="kw">return obj;
      }
  }
 class="kw">return NULL;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Move all objects(by chart ID)                                     |
class=class="str">"cmt">//| to the list of removed graphical objects                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGraphElementsCollection::MoveGraphObjectsToDeletedObjList(class="kw">const class="type">long chart_id)
  {
   CArrayObj *list=GetList(GRAPH_OBJ_PROP_CHART_ID,class="num">0,chart_id,EQUAL);
   if(list==NULL)
      class="kw">return;
   for(class="type">int i=list.Total()-class="num">1;i>WRONG_VALUE;i--)
     {
      CGStdGraphObj *obj=list.At(i);
      if(obj==NULL)
        class="kw">continue;
      this.MoveGraphObjToDeletedObjList(obj);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Move the class object of the graphical object by index             |
class=class="str">"cmt">//| to the list of removed graphical objects                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(class="kw">const class="type">int index)
  {
   CGStdGraphObj *obj=this.m_list_all_graph_obj.Detach(index);
   if(obj==NULL)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST);
      class="kw">return class="kw">false;
     }
   if(!this.m_list_deleted_obj.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST);
      class="kw">delete obj;
      class="kw">return class="kw">false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Move the specified graphical object class object                   |

图形对象回收与图表事件捕获的实现细节

在 MT5 自定义指标或 EA 里管理大量图形对象时,删除对象的归集逻辑直接影响后续回查效率。下面这段把待删对象从总表移入回收表的函数,先对总表做排序再按指针检索下标,最后复用按索引转移的重载方法。 [CODE] bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(CGStdGraphObj *obj) { this.m_list_all_graph_obj.Sort(); int index=this.m_list_all_graph_obj.Search(obj); return this.MoveGraphObjToDeletedObjList(index); } [/CODE] 逐行拆解:第 1 行声明返回布尔值的成员函数,入参为图形对象指针;第 3 行对全量对象链表排序,保证检索下标稳定;第 4 行用 Search 按指针定位对象在链表中的整型下标;第 5 行把下标传给另一个重载函数完成实际迁移并返回结果。 按图表名与 ID 反查已删对象时,先取对应图表 ID 的删除列表,再用名称做等值筛选,命中即返回首元素,否则空指针。 [CODE] CGStdGraphObj *CGraphElementsCollection::GetStdDelGraphObject(const string name,const long chart_id) { CArrayObj *list=this.GetListDel(GRAPH_OBJ_PROP_CHART_ID,0,chart_id); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,0,name,EQUAL); return(list!=NULL && list.Total()>0 ? list.At(0) : NULL); } [/CODE] OnChartEvent 里对 OBJECT_CHANGE / DRAG / CLICK 三类事件做了双向判断:既处理当前图表原生事件,也兼容自定义事件号偏移。图表 ID 的推导规则是——点击事件取 ChartID(),自定义点击事件从 lparam 取,其余按 lparam 为 0 则 ChartID() 否则 lparam。外汇与贵金属图表上这类对象交互频繁,自行扩展图形管理类时建议直接抄这套事件分发,能省掉重复判 ID 的坑。

MQL5 / C++
class="type">bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(CGStdGraphObj *obj)
  {
   this.m_list_all_graph_obj.Sort();
   class="type">int index=this.m_list_all_graph_obj.Search(obj);
   class="kw">return this.MoveGraphObjToDeletedObjList(index);
  }

CGStdGraphObj *CGraphElementsCollection::GetStdDelGraphObject(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id)
  {
   CArrayObj *list=this.GetListDel(GRAPH_OBJ_PROP_CHART_ID,class="num">0,chart_id);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,class="num">0,name,EQUAL);
   class="kw">return(list!=NULL && list.Total()>class="num">0 ? list.At(class="num">0) : NULL);
  }

◍ 对象改名后怎么在集合里同步

在 MT5 自定义图形对象管理类里,用名字去取对象若返回 NULL,说明图表上的对象已被改名、不在原集合清单中。此时先调用 FindMissingObj 在清单里找「图表上已不存在对应图形」的槽位,再调用 FindExtraObj 反查图表上多出来的新名对象。 若两个查找任一落空就直接 return,避免空指针继续跑。拿到旧名对象与新名后,用 EventChartCustom 把旧名发往主控制图(事件码 GRAPH_OBJ_EVENT_RENAME,lparam 传 obj.ChartID()),随后 obj.SetName(name_new) 把集合里的名字改成图表新名,保证两端一致。 标准图形事件走另一分支:idx 落在 (GRAPH_OBJ_EVENT_NO_EVENT, GRAPH_OBJ_EVENTS_NEXT_CODE) 区间内时,按 CREATE / CHANGE / RENAME / DELETE / DEL_CHART 五类用 switch 打印日志。其中前三种会经 GetStdGraphObject(sparam,lparam) 取回对象并 PrintShort(),DELETE 与 DEL_CHART 只打 ChartID 与 symbol。开 MT5 把这段挂到 OnChartEvent 里,改个手绘趋势线名字就能在专家日志看到 RENAME 同步过程。

MQL5 / C++
  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">//--- Send an event with the old name of an object to the control program chart and
     class=class="str">"cmt">//--- set a new name for the collection list object, which does not correspond to any graphical object on the chart
     ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),class="num">0,obj.Name());
     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();
  }
class=class="str">"cmt">//--- Handle standard graphical object events
  if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE)
   {
   class=class="str">"cmt">//--- Depending on the event type, display an appropriate message in the journal
   class="kw">switch(idx)
     {
      case GRAPH_OBJ_EVENT_CREATE   :
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE));
       obj=this.GetStdGraphObject(sparam,lparam);
       if(obj!=NULL)
         obj.PrintShort();
       class="kw">break;
      case GRAPH_OBJ_EVENT_CHANGE   :
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE));
       obj=this.GetStdGraphObject(sparam,lparam);
       if(obj!=NULL)
         obj.PrintShort();
       class="kw">break;
      case GRAPH_OBJ_EVENT_RENAME   :
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME));
       obj=this.GetStdGraphObject(sparam,lparam);
       if(obj!=NULL)
         obj.PrintShort();
       class="kw">break;
      case GRAPH_OBJ_EVENT_DELETE   :
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE));
       class="kw">break;
      case GRAPH_OBJ_EVENT_DEL_CHART:
       ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": ChartID: ",lparam,", ChartSymbol: ",sparam);

「图表事件里的对象改名追踪」

CGraphElementsCollection::OnChartEvent 负责把 MT5 图表上的交互事件路由到对应的图形对象。它先根据事件 id 与 CHARTEVENT_CUSTOM 的差值算出 idx,再判断是否属于 OBJECT_CHANGE、OBJECT_DRAG 或 OBJECT_CLICK 这三类,命中才继续处理。 图表 ID 的取法有讲究:普通点击事件直接用 ChartID();用户自定义事件则从 lparam 取;其余情况先给 WRONG_VALUE,再按 lparam 是否为 0 回退到当前图表或 lparam 本身。 当用 sparam 里的名字在集合里找不到对象时,说明图形对象被改名了。代码会调 FindMissingObj 找集合中缺失图表的对象,再用 FindExtraObj 拿到图表上的新名字,随后通过 SetNamePrev 与 SetName 更新,并发 GRAPH_OBJ_EVENT_RENAME 自定义事件给主图。 这套机制意味着:你在 MT5 上手拖或重命名趋势线时,底层集合能无声同步,不会丢引用。外汇与贵金属图表波动快、事件密集,重命名逻辑若漏判可能让 EA 误以为对象被删,实盘前应在回测器里故意改几次对象名验证。

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     || id==CHARTEVENT_OBJECT_CLICK  ||
       idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      class=class="str">"cmt">//--- Calculate the chart ID
      class=class="str">"cmt">//--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID
      class=class="str">"cmt">//--- If the event ID corresponds to a user event, the chart ID is received from lparam
      class=class="str">"cmt">//--- Otherwise, the chart ID is assigned to -class="num">1
      class="type">long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE);
      class="type">long chart_id=(param==WRONG_VALUE ? (lparam==class="num">0 ? ::ChartID() : lparam) : param);
      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,
         class=class="str">"cmt">//--- and send an event with the new name of the object to the control program chart
         if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name());
        }
      class=class="str">"cmt">//--- Update the properties of the obtained object

对象改名记录与暂停逻辑的底层接口

图形对象集合类里,PrintRenameHistory 方法靠 GetStdGraphObject 按名称与图表 ID 拿到对象指针,空指针直接 return,非空才调用 obj.PrintRenameHistory() 把改名历史吐到日志。想看某个画线是不是被脚本偷偷改过名,直接传原 name 和 chart_id 调这个方法即可。 Pause 的实现很直白:先设等待毫秒数,再设起始时间(time_start 乘 1000 转成毫秒),随后 while 死循环卡到 PauseIsCompleted 为真或 IsStopped 触发。这种忙等待在 EA 里会占满当前线程,回测时若 pause_msc 设 5000 就可能让 tick 处理挂起 5 秒,实盘贵金属跳空时段要警惕卡死风险。 GetListDeletedObj 返回被删对象数组,TotalDeletedGraphObjects 就是它的 Total(),用来数删了几个。GraphGetSizeProperty 透传 m_graph_objects.GetSizeProperty,拿某对象某属性的数组长度——比如查一条趋势线的锚点数组大小,返回 0 往往意味着对象已失效。

MQL5 / C++
class=class="str">"cmt">//--- and check their change
   obj.PropertiesRefresh();
   obj.PropertiesCheckChanged();
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display the history of renaming the object to the journal       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGraphElementsCollection::PrintRenameHistory(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id)
  {
  CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id);
  if(obj==NULL)
    class="kw">return;
  obj.PrintRenameHistory();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Launch the new pause countdown
   class="type">void                  Pause(class="kw">const class="type">class="kw">ulong pause_msc,class="kw">const class="type">class="kw">datetime time_start=class="num">0)
                      {
                       this.PauseSetWaitingMSC(pause_msc);
                       this.PauseSetTimeBegin(time_start*class="num">1000);
                       class="kw">while(!this.PauseIsCompleted() && !::IsStopped()){}
                      }
class=class="str">"cmt">//--- Return the(class="num">1) collection of graphical objects and(class="num">2) the list of removed objects
   CGraphElementsCollection *GetGraphicObjCollection(class="type">void)            { class="kw">return &this.m_graph_objects;            }
   CArrayObj            *GetListDeletedObj(class="type">void)                      { class="kw">return this.m_graph_objects.GetListDeletedObj();    }
class=class="str">"cmt">//--- Return(class="num">1) the number of removed graphical objects and(class="num">2) the size of the class="kw">property array
   class="type">int                  TotalDeletedGraphObjects(class="type">void)               { class="kw">return this.GetListDeletedObj().Total();            }
   class="type">int                  GraphGetSizeProperty(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id,class="kw">const class="type">int prop)
                      {
                       class="kw">return this.m_graph_objects.GetSizeProperty(name,chart_id,prop);
                      }

class=class="str">"cmt">//--- Fill in the array with IDs of the charts opened in the terminal

◍ 把对象事件处理挪进 EA 主响应里跑一遍

验证图形对象集合的事件逻辑,最省事的做法是复用前一篇的 EA,放到 \MQL5\Experts\TestDoEasy\Part91\ 目录下存成 TestDoEasyPart91.mq5。核心改动只有一处:把原本写在图形对象集合类事件处理器里的代码块,整体剪切到 EA 的 OnChartEvent() 函数体内。 那段代码模块的逻辑之前已拆过,这里不重复。编译后挂到图表上,能看到对象被重命名之后状态仍保留在“记忆”里,批量打包删除也按预期生效。 有个坑得记着:当图表子窗口被删掉再还原时,随图表一起消失的图形对象里,并非每一个都能在集合列表里正确恢复。目前还没定位到被略过的对象原因,后续会修,但实盘前你最好自己手动核对一遍对象列表。外汇和贵金属图表对象受tick驱动,这类恢复遗漏可能引发误判,属于高风险操作环节。

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);
            if(form==NULL)
               class="kw">return;

「悬浮窗体渲染与点击取价实现」

在 MT5 自定义指标里做价格行为标注,常需要把鼠标所在 K 线的形态描述弹成一个不可拖动的悬浮窗。下面这段逻辑先把窗体激活并锁死移动(SetMovable(false)),透明度压到 200,背景取色板首色、边框写死暗绿 C'47,70,59',再开阴影:偏移 2 像素、模糊半径 3、不透明度 200。 阴影色不是写死的——先用 ChangeColorSaturation 把背景降饱和成单色,若勾了「用图表背景色」(InpUseColorBG) 就再降 20 亮度,否则取 InpColorForm3。背景用 Erase 铺垂直渐变,边缘 DrawRectangle 描框,最后 Done() 把外观固化进对象列表;加列表失败直接 delete 返出,避免野指针。 窗体存在时,用 TextOnBGbar.BodyTypeDescription() 按中锚点写进窗体中心(字色 C'7,28,21')并 Show(),随后 ChartRedraw() 强制重绘。 点击交互另走 CHARTEVENT_CLICK:必须按住 Ctrl(IsCtrlKeyPressed)才往下走,否则直接 return。用 ChartXYToTimePrice 把鼠标 x/y 转成 timeprice,再 engine.GraphGetArrayChartsID 拉出图表 ID 数组——这一步是后续把标注绑到具体品种子图的前提,外汇与贵金属波动大,误触可能在高杠杆下干扰判断,实盘前应在模拟盘验证命中精度。

MQL5 / C++
form.SetActive(true);
form.SetMovable(class="kw">false);
form.SetOpacity(class="num">200);
form.SetColorBackground(array_clr[class="num">0]);
form.SetColorFrame(C&class="macro">#x27;class="num">47,class="num">70,class="num">59&class="macro">#x27;);
form.SetShadow(true);
class="type">class="kw">color clrS=form.ChangeColorSaturation(form.ColorBackground(),-class="num">100);
class="type">class="kw">color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-class="num">20) : InpColorForm3);
form.DrawShadow(class="num">2,class="num">2,clr,class="num">200,class="num">3);
form.Erase(array_clr,form.Opacity());
form.DrawRectangle(class="num">0,class="num">0,form.Width()-class="num">1,form.Height()-class="num">1,form.ColorFrame(),form.Opacity());
if(!list_forms.Add(form))
  {
   class="kw">delete form;
   class="kw">return;
  }
form.Done();
if(form!=NULL)
  {
   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();
  }
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))
     {
      class="type">long array[];
      engine.GraphGetArrayChartsID(array);

把自定义事件翻译回对象动作

在 MT5 的 EA 里接管图表事件时,自定义事件码要减掉 CHARTEVENT_CUSTOM 才能拿到真实索引。代码里用 ushort idx=ushort(id-CHARTEVENT_CUSTOM) 做这步偏移,再判断 idx 是否落在 GRAPH_OBJ_EVENT_NO_EVENT 与 GRAPH_OBJ_EVENTS_NEXT_CODE 之间,只有命中这段区间才往下处理图形对象事件。 创建与修改是两类高频事件。GRAPH_OBJ_EVENT_CREATE 分支直接 Print 创建提示,再用 GetStdGraphObject(sparam,lparam) 按图表名和对象 ID 取指针,非空就调 PrintShort 把新对象简况打到日志;GRAPH_OBJ_EVENT_CHANGE 分支同理,但额外根据 dparam 落在整数、双精度还是字符串属性区间,调 GetPropertyDescription 把被改的具体属性名也打印出来。 这套机制对外汇和贵金属图表上的手动画线、拖拽锚点特别有用——你改一条趋势线颜色或价位,EA 能立刻在日志看到是哪条线、哪个属性变了。外汇贵金属波动剧烈、杠杆风险高,用事件回显做人工干预审计,比盲目跟信号更稳。

MQL5 / C++
class="type">class="kw">ushort idx=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM);
CGStdGraphObj *obj=NULL;
if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE)
  {
   CChartObjectsControl *chart_ctrl=NULL;
   class="type">int end=class="num">0;
   class="type">class="kw">string evn="";
   class="kw">switch(idx)
     {
      case GRAPH_OBJ_EVENT_CREATE   :
         Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE),":");
         obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
         if(obj!=NULL)
           {
            obj.PrintShort();
           }
         class="kw">break;
      case GRAPH_OBJ_EVENT_CHANGE   :
         Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE),":");
         obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
         if(obj!=NULL)
           {
            obj.PrintShort();
            if(dparam<GRAPH_OBJ_PROP_INTEGER_TOTAL)
               evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_INTEGER)dparam);
            else if(dparam<GRAPH_OBJ_PROP_DOUBLE_TOTAL)
               evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_DOUBLE)dparam);
            else
               evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_STRING)dparam);
           }
         class="kw">break;
     }
  }

◍ 图形对象重命名与删除的事件捕获

在 MT5 的自定义引擎里,图形对象生命周期事件是通过 switch-case 分发处理的。重命名事件 GRAPH_OBJ_EVENT_RENAME 触发时,先用 sparam 和 lparam 从集合里取对象指针,再打印旧名与新名。 取对象后若指针非空,会输出最近一次名称(CurrSize-1 索引)与初始名称(0 索引),并调用 PrintRenameHistory 把整个改名链路写进日志。这类痕迹对复盘手动画线策略很有用。 删除分两种:单对象 GRAPH_OBJ_EVENT_DELETE 用 GetStdDelGraphObject 取已删对象并打印简况;随图表关闭的 GRAPH_OBJ_EVENT_DEL_CHART 则按 lparam 图表 ID 和 sparam 品种名批量处理。 循环边界用 engine.TotalDeletedGraphObjects() 减去 dparam 算 end,若 end<0 则归零,再从列表尾部向前遍历取对象,确保只处理本次事件新增的删除项。

MQL5 / C++
        Print(DFUN,evn);
      }
      class="kw">break;
      class=class="str">"cmt">//--- Graphical object renaming event
      case GRAPH_OBJ_EVENT_RENAME  :
      class=class="str">"cmt">//--- Display the message about renaming the graphical object
      Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME));
      class=class="str">"cmt">//--- Get the pointer to the object by chart name and ID passed to sparam and lparam, respectively
      obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
      if(obj!=NULL)
        {
         class=class="str">"cmt">//--- Display the previous and new object name, as well as its entire renaming history, in the journal
         Print(DFUN,obj.GetProperty(GRAPH_OBJ_PROP_NAME,obj.Properties().CurrSize(GRAPH_OBJ_PROP_NAME)-class="num">1)," >>> ",obj.GetProperty(GRAPH_OBJ_PROP_NAME,class="num">0));
         obj.PrintRenameHistory();
        }
      class="kw">break;
      class=class="str">"cmt">//--- Graphical object deletion event
      case GRAPH_OBJ_EVENT_DELETE  :
      class=class="str">"cmt">//--- Display the message about removing the graphical object
      Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE),":");
      class=class="str">"cmt">//--- Get the pointer to the removed object by chart name and ID passed to sparam and lparam, respectively
      class=class="str">"cmt">//--- and display a class="type">class="kw">short description of the removed object in the journal
      obj=engine.GetGraphicObjCollection().GetStdDelGraphObject(sparam,lparam);
      if(obj!=NULL)
        {
         obj.PrintShort();
        }
      class="kw">break;
      class=class="str">"cmt">//--- Event of removing the graphical object together with the chart window
      case GRAPH_OBJ_EVENT_DEL_CHART:
      class=class="str">"cmt">//--- Display the message about removing graphical objects together with the chart window, whose ID and symbol are passed to lparam and sparam
      Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": #",lparam,", ",sparam,":");
      class=class="str">"cmt">//--- Calculate the end value for the loop by the list of removed graphical objects
      end=engine.TotalDeletedGraphObjects()-(class="type">int)dparam;
      if(end<class="num">0)
         end=class="num">0;
      class=class="str">"cmt">//--- In the loop from the end of the removed graphical objects list up to the value calculated in the &class="macro">#x27;end&class="macro">#x27; variable,
      for(class="type">int i=engine.TotalDeletedGraphObjects()-class="num">1;i>=end;i--)
        {
         class=class="str">"cmt">//--- get the next removed graphical object from the list
         obj=engine.GetListDeletedObj().At(i);

「空对象跳过与简要日志输出」

在遍历图形对象集合时,若取到空指针直接 continue 是最省事也最安全的写法,能避免后续调用成员函数时触发运行时错误。 代码片段里 if(obj==NULL) continue; 之后紧接 obj.PrintShort();,意思是只对有效对象向终端日志打印一段简短描述,方便在 EA 调试阶段快速核对当前画布上的对象状态。 这种写法在外币与贵金属策略调试中很实用:MT5 客户端对象数上限约 2000 个,刷屏前先过滤空值,日志可读性会明显提升。外汇与贵金属杠杆高,任何对象句柄泄漏都可能让脚本在实盘前就崩,建议开 MT5 把这段贴进 OnInit 后的遍历逻辑里跑一遍。

MQL5 / C++
if(obj==NULL)
   class="kw">continue;
class=class="str">"cmt">//--- and display its brief description in the journal
obj.PrintShort();
   }
   class="kw">break;
   class=class="str">"cmt">//---
   class="kw">default:
   class="kw">break;
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

画得少,看得清

这一节原本只是作者交代下一篇要做的对象属性历史存储,以及放出当前函数库压缩包(MQL5.zip,约 4181.64 KB)供下载验证。从第八十六到九十部分一路铺下来,核心落点其实就一句:把图形对象的属性变化和事件先管起来,后面才有办法回溯。 你在 MT5 里把这套 EA 和指标挂上,重点不是看它画了多少线,而是确认对象改动有没有被集合捕获、跨图表同不同步。能跑通这点,下一篇的「属性修改历史」才接得住。 外汇和贵金属图表对象受报价跳变影响大,这类跟踪逻辑在极端滑点下可能漏事件,验证时先用模拟环境跑。

把对象轨迹交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象改名与删除的痕迹,你只需判断要不要回退。

常见问题

重命名事件触发时类对象名称已经变更,若不在改前暂存就丢失了旧名;本篇把前名写进多维属性数组来解决。
可以,小布盯盘的品种页将删除与改名事件做了可视化,省去自己解析事件 ID 和图表 ID 的重复劳动。
被删对象会进入已删除列表,程序可读取其全部属性,也支持意外删除后的恢复操作。
名称为后续全部属性连续快照打底,复合对象可借历史序列重现任意先前状态,下篇将展开。
它允许在任意维度扩大小布存储历史名称,不必预定义固定长度,重命名几次都能追加。