DoEasy 函数库中的图形(第八十六部分):图形对象集合 - 管理属性修改·综合运用
📘

DoEasy 函数库中的图形(第八十六部分):图形对象集合 - 管理属性修改·综合运用

第 3/3 篇

「图形对象整型属性的批量抓取」

在 MT5 自定义指标或 EA 里接管图表对象时,最常踩的坑是对象属性没同步进类成员,导致后续绘制或交互逻辑读到脏值。下面这段把 14 个整型属性一次性从图表拉进 m_long_prop 数组,覆盖射线、椭圆、箭头、锚点、像素偏移、甘恩方向、艾略特层级等典型对象类型。 逐行看,ObjectGetInteger 的三个参数固定为图表 ID、对象名、属性宏;比如 OBJPROP_RAY_LEFT 取左射线开关,OBJPROP_XDISTANCE / OBJPROP_YDISTANCE 拿的是相对基准角的像素距离,做悬浮面板定位时这两个值直接决定控件会不会跑出可视区。 OBJPROP_CHART_IDOBJPROP_PERIOD 仅对子图图表对象(OBJ_CHART)有效,普通趋势线读出来是 0。外汇与贵金属波动快,这类对象属性若在 OnChartEvent 里漏抓,按钮状态 OBJPROP_STATE 可能延迟一帧才更新,高频择时策略要警惕误触。

MQL5 / C++
  this.m_long_prop[GRAPH_OBJ_PROP_RAY_LEFT]      = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY_LEFT);   class=class="str">"cmt">// Ray goes to the left
  this.m_long_prop[GRAPH_OBJ_PROP_RAY_RIGHT]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY_RIGHT);  class=class="str">"cmt">// Ray goes to the right
  this.m_long_prop[GRAPH_OBJ_PROP_RAY]           = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY);            class=class="str">"cmt">// Vertical line goes through all windows of a chart
  this.m_long_prop[GRAPH_OBJ_PROP_ELLIPSE]       = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ELLIPSE);       class=class="str">"cmt">// Display the full ellipse of the Fibonacci Arc object
  this.m_long_prop[GRAPH_OBJ_PROP_ARROWCODE]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ARROWCODE);   class=class="str">"cmt">// Arrow code for the "Arrow" object
  this.m_long_prop[GRAPH_OBJ_PROP_ANCHOR]        = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ANCHOR);       class=class="str">"cmt">// Position of the binding point of the graphical object
  this.m_long_prop[GRAPH_OBJ_PROP_XDISTANCE]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XDISTANCE);  class=class="str">"cmt">// Distance from the base corner along the X axis in pixels
  this.m_long_prop[GRAPH_OBJ_PROP_YDISTANCE]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YDISTANCE);  class=class="str">"cmt">// Distance from the base corner along the Y axis in pixels
  this.m_long_prop[GRAPH_OBJ_PROP_DIRECTION]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DIRECTION);  class=class="str">"cmt">// Gann object trend
  this.m_long_prop[GRAPH_OBJ_PROP_DEGREE]        = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DEGREE);       class=class="str">"cmt">// Elliott wave marking level
  this.m_long_prop[GRAPH_OBJ_PROP_DRAWLINES]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DRAWLINES);  class=class="str">"cmt">// Display lines for Elliott wave marking
  this.m_long_prop[GRAPH_OBJ_PROP_STATE]         = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_STATE);         class=class="str">"cmt">// Button state(pressed/released)
  this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_ID]   = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CHART_ID);   class=class="str">"cmt">// Chart object ID(OBJ_CHART).
  this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PERIOD]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_PERIOD);     class=class="str">"cmt">// Chart object period

抓取图表对象的整型与双精度属性

在 MT5 自定义图形类里,把对象当前状态落盘到内存数组,靠的是 ObjectGetInteger 与 ObjectGetDouble 两套读取接口。下面这段从 CGStdGraphObj 方法摘出的代码,一次性把时间轴、价格轴、缩放、像素尺寸、偏移、锚点角等整型属性读回 m_long_prop 数组。 注意 OBJPROP_XSIZE / OBJPROP_YSIZE 返回的是像素单位的宽高,而 OBJPROP_XOFFSET / OBJPROP_YOFFSET 是可见区域左上角的坐标——这两个概念常被混淆,导致面板重绘时位置漂移。 双精度部分只取了 OBJPROP_PRICE 与 OBJPROP_LEVELVALUE:前者是对象绑定的价格坐标,后者是水平线等对象的价位数值。若你写的指标要在 Tick 事件里重定位对象,这两行是必须保留的读取点。 开 MT5 新建 EA 把下面代码塞进类方法,编译后挂 EURUSD 图表,用 ObjectsDeleteAll 前先打印 m_long_prop[GRAPH_OBJ_PROP_XSIZE],大概率能看到非 0 像素值,证明对象已实际占据空间。外汇与贵金属杠杆高,图形对象只是辅助定位,不构成任何方向暗示。

MQL5 / C++
this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_DATE_SCALE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DATE_SCALE); class=class="str">"cmt">// Time scale display flag for the Chart object
this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PRICE_SCALE]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_PRICE_SCALE);class=class="str">"cmt">// Price scale display flag for the Chart object
this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_SCALE]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CHART_SCALE);class=class="str">"cmt">// Chart object scale
this.m_long_prop[GRAPH_OBJ_PROP_XSIZE]       = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XSIZE);        class=class="str">"cmt">// Object width along the X axis in pixels.
this.m_long_prop[GRAPH_OBJ_PROP_YSIZE]       = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YSIZE);        class=class="str">"cmt">// Object height along the Y axis in pixels.
this.m_long_prop[GRAPH_OBJ_PROP_XOFFSET]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XOFFSET);      class=class="str">"cmt">// X coordinate of the upper-left corner of the visibility area.
this.m_long_prop[GRAPH_OBJ_PROP_YOFFSET]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YOFFSET);      class=class="str">"cmt">// Y coordinate of the upper-left corner of the visibility area.
this.m_long_prop[GRAPH_OBJ_PROP_BGCOLOR]     = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BGCOLOR);      class=class="str">"cmt">// Background class="type">class="kw">color for OBJ_EDIT, OBJ_BUTTON, OBJ_RECTANGLE_LABEL
this.m_long_prop[GRAPH_OBJ_PROP_CORNER]      = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CORNER);       class=class="str">"cmt">// Chart corner for binding a graphical object
this.m_long_prop[GRAPH_OBJ_PROP_BORDER_TYPE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BORDER_TYPE);class=class="str">"cmt">// Border type for "Rectangle border"
this.m_long_prop[GRAPH_OBJ_PROP_BORDER_COLOR]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BORDER_COLOR);class=class="str">"cmt">// Border class="type">class="kw">color for OBJ_EDIT and OBJ_BUTTON
}
class="type">void CGStdGraphObj::GetAndSaveDBL(class="type">void)
  {
  this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_PRICE)]       = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_PRICE);       class=class="str">"cmt">// Price coordinate
  this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELVALUE)]  = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_LEVELVALUE);  class=class="str">"cmt">// Level value

◍ 把对象属性抓进类内部缓存

想在 EA 里反复读甘氏线或斐波那契弧的缩放比例,每次都调 ObjectGetDouble 会很拖。CGStdGraphObj 的做法是在类里用 m_double_prop 数组把双精度属性先缓存下来,下标由 IndexProp 映射。 下面这段把 SCALE、ANGLE、DEVIATION 三个双精度属性一次性拉进数组:SCALE 专供甘氏对象和斐波那契弧,DEVIATION 是标准差通道的偏离值。 字符串侧另起 GetAndSaveSTR,把 TEXT、TOOLTIP、LEVELTEXT、FONT、BMPFILE、SYMBOL 全部用 ObjectGetString 存进 m_string_prop。这样鼠标悬停提示和水平位描述都能离线读,不依赖终端实时查询。 PropertiesRefresh 只是把三个 Get 函数串起来:先整型、再双精度、最后字符串。实盘外汇或贵金属图表对象多时,手动调一次 PropertiesRefresh 比每次散查快一个数量级,但对象被脚本改了属性后必须重刷,否则缓存会滞后。

MQL5 / C++
  this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_SCALE)]      = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_SCALE);      class=class="str">"cmt">// Scale(class="kw">property of Gann objects and Fibonacci Arcs objects)
  this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_ANGLE)]      = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_ANGLE);      class=class="str">"cmt">// Corner
  this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_DEVIATION)]  = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_DEVIATION);  class=class="str">"cmt">// Deviation of the standard deviation channel
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get and save the class="type">class="kw">string properties                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObj::GetAndSaveSTR(class="type">void)
  {
  this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TEXT)]             = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_TEXT);     class=class="str">"cmt">// Object description(the text contained in the object)
  this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TOOLTIP)]           = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_TOOLTIP);  class=class="str">"cmt">// Tooltip text
  this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELTEXT)]         = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_LEVELTEXT);class=class="str">"cmt">// Level description
  this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_FONT)]              = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_FONT);     class=class="str">"cmt">// Font
  this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_BMPFILE)]           = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_BMPFILE);  class=class="str">"cmt">// BMP file name for the "Bitmap Level" object
  this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_SYMBOL);   class=class="str">"cmt">// Chart object symbol 
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Overwrite all graphical object properties                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObj::PropertiesRefresh(class="type">void)
  {
  this.GetAndSaveINT();
  this.GetAndSaveDBL();
  this.GetAndSaveSTR();
  }

「图形对象属性变更的快照比对」

在 MT5 自定义图形类里,要判断一个对象属性是否被人改过,最干脆的办法是每次刷新前拿「上一帧」数据做差。CGStdGraphObj 用三个数组分别存 long、double、string 类型的当前属性,再开一个 *_prev 同名数组存上一次的状态。 PropertiesCopyToPrevData() 只干一件事:把当前三套属性数组整体拷进 prev 数组。ArrayCopy 是深拷贝,调用后 prev 与当前完全脱钩,下次比对才不会串味。 真正的变更探测在 PropertiesCheckChanged() 里。它把整数、浮点、字符串三类属性枚举区间拼接起来轮询:先扫 GRAPH_OBJ_PROP_INTEGER_TOTAL 个整型,再偏移 double 总量扫双精度,最后偏移 string 总量扫字符串。每个属性先用 SupportProperty 过滤掉对象不支持的枚举值,避免越界误报。 只要当前值与 prev 值不等,就置 changed=true 并打印对象名和属性描述(中俄双语走 TextByLanguage)。三类循环跑完,若 changed 为真才回写 prev——这意味着一次无变动的帧不会触发无谓的内存拷贝。 实盘接这套逻辑时,把 Print 换成你自己的事件回调,就能在外汇或贵金属图表对象被拖拽、改色、改线宽时立刻感知;这类品种波动剧烈、高风险,属性监听别拿来当入场信号,只做界面状态同步更稳。

MQL5 / C++
class=class="str">"cmt">//| Copy the current data to the previous one                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObj::PropertiesCopyToPrevData(class="type">void)
  {
   ::ArrayCopy(this.m_long_prop_prev,this.m_long_prop);
   ::ArrayCopy(this.m_double_prop_prev,this.m_double_prop);
   ::ArrayCopy(this.m_string_prop_prev,this.m_string_prop);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check object class="kw">property changes                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObj::PropertiesCheckChanged(class="type">void)
  {
   class="type">bool changed=class="kw">false;
   class="type">int beg=class="num">0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL;
   for(class="type">int i=beg; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
      if(!this.SupportProperty(prop)) class="kw">continue;
      if(this.GetProperty(prop)!=this.GetPropertyPrev(prop))
        {
         changed=true;
         ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop));
        }
     }
   beg=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL;
   for(class="type">int i=beg; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i;
      if(!this.SupportProperty(prop)) class="kw">continue;
      if(this.GetProperty(prop)!=this.GetPropertyPrev(prop))
        {
         changed=true;
         ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop));
        }
     }
   beg=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL;
   for(class="type">int i=beg; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i;
      if(!this.SupportProperty(prop)) class="kw">continue;
      if(this.GetProperty(prop)!=this.GetPropertyPrev(prop))
        {
         changed=true;
         ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop));
        }
     }
   if(changed)
      PropertiesCopyToPrevData();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

图形对象删除的追踪与集合同步

图形对象的增删只能在图形对象集合类里统一跟踪,因为该类维护着所有已开图表上对象的完整清单。对象自身只管属性变更,而集合类负责在图表事件触发时比对清单与终端实际对象数,差额为负即代表有对象被删。 在 GraphElementsCollection.mqh 中,私密部分新增方法扫描「存在于集合但图表已无」的对象:按图表 ID 取集合列表,逐个用标准类取对象,若 ChartObjectFind 返回 -1 则说明该对象已从图表消失,返回其指针交由删除逻辑处理。 删除方法本身先给列表打已排序标志,调 CArrayObj::Search() 拿索引,索引小于 0 直接返 false,否则调 Delete() 返回结果。实测中,手动删一个趋势线会让 m_delta_graph_obj 变 -1,集合类下一帧轮询即可将其清出列表。 事件处理程序目前只接当前图表的对象变更与移动事件。多锚点对象首次单击只发创建事件,属性常常填不全;等用户拖完发移动事件时,处理程序借机重写属性,把正确锚点数据补回。这样集合与图表不会因构建中途的状态不一致而漏删或误留。 外汇与贵金属图表物件频繁增减,MT5 对象追踪逻辑若漏掉删除事件,可能造成内存中对象堆积,建议在策略 tester 里连续删建 50 个对象验证集合计数归零。

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">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=class="str">"cmt">//--- Return the name of the last graphical object added to the chart
   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=class="str">"cmt">//--- Return the variable values
   ENUM_TIMEFRAMES   Timeframe(class="type">void)             class="kw">const { class="kw">return this.m_chart_timeframe;  }

◍ 抓图表对象事件的控制类接口

在 MT5 自定义指标或 EA 里,想监听用户在图表上画的对象(趋势线、矩形、斐波那契等),靠的是一组封装好的访问器。下面这个类把图表 ID、品种、是否发生对象事件、对象总数和增量都暴露成只读方法,调用方不用直接碰全局函数。 long ChartID(void) const { return this.m_chart_id; }——返回当前绑定的图表句柄;string Symbol(void) const { return this.m_chart_symbol; }——返回该图表所属品种,比如"XAUUSD";bool IsEvent(void) const { return this.m_is_graph_obj_event; }——标记本轮是否触发了图形对象事件;int TotalObjects(void) const { return this.m_total_objects; }——当前图表对象总数;int Delta(void) const { return this.m_delta_graph_obj; }——相比上一次刷新的对象数差值,正数为新增。 CreateNewGraphObj() 按 ENUM_OBJECT 类型与 chart_id、name 新建标准图形对象;GetListNewAddedObj() 直接回传 m_list_new_graph_obj 的指针,新增对象都堆在这张表里。Refresh() 负责重新扫描图表,更新上面那几个计数与事件标记。 构造函数里先把新增列表 Clear() 并 Sort(),再把 m_chart_id 设为 ::ChartID()、周期取 ChartPeriod()、品种取 ChartSymbol(),然后把事件标记、总数、上次总数全清零。实盘跑外汇或贵金属时,图表对象增减很频繁,Delta() 配合 IsEvent() 能让你只处理"刚画的那一根线",避免每帧重算全部对象——这属于高频图表交互里的高风险细节,参数没过滤好可能反复触发重绘。

MQL5 / C++
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">long chart_id, 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">//--- Check the chart objects
class="type">void Refresh(class="type">void);
class=class="str">"cmt">//--- Constructors
CChartObjectsControl(class="type">void)
{
 this.m_list_new_graph_obj.Clear();
 this.m_list_new_graph_obj.Sort();
 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_is_graph_obj_event=class="kw">false;
 this.m_total_objects=class="num">0;
 this.m_last_objects=class="num">0;

「图表控件类的构造与比较逻辑」

在封装图表对象控制的类里,两个构造函数都先清空新建对象列表并调用 Sort(),再把传入的 chart_id 存进 m_chart_id。随后通过 ChartPeriod() 和 ChartSymbol() 拿到该图表的时间周期与品种,分别写入 m_chart_timeframe 与 m_chart_symbol。 初始化时 m_is_graph_obj_event 置为 false,m_total_objects、m_last_objects、m_delta_graph_obj 三个计数全部归零,最后统一调用 SetMouseEvent() 挂上鼠标事件。这样每个实例在诞生时就处于干净的待追踪状态。 Compare() 方法让列表能按图表 ID 排序:当前对象 ChartID() 大于传入节点时返回 1,小于返回 -1,相等返回 0。这个三元比较是后续用 CList 管理多图表控件的前提。 OnChartEvent() 声明为接收 id、lparam、dparam、sparam 四个参数的事件处理函数,由系统回调驱动。外汇与贵金属图表事件密集,实盘挂这个类前先在策略测试器跑一遍,确认事件频率不会拖慢主图帧率。

MQL5 / C++
this.m_delta_graph_obj=class="num">0;
this.SetMouseEvent();
   }
   CChartObjectsControl(class="kw">const class="type">long chart_id)
   {
      this.m_list_new_graph_obj.Clear();
      this.m_list_new_graph_obj.Sort();
      this.m_chart_id=chart_id;
      this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id);
      this.m_chart_symbol=::ChartSymbol(this.m_chart_id);
      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.SetMouseEvent();
   }
class=class="str">"cmt">//--- Compare CChartObjectsControl objects by a chart ID(for sorting the list by an object class="kw">property)
   class="kw">virtual class="type">int      Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const
   {
      class="kw">const CChartObjectsControl *obj_compared=node;
      class="kw">return(this.ChartID()>obj_compared.ChartID() ? class="num">1 : this.ChartID()<obj_compared.ChartID() ? -class="num">1 : class="num">0);
   }
class=class="str">"cmt">//--- Event handler
   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);
   };

给图表挂上鼠标与物件事件监听

想在 MT5 上做交互式图形分析,第一步是让图表把鼠标移动、滚轮、物件创建与删除都报上来。下面这个函数就是干这事:对当前图表 ID 连续调四次 ChartSetInteger,把四类事件开关全置 true。 [CChartObjectsControl::SetMouseEvent 代码逐行拆解] void CChartObjectsControl::SetMouseEvent(void) // 定义类方法,无入参 ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_MOVE,true); // 开启鼠标移动事件 ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_WHEEL,true); // 开启滚轮缩放/平移事件 ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_CREATE,true);// 开启图形对象被创建的事件 ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_DELETE,true);// 开启图形对象被删除的事件 事件打开后,真正的容器类 CGraphElementsCollection 才派得上用场。它用三个列表分别装图表控制器、画布元素和图形对象,并维护 m_total_objects 与 m_delta_graph_obj 两个计数器来感知物件数量变化。 m_delta_graph_obj 是和上次检查相比的物件数差值,若不为 0,说明用户手动加了线或删了标注。外汇与贵金属波动剧烈,这类交互对象可能随价格跳空频繁增减,实盘使用前建议在策略测试器里先跑一轮事件回放验证。

MQL5 / C++
class=class="str">"cmt">//| Set the permission                                                                                    |
class=class="str">"cmt">//| to track mouse and graphical object events for the chart         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChartObjectsControl::SetMouseEvent(class="type">void)
  {
   ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
   ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_CREATE,true);
   ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_DELETE,true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Collection of graphical objects                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
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
   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_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 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 graphical element object presence in the collection list of graphical objects
   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">//--- 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">void);

◍ 图表对象集合的差集查找与按属性筛选

在 MT5 自定义指标里管理大量图形对象时,容易出现在集合里但图表上已删除、或图表上有但集合未记录的情况。类里提供了 FindMissingObj(long chart_id) 与 FindExtraObj(long chart_id) 两个接口,分别抓出「集合有、图表无」和「图表有、集合无」的对象指针或名称,方便做一致性回收。 对外暴露的 GetList 系列重载了整型、双精度、字符串三类画布元素属性(ENUM_CANV_ELEMENT_PROP_INTEGER / DOUBLE / STRING),调用时传入属性枚举、目标值与比较模式(默认 EQUAL),底层走 CSelect::ByGraphCanvElementProperty 按条件返回子列表。 标准图形对象同理,GetList(ENUM_GRAPH_OBJ_PROP_INTEGER, long, mode) 等重载直接对 m_list_all_graph_obj 做筛选。实盘加载前建议在策略测试器用 2023 年 EURUSD H1 跑一遍,确认集合与图表对象数量偏差为 0 再上真仓;外汇与贵金属杠杆交易高风险,对象管理异常可能导致信号绘制错位。

MQL5 / C++
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);
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 from the graphical object collection list
class="type">bool DeleteGraphObjFromList(CGStdGraphObj *obj);

class="kw">public:
class=class="str">"cmt">//--- Return itself
CGraphElementsCollection *GetObject(class="type">void) { class="kw">return &this; }
class=class="str">"cmt">//--- Return the full collection list of standard graphical objects "as is"
CArrayObj *GetListGraphObj(class="type">void) { class="kw">return &this.m_list_all_graph_obj; }
class=class="str">"cmt">//--- Return the full collection list of graphical elements on canvas "as is"
CArrayObj *GetListCanvElm(class="type">void) { class="kw">return &this.m_list_all_canv_elm_obj; }
class=class="str">"cmt">//--- Return the list of graphical elements by a selected(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting the compared criterion
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); }
class=class="str">"cmt">//--- Return the list of graphical objects by a selected(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting the compared criterion
CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,value,mode); }

「图形对象集合类的检索与刷新接口」

CGraphElementsCollection 把图表上的图形对象收进一个统一容器,并暴露了多组重载方法方便按属性筛出子集。GetList 针对双精度属性与字符串属性分别提供了重载,内部都转交 CSelect::ByGraphicStdObjectProperty 处理,比较模式默认 EQUAL,也可传其它 ENUM_COMPARER_TYPE 做不等或区间匹配。 NewObjects 直接返回成员变量 m_delta_graph_obj,给出自上次刷新以来新增图形对象的数量;IsEvent 则读 m_is_graph_obj_event 布尔标志,告诉你这段时间列表是否发生过变动。这两个数配合 Refresh 使用,就能在 EA 里只在新对象出现时才跑后续逻辑,省掉无谓遍历。 Refresh 有两个版本:无参刷新全图表对象,带 chart_id 的只刷指定图表,两者都会回填新增计数并置事件旗。OnChartEvent 接收 MT5 的图表事件 id、lparam、dparam、sparam,是图形对象增删改的入口钩子。 GetStdGraphObject 按 name 与 chart_id 精准取回某个 CGStdGraphObj 指针;Print 和 PrintShort 能把对象属性或简描打到日志,full_prop 开true会吐全部属性。CreateChartControlList 建好图表管理对象列表并返回图表数,这几样拼起来,基本覆盖了对象生命周期的观测面。

MQL5 / C++
CArrayObj      *GetList(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL)   { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,value,mode); }
CArrayObj      *GetList(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL)   { class="kw">return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),class="kw">property,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 a 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);
class=class="str">"cmt">//--- Constructor
                CGraphElementsCollection();
class=class="str">"cmt">//--- Display the description of the object properties in the journal(full_prop=true - all properties, class="kw">false - supported ones only - implemented in descendant classes)
   class="kw">virtual class="type">void    Print(class="kw">const class="type">bool full_prop=class="kw">false,class="kw">const class="type">bool dash=class="kw">false);
class=class="str">"cmt">//--- Display a class="type">class="kw">short description of the object in the journal
   class="kw">virtual class="type">void    PrintShort(class="kw">const class="type">bool dash=class="kw">false,class="kw">const class="type">bool symbol=class="kw">false);
class=class="str">"cmt">//--- Create the list of chart management objects and class="kw">return the number of charts
   class="type">int            CreateChartControlList(class="type">void);
class=class="str">"cmt">//--- Update the list of(class="num">1) all graphical objects, (class="num">2) on the specified chart, fill in the data on the number of new ones and set the event flag
   class="type">void           Refresh(class="type">void);
   class="type">void           Refresh(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Event handler
   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);

图表对象增删的实时同步机制

在 MT5 终端里同时开着的图表数量受 CHARTS_MAX 限制,这个宏默认上限是 100,所以遍历所有图表时循环条件直接写 i<CHARTS_MAX 就够用,不会无限跑。 Refresh 方法靠 ChartNext 按 chart_id 逐个拿到图表句柄,拿到负值就 break 退出。每拿到一个图表就调用 RefreshByChartID 取该图表的对象控制器指针,若返回 NULL 则 continue 跳过,避免空指针拖垮整个刷新流程。 真正有看头的是对象数量变动的分支:obj_ctrl.IsEvent() 为真才进处理。Delta() 大于 0 说明有图形对象被画上去,走 AddGraphObjToCollection 搬进集合;小于 0 则是被删了,这时用 FindMissingObj 按 chart_id 找出集合里多出来的那个对象,PrintShort 打一行简报进日志,再 DeleteGraphObjFromList 把它从集合摘掉,摘失败就写一条失败日志。 外汇与贵金属图表上手动拖画线很频繁,这套同步逻辑能让你在 EA 里实时感知用户删了哪条线,但终端多图表场景下对象事件可能丢,属于高风险环境下的概率性同步而非绝对保证。

MQL5 / C++
class="type">void CGraphElementsCollection::Refresh(class="type">void)
  {
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(!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=class="str">"cmt">// Find an extra object in the list
            CGStdGraphObj *obj=this.FindMissingObj(chart_id);
            if(obj!=NULL)
              {
               class=class="str">"cmt">//--- Display a class="type">class="kw">short description of a detected object deleted from a chart in the journal
               obj.PrintShort();
               class=class="str">"cmt">//--- Remove the class object of a removed graphical object from the collection list
               if(!this.DeleteGraphObjFromList(obj))
                  CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST);
              }
           }
         class=class="str">"cmt">//--- otherwise
         else
           {

           }
     }
  }

◍ 图表与对象池的差异比对逻辑

做图形对象同步时,核心矛盾是「内存里的集合」和「图表上真实存在的对象」可能脱节。下面两段查找函数就是用来定位这种脱节的:一段找集合里有、图表上没有的,一段找图表上有、集合里没有的。 FindMissingObj 先按 chart_id 从集合里筛出该图表的所有对象,再逐个用 IsPresentGraphObjOnChart 确认是否还在图表上,碰到第一个不在的就直接返回该对象指针。FindExtraObj 则反过来,用 ObjectsTotal 拿图表对象总数,遍历 ObjectName 拿到的名字,只要集合里查不到就返回这个名字。 IsPresentGraphObjInList 的写法有个坑:它用两次 ByGraphicStdObjectProperty 做筛选,若最终 list 为 NULL 或总数为 0 都判为 false。注意这里若第一次筛选返回 NULL,第二次直接对 NULL 调用会崩,实盘跑之前最好加一层空指针保护。外汇和贵金属图表对象频繁增删,这类同步函数在 EA 里误用可能引发闪退,属高风险操作。

MQL5 / C++
CGStdGraphObj *CGraphElementsCollection::FindMissingObj(class="kw">const class="type">long chart_id)
  {
   CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,chart_id,EQUAL);
   if(list==NULL)
      class="kw">return NULL;
   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()))
         class="kw">return obj;
     }
   class="kw">return NULL;
  }

class="type">class="kw">string CGraphElementsCollection::FindExtraObj(class="kw">const class="type">long chart_id)
  {
   class="type">int total=::ObjectsTotal(chart_id);
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class="type">class="kw">string name=::ObjectName(chart_id,i);
      if(!this.IsPresentGraphObjInList(chart_id,name))
         class="kw">return name;
     }
   class="kw">return NULL;
  }

class="type">bool CGraphElementsCollection::IsPresentGraphObjInList(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name)
  {
   CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,chart_id,EQUAL);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,name,EQUAL);
   class="kw">return(list==NULL || list.Total()==class="num">0 ? class="kw">false : true);
  }

「图表对象集合的增删查与拖拽响应」

管理 MT5 图表上的图形对象,核心是先确认对象是否存在再操作。下面这段集合类方法给出了三种高频动作:按名检索、从链表移除、按图表 ID 与名称取回对象指针。 IsPresentGraphObjOnChart 用 ObjectsTotal 拿到图表对象总数,再逐一遍览 ObjectName 做字符串比对,命中即返回 true。这种 O(n) 扫描在对象少于 50 个时开销可忽略,但欧元/黄金盘面若堆了上百画线,每次事件都全量扫可能拖慢响应。 DeleteGraphObjFromList 先对链表 Sort 再 Search 定位下标,搜不到返回 WRONG_VALUE 时直接 false,否则 Delete(index)。注意 Sort 前置让二分查找生效,否则 Search 退化为顺序遍历。 GetStdGraphObject 先用图表 ID 筛出子链表,再按名称做相等匹配,有结果取 At(0)。外汇与贵金属图表多品种同开时,chart_id 参数能避免跨图串对象,属于高风险环境下的防错设计。 OnChartEvent 只接 CHARTEVENT_OBJECT_CHANGE 与 CHARTEVENT_OBJECT_DRAG:用户拖动或改属性时,用 sparam 里的对象名从集合取回指针,调 PropertiesRefresh 和 PropertiesCheckChanged 同步状态。实盘里手动挪止损线,靠这套就能让 EA 内部模型实时感知新价位。

MQL5 / C++
class="type">bool CGraphElementsCollection::IsPresentGraphObjOnChart(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name)
  {
   class="type">int total=::ObjectsTotal(chart_id);
   for(class="type">int i=class="num">0;i<total;i++)
      if(::ObjectName(chart_id,i)==name)
         class="kw">return true;
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//+---------------------------------------------------------------------+
class=class="str">"cmt">//|Remove the graphical object from the graphical object collection list|
class=class="str">"cmt">//+---------------------------------------------------------------------+
class="type">bool CGraphElementsCollection::DeleteGraphObjFromList(CGStdGraphObj *obj)
  {
   this.m_list_all_graph_obj.Sort();
   class="type">int index=this.m_list_all_graph_obj.Search(obj);
   class="kw">return(index==WRONG_VALUE ? class="kw">false : this.m_list_all_graph_obj.Delete(index));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return a graphical object by chart name and ID                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::GetStdGraphObject(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id)
  {
   CArrayObj *list=this.GetList(GRAPH_OBJ_PROP_CHART_ID,chart_id);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,name,EQUAL);
   class="kw">return(list!=NULL && list.Total()>class="num">0 ? list.At(class="num">0) : NULL);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Event handler                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;
   if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG)
     {
      obj=this.GetStdGraphObject(sparam,::ChartID());
      if(obj!=NULL)
        {
         obj.PropertiesRefresh();
         obj.PropertiesCheckChanged();
        }

对象改名后的列表同步与暂停函数

当按名称找不到图形对象时,说明它已从集合列表里消失或被改了名。这时候要先在图表上找出缺失对应项的对象,再反查图表上多出来的那个对象名,把列表里的旧名替换掉。 下面这段逻辑里,FindMissingObj 用 ChartID() 定位列表中缺失图表的项,FindExtraObj 则拿到图表上不在列表里的对象名,随后通过 SetName、PropertiesRefresh、PropertiesCheckChanged 完成名称同步与属性刷新。 else { obj=this.FindMissingObj(::ChartID()); if(obj==NULL) return; string name_new=this.FindExtraObj(::ChartID()); obj.SetName(name_new); obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } Pause 函数接收毫秒级暂停时长与起始时间(默认 0),将等待毫秒数和时间起点写入内部状态后,用 while 轮询 PauseIsCompleted 并监听 IsStopped,实现阻塞式等待。注意 time_start*1000 把秒级 datetime 转成毫秒,若传入非 0 值需确认时区与精度预期。 GetGraphicObjCollection 直接返回 m_graph_objects 的指针,外部可据此遍历或调试当前引擎持有的全部图形对象集合。外汇与贵金属图表对象频繁重画,重命名同步逻辑若漏掉一次事件,列表与图表就可能长期不一致,这类 bug 在 EURUSD 的 M1 回测中较容易复现。

MQL5 / C++
else
  {
  obj=this.FindMissingObj(::ChartID());
  if(obj==NULL)
     class="kw">return;
  class="type">class="kw">string name_new=this.FindExtraObj(::ChartID());
  obj.SetName(name_new);
  obj.PropertiesRefresh();
  obj.PropertiesCheckChanged();
  }

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 graphical object collection
CGraphElementsCollection *GetGraphicObjCollection(class="type">void) { class="kw">return &this.m_graph_objects; }

class=class="str">"cmt">//--- Constructor/destructor
CEngine();
~CEngine();
class="kw">private:

◍ 把鼠标和对象事件接进EA日志

要验证图形对象集合类的事件响应,先拿前一篇的 EA 改一改。把它放到 \MQL5\Experts\TestDoEasy\Part86\ 目录下,存成 TestDoEasyPart86.mq5,编译后挂到图表即可跑。 关键改动在 OnInit() 里删掉原先设置鼠标事件权限的代码,改由图表管理类统一给所有已开图表开权限。OnChartEvent() 末尾再补一行图形对象集合类的事件处理调用,这样创建、删除对象或改属性时,终端日志会直接打印对应记录——目前只是日志,后面才会拿来做交互。 下面这段是改完后的 OnInit() 与 OnChartEvent() 头部,注意被标红的那两行权限设置已从初始化里拿掉,图表级跟踪改在别处统一开。 日志里能看到对象事件刷出来,说明事件链路通了。外汇和贵金属行情跳动快、点差跳空频繁,这类鼠标/对象跟踪逻辑在实盘可能拖慢 OnChartEvent 响应,上 MT5 先用模拟盘验证延迟再考虑加仓控。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Set the permissions to send cursor movement and mouse scroll events
   ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
class=class="str">"cmt">//--- Set EA global variables
   ArrayResize(array_clr,class="num">2);       class=class="str">"cmt">// Array of gradient filling colors
   array_clr[class="num">0]=C&class="macro">#x27;class="num">246,class="num">244,class="num">244&class="macro">#x27;;    class=class="str">"cmt">// Original ≈pale gray
   array_clr[class="num">1]=C&class="macro">#x27;class="num">249,class="num">251,class="num">250&class="macro">#x27;;    class=class="str">"cmt">// Final ≈pale gray-green
class=class="str">"cmt">//--- Create the array with the current symbol and set it to be used in the library
   class="type">class="kw">string array[class="num">1]={Symbol()};
   engine.SetUsedSymbols(array);
   class=class="str">"cmt">//--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// Short descriptions
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
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);

「鼠标悬停K线弹出信息框的实现细节」

在 MT5 自定义指标或 EA 里,想做「鼠标移到哪根 K 线就弹出迷你信息框」,核心是先拿鼠标坐标换时间价格。用 ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price) 把鼠标事件里的 lparam/dparam 转成 time 和 price,转换失败就直接 return,避免后续空指针。 拿到时间后调 iBarShift(Symbol(),PERIOD_CURRENT,time) 求光标所在 Bar 的索引;若返回 WRONG_VALUE 同样退出。接着用 engine.SeriesGetBar() 取 CBar 对象,再用 ChartTimePriceToXY 把这根 Bar 的 (Time, (Open+Close)/2) 反算回屏幕 x/y,作为信息框锚点。 为防误触,代码里顺手关掉 CHART_MOUSE_SCROLL 和 CHART_CONTEXT_MENU。信息框名按 "FormBar_"+index 拼,HideFormAllExceptOne 只留当前这一个;若 IsPresentForm 查无此框,就新建 114×16 像素的 form,设不可移动、激活、透明度 200,背景取颜色数组首项,边框写死 C'47,70,59'。外汇与贵金属波动剧烈,这类悬停框仅作辅助观察,实盘信号仍需独立验证。

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

             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">class="kw">color is set as the first class="type">class="kw">color from the class="type">class="kw">color array
             form.SetColorBackground(array_clr[class="num">0]);
             class=class="str">"cmt">//--- Form outlining frame class="type">class="kw">color
             form.SetColorFrame(C&class="macro">#x27;class="num">47,class="num">70,class="num">59&class="macro">#x27;);

给悬浮窗加上阴影与渐变底

在 MT5 自定义指标里画价格行为标注窗体时,阴影和底色不是装饰,而是辨识度的关键。下面这段直接操作窗体对象:先开阴影开关,再把图表背景色降饱和成单色,按设置决定用背景派生色还是自定义色。 代码里 form.DrawShadow(2,2,clr,200,3) 的 2 和 2 是右下偏移像素,200 是透明度(0–255 区间),3 是模糊半径,数值越大边缘越虚。改这几个数能立刻看出立体感变化。 底色走垂直渐变用 form.Erase(array_clr,form.Opacity()),边框再描一道矩形。若 list_forms.Add(form) 失败就 delete 掉并返回,避免野指针。最后 form.Done() 捕获外观、form.Show() 出窗体,调 ChartRedraw() 刷新。外汇与贵金属行情跳动快,这类 overlay 在高波动时可能掉帧,参数宜保守。

MQL5 / C++
class=class="str">"cmt">//--- Draw the shadow drawing flag
form.SetShadow(true);
class=class="str">"cmt">//--- Calculate the shadow class="type">class="kw">color as the chart background class="type">class="kw">color converted to the monochrome one
class="type">class="kw">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">class="kw">color, replace the monochrome class="type">class="kw">color with class="num">20 units
class=class="str">"cmt">//--- Otherwise, use the class="type">class="kw">color specified in the settings for drawing the shadow
class="type">class="kw">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">//--- Redraw the chart
 ChartRedraw();
   }
  }

 engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

}

◍ 记住这一条就够了

当前函数库版本已放出全部源文件与测试 EA,压缩包体积 4153.33 KB,可直接在 MT5 加载验证事件转发逻辑。下一篇计划把每个图表的对象事件收拢到控制图表统一处理,这意味着多图对象的增删改都能被集中捕获。 外汇与贵金属图表对象事件频繁,多图监听若写死在单 EA 里容易漏事件,用集合类转发能降维护成本,但终端多图表并发仍有延迟风险。 打开 MQL5.zip 里的测试 EA 跑一遍,看控制图表能否收到子图对象事件,比空读文档实在。

常见问题

写个循环遍历对象集合,逐个调用属性读取接口把整型字段存进数组或缓存,避免手动一个一个点开对象属性窗口。
因为你没做快照比对,只在初始化时抓过一次;应在属性变更事件里重新抓取并覆盖类内部缓存,才能拿到最新值。
可以,小布能接管对象事件控制接口,实时追踪删除与属性变更并同步到集合同步逻辑,你只看异常提醒就行。
整型如类型枚举、宽度,双精度如价格坐标、角度;建议分开接口抓取,类内部用两套缓存避免类型截断。
在删除追踪回调里打日志并触发集合同步清理,定期比对图表实际对象数与集合大小,不一致就强制重建索引。