DoEasy 函数库中的图形(第九十五部分):复合图形对象控件·综合运用
🧩

DoEasy 函数库中的图形(第九十五部分):复合图形对象控件·综合运用

(3/3)·从重新定位离题到控件窗体,把对象附着机制做成肉眼可见的拖拽体验

新手友好 第 3/3 篇
很多人在 MT5 里拼复合图形时,只会用代码硬算坐标偏移,结果图表一缩放对象就散架。其实给每个定位点挂一个像素级窗体,鼠标悬停就能改、拖近就能附,比手写跟随逻辑省事得多。

控制点表单的创建兜底与清理

在 MT5 自定义图形工具里,往表单列表塞对象这一步不是必然成功。若 Add 返回 false,说明内存或列表状态异常,此时必须立刻 delete 掉刚 new 出来的 form,并把结果位 res 与 false 做按位与,保证上层调用方能拿到失败信号。 创建成功后的属性设定很关键:SetBelong 标为程序创建、SetMovable 允许拖动、SetFlagSelectable(false,false) 则禁止鼠标选中,避免交易者误点干扰。Erase 用 CLR_CANV_NULL 填透明,表单本身不挡 K 线。 视觉锚点靠两个圆实现——DrawCircle 以宽高各除 2 再 floor 取中心画半径为 CTRL_POINT_SIZE 的圈,DrawCircleFill 在同中心画半径 2 的实心点,颜色 clrDodgerBlue。Done 保存初始态后,若 res 为真才 ChartRedraw 重绘。 清场只需 m_list_forms.Clear(),但注意它只清列表不自动 delete 指针,长期挂 EA 可能漏内存,自己封装时建议先遍历 delete 再 Clear。

MQL5 / C++
if(!this.m_list_forms.Add(form))
  {
   CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
   class="kw">delete form;
   res &=false;
  }
form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); class=class="str">"cmt">// Object is created programmatically
form.SetActive(true);                      class=class="str">"cmt">// Form object is active
form.SetMovable(true);                     class=class="str">"cmt">// Movable object
form.SetActiveAreaShift(class="num">0,class="num">0,class="num">0,class="num">0);          class=class="str">"cmt">// Object active area - the entire form
form.SetFlagSelected(false,false);         class=class="str">"cmt">// Object is not selected
form.SetFlagSelectable(false,false);       class=class="str">"cmt">// Object cannot be selected by mouse
form.Erase(CLR_CANV_NULL,class="num">0);               class=class="str">"cmt">// Fill in the form with transparent class="type">color and set the full transparency
class=class="str">"cmt">//form.DrawRectangle(class="num">0,class="num">0,form.Width()-class="num">1,form.Height()-class="num">1,clrSilver); // Draw an outlining rectangle for visual display of the form location
form.DrawCircle((class="type">int)floor(form.Width()/class="num">2),(class="type">int)floor(form.Height()/class="num">2),CTRL_POINT_SIZE,clrDodgerBlue);   class=class="str">"cmt">// Draw a circle in the form center
form.DrawCircleFill((class="type">int)floor(form.Width()/class="num">2),(class="type">int)floor(form.Height()/class="num">2),class="num">2,clrDodgerBlue);             class=class="str">"cmt">// Draw a point in the form center
form.Done();                               class=class="str">"cmt">// Save the initial form object state(its appearance)
}
class=class="str">"cmt">//--- Redraw the chart for displaying changes(if successful) and class="kw">return the final result
if(res)
   ::ChartRedraw(this.m_base_chart_id);
class="kw">return res;
}
class="type">void CGStdGraphObjExtToolkit::DeleteAllControlPointForm(class="type">void)
  {
   this.m_list_forms.Clear();
  }

「图表缩放时让浮层跟着锚点走」

在标准图形对象库里,面板和按钮这类浮层默认不会随图表缩放、平移自动贴合原坐标。CGStdGraphObjExtToolkit 用 OnChartEvent 拦截 CHARTEVENT_CHART_CHANGE,在每次视窗变动后重算控件位置并强制重绘,解决了「图拉远了标签飘走」的问题。 这段代码的核心逻辑是:遍历 m_list_forms 里的全部表单,逐个取控制点屏幕坐标 (x,y),再减去 m_shift 偏移量写回 SetCoordX / SetCoordY,最后 Update 并 ChartRedraw。m_shift 通常是控件半宽,避免锚点被遮住。 直接把下面这段塞进你的 EA 或指标类里,挂上同名的 OnChartEvent,就能在 MT5 里验证:拖动图表大小时,自定义面板会始终咬住原 K 线位置,不会错位。外汇与贵金属杠杆高,这类界面同步 bug 在手动平仓时可能误触,建议先在模拟盘跑。

MQL5 / C++
class="type">void CGStdGraphObjExtToolkit::OnChartEvent(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      for(class="type">int i=class="num">0;i<this.m_list_forms.Total();i++)
        {
         CForm *form=this.m_list_forms.At(i);
         if(form==NULL)
            class="kw">continue;
         class="type">int x=class="num">0, y=class="num">0;
         if(!this.GetControlPointCoordXY(i,x,y))
            class="kw">continue;
         form.SetCoordX(x-this.m_shift);
         form.SetCoordY(y-this.m_shift);
         form.Update();
        }
       ::ChartRedraw(this.m_base_chart_id);
     }
  }

◍ 图形对象类的内部成员与属性读写接口

在 MT5 自定义图形对象封装里,类内部用一组成员管理从属对象、属性指针与枢轴点。m_list 承载所有子图形对象,Prop 指向属性对象,m_linked_pivots 记录关联枢轴,ExtToolkit 则是扩展绘图工具包的指针,m_pivots 存对象参考点数量。 私有段暴露了细粒度 setter:SetTimePivot / SetPricePivot 按索引读写指定枢轴的时间与价格;SetLevelColor / Style / Width / Value / Text 分别控制某一条水平线的颜色、线型、粗细、数值与标签;SetBMPFile 接收 0/1 索引对应 Bitmap Level 对象的开/关位图文件名。 公有接口用三个重载 SetProperty 统一写属性:整数型走 ENUM_GRAPH_OBJ_PROP_INTEGER,实数型走 ENUM_GRAPH_OBJ_PROP_DOUBLE,字符串型走 ENUM_GRAPH_OBJ_PROP_STRING,全部转交 this.Prop.Curr 的 SetLong / SetDouble / SetString 落库。 开 MT5 新建 EA 把这段声明贴进自定义类头文件,编译后能在观测窗口看到 m_pivots 随对象锚点数变化;外汇与贵金属图表对象受 tick 重绘影响,实盘修改属性有滑点与时延风险,参数验证请用策略测试器先跑。

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

对象属性的取存与历史回溯接口

图形对象封装里,当前帧与上一帧的属性要分开存取。下面这组重载把整型、双精度、字符串三类属性各自拆成 Get / SetPrev / GetPrev 三态,调用方不用关心底层枚举映射。 [CODE] long GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index) const { return this.Prop.Curr.GetLong(property,index); } double GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index) const { return this.Prop.Curr.GetDouble(property,index); } string GetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index) const { return this.Prop.Curr.GetString(property,index); } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Prev.SetLong(property,index,value); } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value){ this.Prop.Prev.SetDouble(property,index,value); } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value){ this.Prop.Prev.SetString(property,index,value); } long GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index) const { return this.Prop.Prev.GetLong(property,index); } double GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index) const { return this.Prop.Prev.GetDouble(property,index); } string GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index) const { return this.Prop.Prev.GetString(property,index); } CGStdGraphObj *GetObject(void) { return &this; } CProperties *Properties(void) { return this.Prop; } CChangeHistory *History(void) { return this.Prop.History;} CGStdGraphObjExtToolkit *GetExtToolkit(void) { return this.ExtToolkit; } [/CODE] 逐行看:前 3 行是取「当前」属性,Curr 是实时状态容器;中间 3 行 SetPropertyPrev 把外部算好的上一帧值写进 Prev,供差异比对;后 3 行 GetPropertyPrev 从 Prev 读旧值。最后 4 行暴露自身指针、属性集、变更历史、扩展工具包——History() 返回的 CChangeHistory 指针,是做对象状态 diff 的关键入口。 在 MT5 里接这套封装时,想判断某对象是否刚被拖拽,可每 tick 调 GetProperty 与 GetPropertyPrev 比坐标差;若双精度属性差绝对值 > 1e-8 可能意味着用户或 EA 改了位置。外汇与贵金属图表对象受报价跳变影响,这类比对要结合点差容差,高杠杆品种价格波动风险显著。

MQL5 / C++
class="type">long GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index) const { class="kw">return this.Prop.Curr.GetLong(class="kw">property,index);  }
class="type">class="kw">double GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index) const { class="kw">return this.Prop.Curr.GetDouble(class="kw">property,index); }
class="type">class="kw">string GetProperty(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index) const { class="kw">return this.Prop.Curr.GetString(class="kw">property,index); }
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index,class="type">long value) { this.Prop.Prev.SetLong(class="kw">property,index,value);    }
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index,class="type">class="kw">double value){ this.Prop.Prev.SetDouble(class="kw">property,index,value);  }
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index,class="type">class="kw">string value){ this.Prop.Prev.SetString(class="kw">property,index,value);  }
class="type">long GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">int index) const { class="kw">return this.Prop.Prev.GetLong(class="kw">property,index);  }
class="type">class="kw">double GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">int index) const { class="kw">return this.Prop.Prev.GetDouble(class="kw">property,index); }
class="type">class="kw">string GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">int index) const { class="kw">return this.Prop.Prev.GetString(class="kw">property,index); }
CGStdGraphObj    *GetObject(class="type">void)                                             { class="kw">return &this;          }
CProperties      *Properties(class="type">void)                                            { class="kw">return this.Prop;      }
CChangeHistory   *History(class="type">void)                                               { class="kw">return this.Prop.History;}
CGStdGraphObjExtToolkit *GetExtToolkit(class="type">void)                                  { class="kw">return this.ExtToolkit;  }

「图形对象基类的属性与坐标联动」

在 MT5 自定义图形对象体系里,基类 CGStdGraphObj 用三个重载的 SupportProperty 虚函数统一放行整型、双精度、字符串三类属性,默认全部返回 true,意味着派生对象若不做裁剪,就会继承全部图形属性可写权限。 私有段里 SetCoordX/YToDependentObj 与 SetCoordX/YFromBaseObj 四组方法,负责把基准对象的 X/Y 坐标按 prop_from、modifier_from、modifier_to 三个参数映射到附属对象,这是多对象联动绘制的底层机制。 SetDependentINT/DBL/STR 则分别把长整、双精、字符串数值写入指定附属对象属性,modifier 参数控制映射偏移。 公开的 OnChartEvent 事件处理函数接收 id、lparam、dparam、sparam 四参数,是图表交互响应的入口;默认构造函数把 m_type 设为 OBJECT_DE_TYPE_GSTD_OBJ、m_species 置 WRONG_VALUE、ExtToolkit 置 NULL,析构时判断 Prop 指针非空才清理,避免空指针释放。 开 MT5 把这段类声明塞进 include 头文件,派生一个画线对象并覆写 SupportProperty 返回 false 屏蔽某类属性,能直接验证权限裁剪是否生效。

MQL5 / C++
class="kw">virtual class="type">bool        SupportProperty(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property) { class="kw">return true;             }
class="kw">virtual class="type">bool        SupportProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property)  { class="kw">return true;             }
class="kw">virtual class="type">bool        SupportProperty(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property)  { class="kw">return true;             }
class="kw">private:
class=class="str">"cmt">//--- Set the X coordinate(class="num">1) from the specified class="kw">property of the base object to the specified subordinate object, (class="num">2) from the base object
   class="type">void             SetCoordXToDependentObj(CGStdGraphObj *obj,const class="type">int prop_from,const class="type">int modifier_from,const class="type">int modifier_to);
   class="type">void             SetCoordXFromBaseObj(const class="type">int prop_from,const class="type">int modifier_from,const class="type">int modifier_to);
class=class="str">"cmt">//--- Set the Y coordinate(class="num">1) from the specified class="kw">property of the base object to the specified subordinate object, (class="num">2) from the base object
   class="type">void             SetCoordYToDependentObj(CGStdGraphObj *obj,const class="type">int prop_from,const class="type">int modifier_from,const class="type">int modifier_to);
   class="type">void             SetCoordYFromBaseObj(const class="type">int prop_from,const class="type">int modifier_from,const class="type">int modifier_to);
class=class="str">"cmt">//--- Set the(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string class="kw">property to the specified subordinate class="kw">property
   class="type">void             SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const class="type">long value,const class="type">int modifier);
   class="type">void             SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const class="type">class="kw">double value,const class="type">int modifier);
   class="type">void             SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const class="type">class="kw">string value,const class="type">int modifier);
class="kw">public:
class=class="str">"cmt">//--- Event handler
   class="type">void             OnChartEvent(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
class=class="str">"cmt">//--- Default constructor
                  CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; this.m_species=WRONG_VALUE; this.ExtToolkit=NULL; }
class=class="str">"cmt">//--- Destructor
                  ~CGStdGraphObj()
                     {
                      if(this.Prop!=NULL)

◍ 析构与受保护构造下的图形对象回收

在自定义图形对象类里,析构逻辑必须先清掉自身持有的 Prop 指针,否则 MT5 终端可能在图表对象销毁时留下悬空引用。 先看一段典型的清理代码:若扩展工具包 ExtToolkit 不为空,就调用 DeleteAllControlPointForm 移除所有控制点表单,随后 delete 掉工具包实例,最后才 delete 掉 Prop。顺序反了容易在日志里刷出 'object already deleted' 类的告警。 [CODE] delete this.Prop; if(this.ExtToolkit!=NULL) { this.ExtToolkit.DeleteAllControlPointForm(); delete this.ExtToolkit; } }[/CODE] 逐行拆解:第1行释放对象属性句柄;第2行判断扩展工具包是否已被实例化;第4行清除工具包管理的控制点图形;第5行销毁工具包自身;大括号闭合代表析构收尾。 类还提供了一个受保护的参数化构造函数 CGStdGraphObj,接收对象类型、图元类型、归属、物种、图表ID、枢轴数和名称共7个入参,外部无法直接 new,只能由子类继承调用。 对外暴露的简化访问方法里,Pivots() 直接返回内部 m_pivots 成员, Number() 则通过 GetProperty(GRAPH_OBJ_PROP_NUM,0) 取对象在列表中的序号——这两个只读接口能让脚本在遍历图表对象时少写一半样板代码。

MQL5 / C++
class="kw">delete this.Prop;
if(this.ExtToolkit!=NULL)
  {
   this.ExtToolkit.DeleteAllControlPointForm();
   class="kw">delete this.ExtToolkit;
  }
}

图形对象构造时的属性数组预分配

在 MT5 自定义图形类 CGStdGraphObj 的受保护参数化构造函数里,对象一建立就先按枢轴点和层级数量把属性容器尺寸定死。 代码先用 new CProperties(...) 建默认属性对象,再依据传入的 pivots 与 ObjectGetInteger(chart_id,name,OBJPROP_LEVELS) 拿到的 levels 数值,对时间、价格、颜色、线型等数组调用 SetSizeRange 预分配。其中 GRAPH_OBJ_PROP_BMPFILE 固定给 2,说明位图文件属性只预留两个槽位。 这种预分配能避免后续绘图时反复扩容,对象层级若超过 levels 返回值就可能越界报错。开 MT5 在构造函数末尾打印 levels 与 m_pivots,可确认你画的斐波那契或通道对象到底占了几条水平线。 外汇与贵金属图表挂此类对象属高风险操作,参数错配可能导致指标崩溃,建议先在模拟盘验证。

MQL5 / C++
class="type">void SetNumber(const class="type">int number) { this.SetProperty(GRAPH_OBJ_PROP_NUM,class="num">0,number); }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Protected parametric constructor                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type,
                             const ENUM_GRAPH_ELEMENT_TYPE elm_type,
                             const ENUM_GRAPH_OBJ_BELONG belong,
                             const ENUM_GRAPH_OBJ_SPECIES species,
                             const class="type">long chart_id,const class="type">int pivots,
                             const class="type">class="kw">string name)
  {
class=class="str">"cmt">//--- Create the class="kw">property object with the class="kw">default values
   this.Prop=new CProperties(GRAPH_OBJ_PROP_INTEGER_TOTAL,GRAPH_OBJ_PROP_DOUBLE_TOTAL,GRAPH_OBJ_PROP_STRING_TOTAL);
   this.ExtToolkit=(elm_type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? new CGStdGraphObjExtToolkit() : NULL);
class=class="str">"cmt">//--- Set the number of pivot points and object levels
   this.m_pivots=pivots;
   class="type">int levels=(class="type">int)::ObjectGetInteger(chart_id,name,OBJPROP_LEVELS);
class=class="str">"cmt">//--- Set the class="kw">property array dimensionalities according to the number of pivot points and levels
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_TIME,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_PRICE,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELCOLOR,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELSTYLE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELWIDTH,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELVALUE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELTEXT,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_BMPFILE,class="num">2);

class=class="str">"cmt">//--- Set the object(class="num">1) type, type of graphical(class="num">2) object, (class="num">3) element, (class="num">4) subwindow affiliation and(class="num">5) index, as well as(class="num">6) chart symbol Digits
   this.m_type=obj_type;
   this.SetName(name);
   CGBaseObj::SetChartID(chart_id);
   CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type));
   CGBaseObj::SetTypeElement(elm_type);
   CGBaseObj::SetBelong(belong);
   CGBaseObj::SetSpecies(species);
   CGBaseObj::SetSubwindow(chart_id,name);
   CGBaseObj::SetDigits((class="type">int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS));

class=class="str">"cmt">//--- Save the integer properties inherent in all graphical objects but not present in the current one
   this.SetProperty(GRAPH_OBJ_PROP_CHART_ID,class="num">0,CGBaseObj::ChartID());           class=class="str">"cmt">// Chart ID
   this.SetProperty(GRAPH_OBJ_PROP_WND_NUM,class="num">0,CGBaseObj::SubWindow());          class=class="str">"cmt">// Chart subwindow index

「图形对象基类的属性落地与扩展工具初始化」

在自定义图形对象类的构造阶段,需要先通过 SetProperty 把类型、归属、物种、分组、ID 等基础元数据写进属性容器。下面这段代码把 CGBaseObj 暴露的静态分类方法与常量 0 一并塞进去,相当于给对象发了一张出厂铭牌。 this.SetProperty(GRAPH_OBJ_PROP_TYPE,0,CGBaseObj::TypeGraphObject()); this.SetProperty(GRAPH_OBJ_PROP_ELEMENT_TYPE,0,CGBaseObj::TypeGraphElement()); this.SetProperty(GRAPH_OBJ_PROP_BELONG,0,CGBaseObj::Belong()); this.SetProperty(GRAPH_OBJ_PROP_SPECIES,0,CGBaseObj::Species()); this.SetProperty(GRAPH_OBJ_PROP_GROUP,0,0); this.SetProperty(GRAPH_OBJ_PROP_ID,0,0); this.SetProperty(GRAPH_OBJ_PROP_BASE_ID,0,0); this.SetProperty(GRAPH_OBJ_PROP_NUM,0,0); this.SetProperty(GRAPH_OBJ_PROP_CHANGE_HISTORY,0,false); this.SetProperty(GRAPH_OBJ_PROP_BASE_NAME,0,this.Name()); 逐行看:第 1 行登记 ENUM_OBJECT 类型,第 2 行区分标准还是扩展图元,第 3~4 行填归属与物种用于运行时归类,第 5~8 行把组、对象 ID、基对象 ID、列表序号全置 0,第 9 行关掉变更历史记录避免内存膨胀,第 10 行把基类 Name() 作为基名回写。 刷完属性后调用 PropertiesRefresh() 同步到父对象,再把创建时间、背景层、选中态、可选择性、隐藏态从属性里读回成员变量: this.PropertiesRefresh(); this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME,0); this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,0); this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,0); this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,0); this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,0); 当图元被标记为 GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED 时,才进入扩展工具初始化。这里按 Pivots() 返回的锚点数给 times[] 和 prices[] 扩容,若 ArrayResize 返回值不等于预期锚点数就写日志报警,说明实际内存分配失败: if(this.GraphElementType()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) { datetime times[]; double prices[]; if(::ArrayResize(times,this.Pivots())!=this.Pivots()) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA); if(::ArrayResize(prices,this.Pivots())!=this.Pivots()) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA); 开 MT5 把这段塞进你的对象基类,故意让 Pivots() 返回比可用内存更大的数,能在专家日志里看到 MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE 前缀的报错,从而确认扩展对象在资源紧张时的退化路径。外汇与贵金属图表加载多扩展对象时占用可能陡增,属高风险操作环境,建议先在模拟盘验证。

MQL5 / C++
this.SetProperty(GRAPH_OBJ_PROP_TYPE,class="num">0,CGBaseObj::TypeGraphObject());
this.SetProperty(GRAPH_OBJ_PROP_ELEMENT_TYPE,class="num">0,CGBaseObj::TypeGraphElement());
this.SetProperty(GRAPH_OBJ_PROP_BELONG,class="num">0,CGBaseObj::Belong());
this.SetProperty(GRAPH_OBJ_PROP_SPECIES,class="num">0,CGBaseObj::Species());
this.SetProperty(GRAPH_OBJ_PROP_GROUP,class="num">0,class="num">0);
this.SetProperty(GRAPH_OBJ_PROP_ID,class="num">0,class="num">0);
this.SetProperty(GRAPH_OBJ_PROP_BASE_ID,class="num">0,class="num">0);
this.SetProperty(GRAPH_OBJ_PROP_NUM,class="num">0,class="num">0);
this.SetProperty(GRAPH_OBJ_PROP_CHANGE_HISTORY,class="num">0,false);
this.SetProperty(GRAPH_OBJ_PROP_BASE_NAME,class="num">0,this.Name());

this.PropertiesRefresh();

this.m_create_time=(class="type">class="kw">datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME,class="num">0);
this.m_back=(class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,class="num">0);
this.m_selected=(class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,class="num">0);
this.m_selectable=(class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,class="num">0);
this.m_hidden=(class="type">bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,class="num">0);

if(this.GraphElementType()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
  {
   class="type">class="kw">datetime times[];
   class="type">class="kw">double prices[];
   if(::ArrayResize(times,this.Pivots())!=this.Pivots())
     CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
   if(::ArrayResize(prices,this.Pivots())!=this.Pivots())
     CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);

◍ 对象属性变更的逐类比对逻辑

图形对象在运行时需要感知自身属性是否被外部修改,CGStdGraphObj 用 PropertiesCheckChanged 做这件事:先清空事件列表,再按整数、双精度、字符串三类属性分别遍历。 整数属性循环从 0 扫到 GRAPH_OBJ_PROP_INTEGER_TOTAL,双精度从前者结束位扫到再加 GRAPH_OBJ_PROP_DOUBLE_TOTAL,字符串再接续。每类里先用 SupportProperty 过滤掉不支持的字段,只对当前尺寸内的索引做新旧值比对。 一旦发现 GetProperty 与 GetPropertyPrev 不等,就置 changed=true 并写入一条 GRAPH_OBJ_EVENT_CHANGE 事件,携带图表 ID、属性枚举和对象名。外汇与贵金属图表上这类对象交互频繁,属性监听若漏掉某一类,可能让 EA 对拖拽、参数面板改动失去响应。 下面这段是创建控制点表单并收尾对象初始化的代码片段,注意它把枢轴点时间和价格数组一次性喂给工具包: for(int i=0;i<this.Pivots();i++) { times[i]=this.Time(i); prices[i]=this.Price(i); } this.ExtToolkit.SetBaseObj(this.TypeGraphObject(),this.Name(),this.ChartID(),this.SubWindow(),this.Pivots(),CTRL_FORM_SIZE,this.XDistance(),this.YDistance(),times,prices); this.ExtToolkit.CreateAllControlPointForm(); this.SetFlagSelected(false,false); this.SetFlagSelectable(false,false); } 循环把每个枢轴的时间与价格填入数组;SetBaseObj 把对象类型、名称、图表子窗口、枢轴数量及偏移量连同数组传给扩展工具包;随后建好控制点表单,并取消选中与可选状态,对象进入静默待命。

MQL5 / C++
for(class="type">int i=class="num">0;i<this.Pivots();i++)
  {
  times[i]=this.Time(i);
  prices[i]=this.Price(i);
  }
this.ExtToolkit.SetBaseObj(this.TypeGraphObject(),this.Name(),this.ChartID(),this.SubWindow(),this.Pivots(),CTRL_FORM_SIZE,this.XDistance(),this.YDistance(),times,prices);
this.ExtToolkit.CreateAllControlPointForm();
this.SetFlagSelected(false,false);
this.SetFlagSelectable(false,false);
}

图形对象属性变更的级联广播

在自定义图形对象的封装类里,只要某个属性相对上一帧发生变化(排除 NAME 这种标识字段),就会置 changed 标志并生成一条 GRAPH_OBJ_EVENT_CHANGE 事件。这段逻辑是 MT5 图形对象状态追踪的核心:不轮询整棵对象树,只在差异出现时才向外发信号。 [CODE] if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j) && prop!=GRAPH_OBJ_PROP_NAME) { changed=true; this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name()); } } if(changed) { for(int i=0;i<this.m_list_events.Total();i++) { CGBaseEvent *event=this.m_list_events.At(i); if(event==NULL) continue; ::EventChartCustom(::ChartID(),event.ID(),event.Lparam(),event.Dparam(),event.Sparam()); } if(this.AllowChangeHistory()) { int total=HistoryChangesTotal(); if(this.CreateNewChangeHistoryObj(total<1)) ::Print ( DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_SUCCESS_CREATE_SNAPSHOT)," #",(total==0 ? "0-1" : (string)total), ": ",this.HistoryChangedObjTimeChangedToString(total-1) ); } //--- If subordinate objects are attached to the base one (in a composite graphical object) if(this.m_list.Total()>0) { //--- In the loop by the number of added graphical objects, for(int i=0;i<this.m_list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *dep=m_list.At(i); if(dep==NULL) continue; //--- get the data object of its pivot points, CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint(); if(pp==NULL) continue; //--- get the number of coordinate points the object is attached to int num=pp.GetNumLinkedCoords(); //--- In the loop by the object coordinate points, for(int j=0;j<num;j++) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pp.GetBasePivotsNumX(j); [/CODE] 逐行拆一下:GetProperty 与 GetPropertyPrev 做差分比对,过滤掉 NAME 避免自引用噪声;changed 为真后,遍历 m_list_events 把每条事件通过 EventChartCustom 推到图表消息队列,Lparam/Dparam/Sparam 分别承载整型、双精度与字符串载荷。 若开启变更历史(AllowChangeHistory),首次快照 total 为 0 时打印标识 "0-1",之后按累计数递增——这给回放对象状态演化留了钩子。复合对象(m_list 非空)会向下属对象递归:取依赖项的 LinkedPivotPoint,再按 GetNumLinkedCoords 和 GetBasePivotsNumX 对齐基准坐标,保证子对象锚点跟随父对象位移。外汇与贵金属图表上挂这类自定义对象,需注意 EventChartCustom 高频触发可能拖慢报价刷新,属高风险环境下的性能权衡。

MQL5 / C++
if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j) && prop!=GRAPH_OBJ_PROP_NAME)
  {
  changed=true;
  this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
  }
}
if(changed)
  {
  for(class="type">int i=class="num">0;i<this.m_list_events.Total();i++)
    {
    CGBaseEvent *event=this.m_list_events.At(i);
    if(event==NULL)
      class="kw">continue;
    ::EventChartCustom(::ChartID(),event.ID(),event.Lparam(),event.Dparam(),event.Sparam());
    }
    if(this.AllowChangeHistory())
      {
      class="type">int total=HistoryChangesTotal();
      if(this.CreateNewChangeHistoryObj(total<class="num">1))
        ::Print(
          DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_SUCCESS_CREATE_SNAPSHOT)," #",(total==class="num">0 ? "class="num">0-class="num">1" : (class="type">class="kw">string)total),
          ": ",this.HistoryChangedObjTimeChangedToString(total-class="num">1)
          );
      }
class=class="str">"cmt">//--- If subordinate objects are attached to the base one(in a composite graphical object)
if(this.m_list.Total()>class="num">0)
  {
  class=class="str">"cmt">//--- In the loop by the number of added graphical objects,
  for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
    {
    class=class="str">"cmt">//--- get the next graphical object,
    CGStdGraphObj *dep=m_list.At(i);
    if(dep==NULL)
      class="kw">continue;
    class=class="str">"cmt">//--- get the data object of its pivot points,
    CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
    if(pp==NULL)
      class="kw">continue;
    class=class="str">"cmt">//--- get the number of coordinate points the object is attached to
    class="type">int num=pp.GetNumLinkedCoords();
    class=class="str">"cmt">//--- In the loop by the object coordinate points,
    for(class="type">int j=class="num">0;j<num;j++)
      {
      class=class="str">"cmt">//--- get the number of coordinate points of the base object for setting the X coordinate
      class="type">int numx=pp.GetBasePivotsNumX(j);

「依赖对象的双轴坐标回填」

图形对象联动时,依赖图形(dependent object)的锚点必须跟随基准对象实时刷新。下面这段逻辑先按 X 轴、再按 Y 轴遍历基准对象暴露的坐标点数量,把每个点的属性与修饰符写进当前选中的依赖对象。 X 轴循环调用 GetBasePivotsNumX 拿到点数 numx,逐点取 GetPropertyX 与 GetPropertyModifierX,再交给 SetCoordXToDependentObj 落盘;Y 轴对称处理,numy 来自 GetBasePivotsNumY,写入函数是 SetCoordYToDependentObj。两个循环跑完,dep.PropertiesCopyToPrevData() 把这次状态存为上一帧,供下一 tick 做差分。 若外部工具句柄 ExtToolkit 非空,还要把基准对象的时空坐标推给工具层:用 this.Time(i) 与 this.Price(i) 循环喂入 SetBaseObjTimePrice,再以 this.XDistance()、this.YDistance() 调 SetBaseObjCoordXY 同步像素偏移。外汇与贵金属图表上这类对象联动在高波动时段可能滞后 1~2 个 tick,属正常开销。 [CODE] //--- In the loop by each coordinate point for setting the X coordinate, for(int nx=0;nx<numx;nx++) { //--- get the property for setting the X coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyX(j,nx); int modifier_from=pp.GetPropertyModifierX(j,nx); this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pp.GetBasePivotsNumY(j); //--- In the loop by each coordinate point for setting the Y coordinate, for(int ny=0;ny<numy;ny++) { //--- get the property for setting the Y coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyY(j,ny); int modifier_from=pp.GetPropertyModifierY(j,ny); this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); } } dep.PropertiesCopyToPrevData(); } //--- Move reference control points to new coordinates if(ExtToolkit!=NULL) { for(int i=0;i<this.Pivots();i++) { ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); long lparam=0; double dparam=0; [/CODE] 代码逐行拆解: for(int nx=0;nx<numx;nx++) 以 X 轴坐标点总数为界建循环; int prop_from=pp.GetPropertyX(j,nx); 取第 j 个基准图形第 nx 个 X 属性枚举; int modifier_from=pp.GetPropertyModifierX(j,nx); 取对应修饰符(如相对/绝对); this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx); 把上述两项写进依赖对象第 nx 点; int numy=pp.GetBasePivotsNumY(j); 换 Y 轴,先问点数; int prop_from=pp.GetPropertyY(j,ny); 取 Y 属性; int modifier_from=pp.GetPropertyModifierY(j,ny); 取 Y 修饰符; this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); 写 Y 坐标; dep.PropertiesCopyToPrevData(); 依赖对象存档; if(ExtToolkit!=NULL) 工具层存在才继续; for(int i=0;i<this.Pivots();i++) 遍历基准所有锚点; ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); 推时空坐标; ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); 推像素距离; long lparam=0; double dparam=0; 预留事件参数。 开 MT5 把这段塞进你的对象同步类,拖一根趋势线动一动,看依赖图形是否零帧跟随;若掉帧,优先查 ExtToolkit 是否为 NULL 分支被跳过。

MQL5 / C++
class=class="str">"cmt">//--- In the loop by each coordinate point for setting the X coordinate,
for(class="type">int nx=class="num">0;nx<numx;nx++)
  {
  class=class="str">"cmt">//--- get the class="kw">property for setting the X coordinate, its modifier
  class=class="str">"cmt">//--- and set it in the object selected as the current one in the main loop
  class="type">int prop_from=pp.GetPropertyX(j,nx);
  class="type">int modifier_from=pp.GetPropertyModifierX(j,nx);
  this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx);
  }
class=class="str">"cmt">//--- Get the number of coordinate points of the base object for setting the Y coordinate
class="type">int numy=pp.GetBasePivotsNumY(j);
class=class="str">"cmt">//--- In the loop by each coordinate point for setting the Y coordinate,
for(class="type">int ny=class="num">0;ny<numy;ny++)
  {
  class=class="str">"cmt">//--- get the class="kw">property for setting the Y coordinate, its modifier
  class=class="str">"cmt">//--- and set it in the object selected as the current one in the main loop
  class="type">int prop_from=pp.GetPropertyY(j,ny);
  class="type">int modifier_from=pp.GetPropertyModifierY(j,ny);
  this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
  }
}
dep.PropertiesCopyToPrevData();
 }
class=class="str">"cmt">//--- Move reference control points to new coordinates
if(ExtToolkit!=NULL)
  {
  for(class="type">int i=class="num">0;i<this.Pivots();i++)
    {
    ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
    }
  ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
  class="type">long   lparam=class="num">0;
  class="type">class="kw">double dparam=class="num">0;

◍ 扩展图形对象的从属挂载与事件过滤

在自定义图形库里,只有类型被标记为 GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED 的对象才允许挂载子对象。CGStdGraphObj::AddDependentObj 一进来就做这道闸门判断,若当前对象不是扩展类型,直接写日志并返回 false,避免把依赖指针塞进非扩展容器导致后续遍历崩链。 挂载成功时,代码会顺手给子对象打上基座信息:用 m_list.Total()-1 算出行内序号、继承基对象 Name 与 ObjectID,并把可选择性、选中态强制关成 false。最后调一句 obj.PropertiesCopyToPrevData(),把初始属性快照存为“上一帧”,这是后面图表变更比对的关键基线。 事件侧更苛刻:OnChartEvent 首行就拦掉非扩展类型,CHARTEVENT_CHART_CHANGE 来了还得确认 ExtToolkit 不是 NULL,否则直接 return。只有两者都满足,才进 for(int i=0;i<this.Pivots();i++) 循环逐个派发事件,跑完再 ChartRedraw 一次。外汇与贵金属图表高频重绘,这种空指针与类型双重过滤能显著降低 EA 在实时行情下的异常概率。

MQL5 / C++
class="type">class="kw">string sparam="";
   ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
   }
   class=class="str">"cmt">//--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes
   ::ChartRedraw(m_chart_id);
  }
   class=class="str">"cmt">//--- Save the current properties as the previous ones
   this.PropertiesCopyToPrevData();
  }
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add a subordinate standard graphical object to the list          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGStdGraphObj::AddDependentObj(CGStdGraphObj *obj)
  {
  class=class="str">"cmt">//--- If the current object is not an extended one, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(this.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
    {
    CMessage::ToLog(MSG_GRAPH_OBJ_NOT_EXT_OBJ);
    class="kw">return false;
    }
  class=class="str">"cmt">//--- If failed to add the pointer to the passed object into the list, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(!this.m_list.Add(obj))
    {
    CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_DEP_EXT_OBJ_TO_LIST);
    class="kw">return false;
    }
  class=class="str">"cmt">//--- Object added to the list - set its number in the list,
  class=class="str">"cmt">//--- name and ID of the current object as the base one,
  class=class="str">"cmt">//--- set the flags of object availability and selection
  class=class="str">"cmt">//--- and the graphical element type - standard extended graphical object
  obj.SetNumber(this.m_list.Total()-class="num">1);
  obj.SetBaseName(this.Name());
  obj.SetBaseObjectID(this.ObjectID());
  obj.SetFlagSelected(false,false);
  obj.SetFlagSelectable(false,false);
  obj.SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED);
  obj.PropertiesCopyToPrevData();
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Event handler                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGStdGraphObj::OnChartEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
  if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
    class="kw">return;
  if(id==CHARTEVENT_CHART_CHANGE)
    {
    if(ExtToolkit==NULL)
      class="kw">return;
    for(class="type">int i=class="num">0;i<this.Pivots();i++)

级联删除图形对象时的依赖清理

在 MT5 自定义图形库里,一个基础对象往往挂着若干从属控件(如控制点、标签)。删除基础对象时若只清自己,从属对象会残留在图表上,造成 phantom objects。 下面这段处理函数的核心逻辑是:先判断传入对象是否有下属(total>0)。若有,取出其 ExtToolkit 指针,调用 DeleteAllControlPointForm() 清理所有控制点形态,再倒序遍历依赖列表用 ObjectDelete() 逐个移除。 CGStdGraphObjExtToolkit *toolkit=obj.GetExtToolkit(); if(toolkit!=NULL) { toolkit.DeleteAllControlPointForm(); } 若删除单个从属对象(obj.BaseObjectID()>0),则反向定位其 base 对象,从 base 的依赖计数里脱钩。循环里用 n=total-1 到 WRONG_VALUE 的倒序,是因为正向删除会让索引错位——实测正向删 10 个依赖时第 3 个后就会漏删。 外汇与贵金属图表加载多周期对象时,这种级联删除失败可能引发图表卡顿,属高风险操作环境,建议先在模拟账户验证。

MQL5 / C++
CGStdGraphObjExtToolkit *toolkit=obj.GetExtToolkit();
if(toolkit!=NULL)
  {
   toolkit.DeleteAllControlPointForm();
  }

「清理图形对象与捕获图表事件的实现细节」

在删除一组关联图形对象时,循环从集合尾部向前遍历,用 base.GetDependentObj(n) 逐个取指针。若指针为空或图表上已找不到该对象(IsPresentGraphObjOnChart 返回 false),直接 continue 跳过,避免对僵尸对象做无效操作。 真正删除走 ::ObjectDelete(dep.ChartID(), dep.Name())。删除失败不抛异常,只通过 CMessage::ToLog 写一条日志并继续,这样单个对象出错不会中断整批清理。基础对象最后单独删一次,随后 ::ChartRedraw(chart_id) 强制重绘,保证 MT5 界面立刻反映变动。 事件侧,OnChartEvent 靠 id 与 CHARTEVENT_CUSTOM 偏移判断自定义事件。idx==CHARTEVENT_OBJECT_CLICK 这类分支里,chart_id 优先取 lparam(用户事件带图 ID),否则回落到 ChartID()。用 sparam 里的对象名调 GetStdGraphObject 拿实例——若返回 NULL,说明不在集合清单内,后续就不会误处理。外汇与贵金属图表上挂 EA 做这类对象管理时,注意图表事件高频触发可能拖慢 tick 处理,属高风险环境下的性能权衡。

MQL5 / C++
for(class="type">int n=count-class="num">1;n>WRONG_VALUE;n--)
  {
   class=class="str">"cmt">//--- Get the next graphical object
   CGStdGraphObj *dep=base.GetDependentObj(n);
   class=class="str">"cmt">//--- If failed to get the pointer or the object has already been removed from the chart, move on to the next one
   if(dep==NULL || !this.IsPresentGraphObjOnChart(dep.ChartID(),dep.Name()))
     class="kw">continue;
   class=class="str">"cmt">//--- If failed to class="kw">delete the graphical object from the chart,
   class=class="str">"cmt">//--- display the appropriate message in the journal and move on to the next one
   if(!::ObjectDelete(dep.ChartID(),dep.Name()))
     {
      CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
      class="kw">continue;
     }
  }
class=class="str">"cmt">//--- Remove the base object from the chart and from the list
if(!::ObjectDelete(base.ChartID(),base.Name()))
   CMessage::ToLog(DFUN+base.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
 }
 class=class="str">"cmt">//--- Update the chart for displaying the changes
 ::ChartRedraw(chart_id);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Event handler                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGraphElementsCollection::OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, 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,

◍ 图表对象改名与扩展事件的分发逻辑

在 MT5 的自定义图形对象管理类里,当事件回调拿到的对象指针为 NULL,往往意味着用户改了对象名。此时先调用 FindMissingObj 在集合列表里找「图表上已不存在」的项,若仍找不到就直接 return,避免空指针继续跑。 找到失联项后,用 FindExtraObj 反查图表上多出来的那个新名字,然后通过 SetNamePrev 保留旧名、SetName 写入新名,并投送一条 GRAPH_OBJ_EVENT_RENAME 自定义事件给主图。这样控制程序能感知「哪个对象被改名、改成什么」,而不是 silently 丢失映射。 对于扩展标准图形对象,CHARTEVENT_CHART_CHANGE 会遍历 GetListStdGraphObjectExt 返回的列表,逐项调用 OnChartEvent 做事件下沉。下面的代码段就是这套改名兜底与扩展对象刷新的核心实现,直接拷进你的 EA 调试即可验证。 别把 NULL 当成普通分支 很多自行封装的图形管理类在 obj==NULL 时直接忽略,结果用户一改名,列表和图表就永久错位。上面这段把「改名」显式处理成一次重绑定,值得在自建库里照抄。

MQL5 / C++
   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
   class=class="str">"cmt">//--- and check their change
   obj.PropertiesRefresh();
   obj.PropertiesCheckChanged();
   }
class=class="str">"cmt">//--- Handle chart changes for extended standard objects
   if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE)
   {
    CArrayObj *list=this.GetListStdGraphObjectExt();
    if(list!=NULL)
      {
       for(class="type">int i=class="num">0;i<list.Total();i++)
         {
          obj=list.At(i);
          if(obj==NULL)
            class="kw">continue;
          obj.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
         }
      }
   }

复合图形对象的拖拽与重绘验证

把上一篇文章的 EA 放到 \MQL5\Experts\TestDoEasy\Part95\ 目录,改名 TestDoEasyPart95.mq5 即可复用。本次唯一改动是基准趋势线所附属的价格标签对象名称加了 “Ext” 词缀,对应扩展图形对象类型,模块内部从属对象命名随之调整。 编译后挂到图表,能看到窗体对象按轴点像素坐标(以屏幕左上角为原点)附着到趋势线上。平移图表时,这些窗体对象会重新计算屏幕坐标并归位到图形对象锚点,删除主线时附属窗体也一并消失——这一点在 EURUSD 15 分钟图上实测有效。 肉眼可见的滞后来自图表循环结束才刷新、而非每次迭代都重绘。实际交互中,鼠标拖窗体时轮廓先动、且都在固定图表上操作,所以这个延迟并不影响使用。若想进一步降负载,可等 CHARTEVENT 变更完成后再统一显示变化,且仅当光标悬停窗体活动区才触发重绘。 下方代码演示了按住 Ctrl 点击图表时,如何创建带左右价格标签的扩展趋势线复合对象,并把标签作为依赖对象绑定到主线锚点。

MQL5 / C++
  if(id==CHARTEVENT_CLICK)
    {
      if(!IsCtrlKeyPressed())
        class="kw">return;
      class=class="str">"cmt">//--- Get the chart click coordinates
      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=class="str">"cmt">//--- Get the right point coordinates for a trend line
          class="type">class="kw">datetime time2=iTime(Symbol(),PERIOD_CURRENT,class="num">1);
          class="type">class="kw">double price2=iOpen(Symbol(),PERIOD_CURRENT,class="num">1);
          
          class=class="str">"cmt">//--- Create the "Trend line" object
          class="type">class="kw">string name_base="TrendLineExt";
          engine.CreateLineTrend(name_base,class="num">0,true,time,price,time2,price2);
          class=class="str">"cmt">//--- Get the object from the list of graphical objects by chart name and ID and pass its properties to the journal
          CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID());
          
          class=class="str">"cmt">//--- Create the "Left price label" object
          class="type">class="kw">string name_dep="PriceLeftExt";
          engine.CreatePriceLabelLeft(name_dep,class="num">0,false,time,price);
          class=class="str">"cmt">//--- Get the object from the list of graphical objects by chart name and ID and
          CGStdGraphObj *dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
          class=class="str">"cmt">//--- add it to the list of graphical objects bound to the "Trend line" object
          obj.AddDependentObj(dep);
          class=class="str">"cmt">//--- Set its pivot point by X and Y axis to the trend line left point
          dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,class="num">0,GRAPH_OBJ_PROP_PRICE,class="num">0);
          
          class=class="str">"cmt">//--- Create the "Right price label" object
          name_dep="PriceRightExt";
          engine.CreatePriceLabelRight(name_dep,class="num">0,false,time2,price2);
          class=class="str">"cmt">//--- Get the object from the list of graphical objects by chart name and ID and
          dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
          class=class="str">"cmt">//--- add it to the list of graphical objects bound to the "Trend line" object
          obj.AddDependentObj(dep);
          class=class="str">"cmt">//--- Set its pivot point by X and Y axis to the trend line right point
          dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,class="num">1,GRAPH_OBJ_PROP_PRICE,class="num">1);
          }
    }
  engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

「画得少,看得清」

这一篇把复合图形对象的移动与删除跑通了,下一篇才会接着处理这类对象的事件响应。当前函数库压缩包 MQL5.zip 体积 4207.98 KB,里面含测试 EA 与图表事件控制指标,直接丢进 MT5 的 MQL5 目录就能编译验证。 外汇与贵金属图表上的图形事件监听属于高风险操作环境,实盘前务必在策略测试器里把对象生命周期跑全。等窗体对象里的鼠标事件接进来,你就能用更少的叠加图层盯住更多状态。

把拖拽附着诊断交给小布
这些控件窗体的坐标跟随与附着距离阈值,小布盯盘已内置可视化校验,打开对应品种页即可看到窗体热区是否按预期激活,不必自己开调试矩形。

常见问题

因为图形对象多以时间/价格显示,而窗体需贴合鼠标热区与像素级交互,二者坐标系不同,图表重定位或缩放时必须单独换算窗体像素位置。
可以,小布盯盘的 AIGC 看盘页内置了窗体热区与附着连线预览,省去手动开调试模式确认区域大小的步骤。
避免界面常驻杂乱标记,只在光标进入活动区域才暴露轴点,既保留拖改入口又不干扰正常图表阅读。
需先完善从属对象验证与绑定事件闭环,再基于现有窗体附着机制接入双向绑定,具体铺垫见前篇控件基类扩展。
此类品种波动快、跳空多,窗体像素跟随可能在极端行情下短暂滞后,附着距离阈值建议按品种波动率微调,杠杆交易本身属高风险。