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

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

(2/3)· 当对象被改名或误删,程序如何记住它之前叫什么、处在哪,这篇给出底层实现思路

含代码示例 第 2/3 篇
很多交易者写 EA 时只捕获对象事件 ID,却拿不到改名前后的具体名称,排查图表异动只能靠肉眼翻日志。把属性变化留痕做进库类,事件回调里就能直接读出旧名与新名,不必在删除后才拍脑袋猜。

「图表对象增量刷新的底层逻辑」

在 MT5 自定义控件库里,监控图表上手动画的对象不能靠轮询全量,而是用差值比对。CChartObjectsControl::Refresh 每次被调用时,先清空“新增对象列表”,再用 ObjectsTotal(ChartID()) 拿到当前图表对象总数,减去上次记录的 m_last_objects,得到 m_delta_graph_obj。 如果 delta 大于 0,说明有对象被加进来。此时分两种情况:只加了 1 个就用 LastAddedGraphObjName() 直接取名字;加了多个就按终端对象列表索引用 ObjectName() 遍历。这里会过滤掉程序自身用 m_name_program 前缀创建的对象,避免自己画的线被当成手动对象。 拿到名字后读 OBJPROP_TYPE 判断类型,映射到内部枚举并调 CreateNewGraphObj 生成包装类。创建失败或加不进列表都会写日志并 delete 掉,保证列表里全是有效指针。你可以把这段逻辑拷进自己的 EA 面板类,在 OnChartEvent 里触发 Refresh,就能实时捕获交易者在图上手画的支撑线。 外汇和贵金属图表上手动标线很频繁,这种增量捕获比全量扫描省 CPU,但高频画图时仍可能漏掉极短生命周期的对象,概率上需注意事件队列积压。

MQL5 / C++
class="type">void CChartObjectsControl::Refresh(class="type">void)
  {
class=class="str">"cmt">//--- Clear the list of newly added objects
   this.m_list_new_graph_obj.Clear();
class=class="str">"cmt">//--- Calculate the number of new 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 an object is added to the chart
   if(this.m_delta_graph_obj>class="num">0)
     {
      class=class="str">"cmt">//--- Create the list of added graphical objects
      for(class="type">int i=class="num">0;i<this.m_delta_graph_obj;i++)
        {
         class=class="str">"cmt">//--- Get the name of the last added object(if a single new object is added),
         class=class="str">"cmt">//--- or a name from the terminal object list by index(if several objects have been added)
         class="type">class="kw">string name=(this.m_delta_graph_obj==class="num">1 ? this.LastAddedGraphObjName() : ::ObjectName(this.m_chart_id,i));
         class=class="str">"cmt">//--- Handle only non-programmatically created objects
         if(name==NULL || ::StringFind(name,this.m_name_program)>WRONG_VALUE)
            class="kw">continue;
         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);
         class=class="str">"cmt">//--- If failed to create an object, inform of that and move on to the new iteration
         if(obj==NULL)
            {
            CMessage::ToLog(DFUN,MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ);
            class="kw">continue;
            }
         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);
         class=class="str">"cmt">//--- If failed to add the object to the list, inform of that, remove the object and move on to the next iteration
         if(!this.m_list_new_graph_obj.Add(obj))
            {
            CMessage::ToLog(DFUN_ERR_LINE,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
            class="kw">delete obj;
            class="kw">continue;
            }
         }
      class=class="str">"cmt">//--- Send events to the control program chart from the created list
      for(class="type">int i=class="num">0;i<this.m_list_new_graph_obj.Total();i++)
        {
         CGStdGraphObj *obj=this.m_list_new_graph_obj.At(i);
         if(obj==NULL)

◍ 跨图表事件转发与对象回收的实现细节

在图表对象监控类里,发现新建图形对象后会立即用 EventChartCustom 把事件推给主控图表的 ChartID。注意第三个参数传的是 obj.TimeCreate() 的时间戳,配合对象名做唯一标识,这样主控 EA 收到自定义事件时能精确定位是哪个子图、哪个时刻画的线。 AddEventControlInd 方法负责把事件控制指标挂到当前图表。它先拼出 shortname,格式是 EventSend_From#<当前图ID>_To#<主控图ID>,再遍历 ChartIndicatorsTotal 返回的已挂指标数做重名检查。若同名指标已存在就直接返回 true,避免重复挂载导致事件双发。 对象集合类 CGraphElementsCollection 用 m_list_deleted_obj 单独存被删对象,和 m_list_all_graph_obj 全量列表分离。这样在高频重绘场景下(例如 1 分钟周期手动拖拽趋势线),删除事件不会污染活跃对象枚举,回溯删除记录只需查这一个数组。 外汇与贵金属图表对象事件联动属于高概率辅助手段,实际触发延迟受 MT5 主频与图表数量影响,可能在大行情时丢事件,建议开两个图表手动画一根线验证 EventChartCustom 的到达时延。

MQL5 / C++
class="kw">continue;
class=class="str">"cmt">//--- Send an event to the control program chart
::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_CREATE,this.ChartID(),obj.TimeCreate(),obj.Name());
}
}
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: Add the event control indicator to the chart|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartObjectsControl::AddEventControlInd(class="type">void)
  {
  if(this.m_handle_ind==INVALID_HANDLE)
    class="kw">return class="kw">false;
  ::ResetLastError();
  class="type">class="kw">string shortname="EventSend_From#"+(class="type">class="kw">string)this.ChartID()+"_To#"+(class="type">class="kw">string)this.m_chart_id_main;
  class="type">int total=::ChartIndicatorsTotal(this.ChartID(),class="num">0);
  for(class="type">int i=class="num">0;i<total;i++)
    if(::ChartIndicatorName(this.ChartID(),class="num">0,i)==shortname)
      {
       CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_ALREADY_EXIST_EVN_CTRL_INDICATOR);
       class="kw">return true;
      }
  class="kw">return ::ChartIndicatorAdd(this.ChartID(),class="num">0,this.m_handle_ind);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Collection of graphical objects                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\"+PATH_TO_EVENT_CTRL_IND;     class=class="str">"cmt">// Indicator for controlling graphical object events packed into the program resources
class CGraphElementsCollection : class="kw">public CBaseObj
  {
class="kw">private:
  CArrayObj            m_list_charts_control;     class=class="str">"cmt">// List of chart management objects
  CListObj             m_list_all_canv_elm_obj;   class=class="str">"cmt">// List of all graphical elements on canvas
  CListObj             m_list_all_graph_obj;      class=class="str">"cmt">// List of all graphical objects
  CArrayObj            m_list_deleted_obj;        class=class="str">"cmt">// List of removed graphical objects
  class="type">bool                 m_is_graph_obj_event;      class=class="str">"cmt">// Event flag in the list of graphical objects

图形对象集合的底层接口清单

在 MT5 自定义指标或 EA 里统一管理散落在多图表上的图形对象,核心是先维护一份内存中的对象集合。下面这段头文件声明给出了集合类该暴露的最小接口面,直接决定了后续能否低延迟地感知图表上对象的增删。 两个整型成员是集合状态的锚点:m_total_objects 记录当前图形对象总数,m_delta_graph_obj 存本次检查相对上一次的数量差。盯盘时若 delta 突然非零,往往意味着脚本外手动画了线或被别的 EA 删了对象,属于高概率需要重新布局信号层的触发点。 接口按职责切得很碎:IsPresentGraphElmInList / IsPresentGraphObjInList / IsPresentGraphObjOnChart 三层存在性判断,分别针对画布元素、集合内对象、图表实盘对象;GetChartObjectCtrlObj 与 CreateChartObjectCtrlObj 负责按 chart_id 拿或建控制器;RefreshByChartID 做指定图表的全量同步。外汇与贵金属图表切换品种频繁,这类按 ID 刷新比全局扫表更不容易卡顿。 集合自净逻辑也在这里:FindMissingObj 捞出「在集合却不在图表」的孤儿对象,FindExtraObj 反向找「在图表却没进集合」的漏网对象,再经 DeleteGraphObjFromList / MoveGraphObjToDeletedObjList 清退。实盘跑多周期黄金 EA 时,这类漏同步会让旧水平线残留,干扰人工判势,属于高风险操作下的隐性坑。

MQL5 / C++
  class="type">int                m_total_objects;                 class=class="str">"cmt">// Number of graphical objects
  class="type">int                m_delta_graph_obj;                class=class="str">"cmt">// Difference in the number of graphical objects compared to the previous check

class=class="str">"cmt">//--- Return the flag indicating the graphical element class object presence in the collection list of graphical elements
  class="type">bool               IsPresentGraphElmInList(class="kw">const class="type">int id,class="kw">const ENUM_GRAPH_ELEMENT_TYPE type_obj);
class=class="str">"cmt">//--- Return the flag indicating the presence of the graphical object class in the graphical object collection list
  class="type">bool               IsPresentGraphObjInList(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name);
class=class="str">"cmt">//--- Return the flag indicating the presence of a graphical object on a chart by name
  class="type">bool               IsPresentGraphObjOnChart(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name);
class=class="str">"cmt">//--- Return the pointer to the object of managing objects of the specified chart
  CChartObjectsControl *GetChartObjectCtrlObj(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Create a new object of managing graphical objects of a specified chart and add it to the list
  CChartObjectsControl *CreateChartObjectCtrlObj(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Update the list of graphical objects by chart ID
  CChartObjectsControl *RefreshByChartID(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Check if the chart window is present
  class="type">bool               IsPresentChartWindow(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Handle removing the chart window
  class="type">void               RefreshForExtraObjects(class="type">void);
class=class="str">"cmt">//--- Return the first free ID of the graphical(class="num">1) object and(class="num">2) element on canvas
  class="type">long               GetFreeGraphObjID(class="type">bool program_object);
  class="type">long               GetFreeCanvElmID(class="type">void);
class=class="str">"cmt">//--- Add a graphical object to the collection
  class="type">bool               AddGraphObjToCollection(class="kw">const class="type">class="kw">string source,CChartObjectsControl *obj_control);
class=class="str">"cmt">//--- Find an object present in the collection but not on a chart
  CGStdGraphObj     *FindMissingObj(class="kw">const class="type">long chart_id);
  CGStdGraphObj     *FindMissingObj(class="kw">const class="type">long chart_id,class="type">int &index);
class=class="str">"cmt">//--- Find the graphical object present on a chart but not in the collection
  class="type">class="kw">string             FindExtraObj(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Remove the graphical object class object from the graphical object collection list: (class="num">1) specified object, (class="num">2) by chart ID
  class="type">bool               DeleteGraphObjFromList(CGStdGraphObj *obj);
  class="type">void               DeleteGraphObjectsFromList(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Move the graphical object class object to the list of removed graphical objects: (class="num">1) specified object, (class="num">2) by index
  class="type">bool               MoveGraphObjToDeletedObjList(CGStdGraphObj *obj);

「图形对象生命周期的底层接管」

在 MT5 自定义指标或 EA 里管理图表对象,容易踩坑的是对象销毁时机不受控。下面这组接口把图形对象的迁移与新建收口到统一控制层,避免图表上残留孤儿对象。 MoveGraphObjToDeletedObjList(const int index) 按索引把单个对象移入“已删除”列表,返回 bool 表示操作是否成功;MoveGraphObjectsToDeletedObjList(const long chart_id) 则按图表 ID 批量迁移该图表下全部对象,无返回值。 DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj) 负责把管理图表的对象控制器自身从列表剔除,防止控制器析构时反复引用。CreateNewStdGraphObject(...) 是新建标准图形对象的核心,最多支持 5 个时间点与价格坐标(time1~time5、price1~price5),后四个坐标默认填 0,适配折线、通道等多点对象。 外汇与贵金属图表对象受报价跳空影响,批量迁移逻辑可能在极端波动时滞后,实盘前建议在 MTK 策略测试器用历史数据跑一遍对象计数。

MQL5 / C++
class="type">bool MoveGraphObjToDeletedObjList(class="kw">const class="type">int index);
class=class="str">"cmt">//--- Move all objects by chart ID to the list of removed graphical objects
class="type">void MoveGraphObjectsToDeletedObjList(class="kw">const class="type">long chart_id);
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);

◍ 图形元素集合的取数接口

CGraphElementsCollection 类提供了一组 Get 方法,把内部维护的对象集合对外暴露,方便上层逻辑直接拿指针操作,不必关心底层存储结构。 GetObject() 返回类实例自身引用;GetListGraphObj() 与 GetListCanvElm() 分别返回标准图形对象总表、画布元素总表的原始 CArrayObj 指针,调用方可以遍历全部元素。 按属性筛选的 GetList 重载了三套参数:画布元素支持整数、实数、字符串属性加比较模式(默认 EQUAL);标准图形对象则多一个 index 参数定位具体对象属性。筛选结果也是 CArrayObj 指针,底层交给 CSelect 静态方法执行。 在 MT5 里接这段类定义后,可直接用 collection.GetList(CANV_ELEMENT_PROP_INTEGER::XXX, value) 拿到符合条件的画布元素数组,省去自己写遍历判断。外汇与贵金属图表元素受报价跳变影响,筛选结果可能随帧变化,属高风险环境下的动态数据。

MQL5 / C++
CGraphElementsCollection *GetObject(class="type">void) { class="kw">return &this; }
CArrayObj *GetListGraphObj(class="type">void) { class="kw">return &this.m_list_all_graph_obj; }
CArrayObj *GetListCanvElm(class="type">void) { class="kw">return &this.m_list_all_canv_elm_obj; }
CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),class="kw">property,value,mode); }
CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),class="kw">property,value,mode); }
CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),class="kw">property,value,mode); }
CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,index,value,mode); }
CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,index,value,mode); }
CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,index,value,mode); }

从已删对象列表反向捞回图形痕迹

做图形对象回放时,活着的对象好查,难的是用户手滑删掉的那些。上面这组接口把已删除对象单独挂进 m_list_deleted_obj,并通过三个重载的 GetListDel 按整型、双精度、字符串属性去筛,默认比较模式是 EQUAL,也就是精确匹配。 GetLastDeletedGraphObj 直接取 m_list_deleted_obj 的末尾元素,索引算的是 Total()-1。这意味着你若想复盘最近一次被清掉的画线,不必遍历全表,一行调用就能拿到句柄,但注意列表为空时 Total() 返回 0,减 1 会越界,实盘前务必先判 NewObjects 或 IsEvent 的状态。 GetListDeletedObj 返回的是指针引用,配合 CSelect::ByGraphicStdObjectProperty 能在 MT5 里把某张图表历史上删过的对象按属性捞回来。外汇与贵金属图表对象受价格跳空影响,删除事件可能集中爆发,这类查询只反映终端内存状态,不代行情方向,杠杆品种高风险依旧。

MQL5 / C++
CArrayObj      *GetListDel(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL)  { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),class="kw">property,index,value,mode);  }
CArrayObj      *GetListDel(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),class="kw">property,index,value,mode);  }
CArrayObj      *GetListDel(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),class="kw">property,index,value,mode);  }
class=class="str">"cmt">//--- Return the number of new graphical objects, (class="num">3) the flag of the occurred change in the list of graphical objects
  class="type">int            NewObjects(class="type">void)  class="kw">const                                                  { class="kw">return this.m_delta_graph_obj;     }
  class="type">bool           IsEvent(class="type">void) class="kw">const                                                      { class="kw">return this.m_is_graph_obj_event;  }
class=class="str">"cmt">//--- Return an(class="num">1) existing and(class="num">2) removed graphical object by chart name and ID
  CGStdGraphObj   *GetStdGraphObject(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id);
  CGStdGraphObj   *GetStdDelGraphObject(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Return the list of(class="num">1) chart management objects and(class="num">2) removed graphical objects
  CArrayObj      *GetListChartsControl(class="type">void)                                              { class="kw">return &this.m_list_charts_control;  }
  CArrayObj      *GetListDeletedObj(class="type">void)                                                 { class="kw">return &this.m_list_deleted_obj;    }
class=class="str">"cmt">//--- Return(class="num">1) the last removed graphical object and(class="num">2) the array size of graphical object properties
  CGStdGraphObj   *GetLastDeletedGraphObj(class="type">void)     class="kw">const { class="kw">return this.m_list_deleted_obj.At(this.m_list_deleted_obj.Total()-class="num">1); }
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象更名与删除的留痕视图,把重复劳动交给小布,你专注决策。

常见问题

库类在事件回调中比对图表物理对象与集合内类对象的名称字段,名称不一致且 ID 相同即判定为重命名,可触发历史记录写入。
可以,小布盯盘的品种页已集成对象事件留痕,能列出改名前与改名后的名称及发生时间,省去自己解析日志。
暂存后能读取其全部属性,既可编程恢复误删对象,也为后续复现属性连续变化提供数据基础。
仅在重命名时追加一条记录,常规行情刷新不触碰该数组,对高频刷新的影响概率较低,但对象极多时仍建议限制历史深度。