DoEasy 函数库中的图形(第九十五部分):复合图形对象控件·综合运用
(3/3)·从重新定位离题到控件窗体,把对象附着机制做成肉眼可见的拖拽体验
控制点表单的创建兜底与清理
在 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。
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 在手动平仓时可能误触,建议先在模拟盘跑。
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 重绘影响,实盘修改属性有滑点与时延风险,参数验证请用策略测试器先跑。
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 改了位置。外汇与贵金属图表对象受报价跳变影响,这类比对要结合点差容差,高杠杆品种价格波动风险显著。
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 屏蔽某类属性,能直接验证权限裁剪是否生效。
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) 取对象在列表中的序号——这两个只读接口能让脚本在遍历图表对象时少写一半样板代码。
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,可确认你画的斐波那契或通道对象到底占了几条水平线。 外汇与贵金属图表挂此类对象属高风险操作,参数错配可能导致指标崩溃,建议先在模拟盘验证。
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 前缀的报错,从而确认扩展对象在资源紧张时的退化路径。外汇与贵金属图表加载多扩展对象时占用可能陡增,属高风险操作环境,建议先在模拟盘验证。
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 把对象类型、名称、图表子窗口、枢轴数量及偏移量连同数组传给扩展工具包;随后建好控制点表单,并取消选中与可选状态,对象进入静默待命。
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 高频触发可能拖慢报价刷新,属高风险环境下的性能权衡。
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 分支被跳过。
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 在实时行情下的异常概率。
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 个后就会漏删。 外汇与贵金属图表加载多周期对象时,这种级联删除失败可能引发图表卡顿,属高风险操作环境,建议先在模拟账户验证。
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 处理,属高风险环境下的性能权衡。
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 时直接忽略,结果用户一改名,列表和图表就永久错位。上面这段把「改名」显式处理成一次重绑定,值得在自建库里照抄。
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 点击图表时,如何创建带左右价格标签的扩展趋势线复合对象,并把标签作为依赖对象绑定到主线锚点。
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 目录就能编译验证。 外汇与贵金属图表上的图形事件监听属于高风险操作环境,实盘前务必在策略测试器里把对象生命周期跑全。等窗体对象里的鼠标事件接进来,你就能用更少的叠加图层盯住更多状态。