DoEasy 函数库中的图形(第九十一部分):标准图形对象事件。 对象名称更改历史记录·综合运用
(3/3)·还在日志里盲猜哪个画线被改名?本篇把对象重命名轨迹直接存进属性数组
「图形对象集合类的构造与尺寸读取」
在 MT5 自定义图形库里,CGraphElementsCollection 负责统管图表上所有图形对象。它的构造函数不只设类型标识,还顺手打开了鼠标移动与滚轮事件监听,方便后续交互捕捉。 读取某个对象当前尺寸时,下面这个函数先按名和图表 ID 拿到标准图形对象指针,空指针就返回 0,否则取对应属性尺寸。 需要注意,Refresh 分两种重载:无参刷新全部图表对象,带 chart_id 的只刷指定图表,并会统计新增数量、置事件旗。外汇与贵金属图表对象密集时,频繁 Refresh 可能拖慢主线程,建议按图表粒度调用。 构造函数里还把总对象链表按画布元素 ID 排序并清空,控制图表链表也清空待填,这意味着每次新建集合都是从干净状态起步。
class="type">int GetSizeProperty(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id,class="kw">const class="type">int prop) { CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id); class="kw">return(obj!=NULL ? obj.Properties().CurrSize(prop) : class="num">0); }
图表对象集合的刷新与增删追踪
图形对象集合类在 Refresh 里先调用 RefreshForExtraObjects 处理额外对象,随后用 ChartNext 遍历终端内所有已开图表,MT5 常量 CHARTS_MAX 上限为 100,因此循环最多跑 100 次,避免无边界扫描拖慢 EA。 每次拿到 chart_id 后,RefreshByChartID 返回 CChartObjectsControl 指针并就地更新该图表的对象清单;若返回 NULL 直接 continue 跳到下一张图,不阻塞后续刷新。 真正的变化判定靠 obj_ctrl.IsEvent():当 Delta() 大于 0 说明有对象被画上图表,走 AddGraphObjToCollection 搬进集合;小于 0 则说明被删,按 -Delta() 的次数循环,用 FindMissingObj 在旧列表里定位消失的对象并提取其 ChartID 等参数。 删除分支里 m_list_deleted_obj 先 Clear 再 Sort,意味着每次刷新前清空已删记录、重新排序待处理列表;这套机制让集合在任何图表对象变动后都能在下次 tick 前保持一致状态,外汇与贵金属图表多对象高频重绘时尤其容易触发,属于高波动环境下的常见技术风险点。
this.m_total_objects=class="num">0; this.m_is_graph_obj_event=class="kw">false; this.m_list_deleted_obj.Clear(); this.m_list_deleted_obj.Sort(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update the list of all graphical objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGraphElementsCollection::Refresh(class="type">void) { this.RefreshForExtraObjects(); class=class="str">"cmt">//--- Declare variables to search for charts class="type">long chart_id=class="num">0; class="type">int i=class="num">0; class=class="str">"cmt">//--- In the loop by all open charts in the terminal(no more than class="num">100) class="kw">while(i<CHARTS_MAX) { class=class="str">"cmt">//--- Get the chart ID chart_id=::ChartNext(chart_id); if(chart_id<class="num">0) class="kw">break; class=class="str">"cmt">//--- Get the pointer to the object for managing graphical objects class=class="str">"cmt">//--- and update the list of graphical objects by chart ID CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id); class=class="str">"cmt">//--- If failed to get the pointer, move on to the next chart if(obj_ctrl==NULL) class="kw">continue; class=class="str">"cmt">//--- If the number of objects on the chart changes if(obj_ctrl.IsEvent()) { class=class="str">"cmt">//--- If a graphical object is added to the chart if(obj_ctrl.Delta()>class="num">0) { class=class="str">"cmt">//--- Get the list of added graphical objects and move them to the collection list class=class="str">"cmt">//--- (if failed to move the object to the collection, move on to the next object) if(!this.AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl)) class="kw">continue; } class=class="str">"cmt">//--- If the graphical object has been removed else if(obj_ctrl.Delta()<class="num">0) { class="type">int index=WRONG_VALUE; class=class="str">"cmt">//--- In the loop by the number of removed graphical objects for(class="type">int j=class="num">0;j<-obj_ctrl.Delta();j++) { class=class="str">"cmt">// Find an extra object in the list CGStdGraphObj *obj=this.FindMissingObj(chart_id,index); if(obj!=NULL) { class=class="str">"cmt">//--- Get the removed object parameters class="type">long lparam=obj.ChartID();
◍ 图表子窗口被删时的对象回收逻辑
在 MT5 的自定义图形对象管理类里,当某个受控图表窗口被交易者手动关闭或程序卸载时,主图控件必须感知这一事件并把残留对象清理掉,否则集合类会一直持有悬空指针。 RefreshForExtraObjects 方法从后往前遍历 m_list_charts_control,用 IsPresentChartWindow 比对窗口是否还在终端里。若不在,就先记下 chart_id 与 symbol,再调用 DeleteGraphObjCtrlObjFromList 把该窗口的控制器移出列表。 关键的回收动作是 MoveGraphObjectsToDeletedObjList(chart_id)——它会把该图表下所有标准图形对象转存到“已删除”列表,随后通过 EventChartCustom 向主图抛 GRAPH_OBJ_EVENT_DEL_CHART 事件,参数里带了 del_obj 数量,方便上层统计一次清理掉了多少对象。 FindMissingObj 则反向排查:从总对象列表里按 GRAPH_OBJ_PROP_CHART_ID 筛出属于某 chart_id 的项,若图表已失联就能定位到“在集合里、不在图上”的孤儿对象。实盘跑这套代码时,建议你在 EURUSD 的 M5 上开两个离线子窗口画线,关掉其中一个,观察主图自定义事件里的 del_obj 是否等于你画的数量。外汇与贵金属杠杆高,这类对象泄漏在长时间挂 EA 时可能引发内存漂移,需自行验证稳定性。
class="type">void CGraphElementsCollection::RefreshForExtraObjects(class="type">void) { for(class="type">int i=this.m_list_charts_control.Total()-class="num">1;i>WRONG_VALUE;i--) { CChartObjectsControl *obj_ctrl=this.m_list_charts_control.At(i); if(obj_ctrl==NULL) class="kw">continue; if(!this.IsPresentChartWindow(obj_ctrl.ChartID())) { class="type">long chart_id=obj_ctrl.ChartID(); class="type">class="kw">string chart_symb=obj_ctrl.Symbol(); class="type">int total_ctrl=this.m_list_charts_control.Total(); this.DeleteGraphObjCtrlObjFromList(obj_ctrl); class="type">int total_obj=this.m_list_all_graph_obj.Total(); this.MoveGraphObjectsToDeletedObjList(chart_id); class="type">int del_ctrl=total_ctrl-this.m_list_charts_control.Total(); class="type">int del_obj=total_obj-this.m_list_all_graph_obj.Total(); ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DEL_CHART,chart_id,del_obj,chart_symb); } } } CGStdGraphObj *CGraphElementsCollection::FindMissingObj(class="kw">const class="type">long chart_id,class="type">int &index) { CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,class="num">0,chart_id,EQUAL); if(list==NULL) class="kw">return NULL; index=WRONG_VALUE;
「图表对象回收时的索引与内存处理」
在图形对象集合类里,查找第一个不在当前图表上的对象时,用正向循环从 0 扫到 list.Total(),一旦 IsPresentGraphObjOnChart 返回 false 就记下 index 并直接 return obj,平均查找复杂度与残留对象数线性相关。 按图表 ID 批量迁移到已删除列表时,代码反过来用 for(int i=list.Total()-1;i>WRONG_VALUE;i--) 倒序遍历,避免 Detach 改变数组下标后漏删。这一处若改成正向循环,删除第 i 个后后面的元素前移,会跳过相邻对象。 单个对象迁移靠 Detach 先从总表剥离指针,再尝试 Add 进删除表;任意一步失败都写日志并返回 false,Add 失败还会 delete obj 防止野指针。外汇与贵金属图表上脚本频繁增删对象时,这种显式回收能降低 MT5 终端内存泄漏概率,但高频调用仍属高风险操作,建议开 MT5 用 Experts 日志验证 Detach/Add 的返回路径。
for(class="type">int i=class="num">0;i<list.Total();i++) { CGStdGraphObj *obj=list.At(i); if(obj==NULL) class="kw">continue; if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name())) { index=i; class="kw">return obj; } } class="kw">return NULL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Move all objects(by chart ID) | class=class="str">"cmt">//| to the list of removed graphical objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGraphElementsCollection::MoveGraphObjectsToDeletedObjList(class="kw">const class="type">long chart_id) { CArrayObj *list=GetList(GRAPH_OBJ_PROP_CHART_ID,class="num">0,chart_id,EQUAL); if(list==NULL) class="kw">return; for(class="type">int i=list.Total()-class="num">1;i>WRONG_VALUE;i--) { CGStdGraphObj *obj=list.At(i); if(obj==NULL) class="kw">continue; this.MoveGraphObjToDeletedObjList(obj); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Move the class object of the graphical object by index | class=class="str">"cmt">//| to the list of removed graphical objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(class="kw">const class="type">int index) { CGStdGraphObj *obj=this.m_list_all_graph_obj.Detach(index); if(obj==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST); class="kw">return class="kw">false; } if(!this.m_list_deleted_obj.Add(obj)) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST); class="kw">delete obj; class="kw">return class="kw">false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Move the specified graphical object class object |
图形对象回收与图表事件捕获的实现细节
在 MT5 自定义指标或 EA 里管理大量图形对象时,删除对象的归集逻辑直接影响后续回查效率。下面这段把待删对象从总表移入回收表的函数,先对总表做排序再按指针检索下标,最后复用按索引转移的重载方法。 [CODE] bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(CGStdGraphObj *obj) { this.m_list_all_graph_obj.Sort(); int index=this.m_list_all_graph_obj.Search(obj); return this.MoveGraphObjToDeletedObjList(index); } [/CODE] 逐行拆解:第 1 行声明返回布尔值的成员函数,入参为图形对象指针;第 3 行对全量对象链表排序,保证检索下标稳定;第 4 行用 Search 按指针定位对象在链表中的整型下标;第 5 行把下标传给另一个重载函数完成实际迁移并返回结果。 按图表名与 ID 反查已删对象时,先取对应图表 ID 的删除列表,再用名称做等值筛选,命中即返回首元素,否则空指针。 [CODE] CGStdGraphObj *CGraphElementsCollection::GetStdDelGraphObject(const string name,const long chart_id) { CArrayObj *list=this.GetListDel(GRAPH_OBJ_PROP_CHART_ID,0,chart_id); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,0,name,EQUAL); return(list!=NULL && list.Total()>0 ? list.At(0) : NULL); } [/CODE] OnChartEvent 里对 OBJECT_CHANGE / DRAG / CLICK 三类事件做了双向判断:既处理当前图表原生事件,也兼容自定义事件号偏移。图表 ID 的推导规则是——点击事件取 ChartID(),自定义点击事件从 lparam 取,其余按 lparam 为 0 则 ChartID() 否则 lparam。外汇与贵金属图表上这类对象交互频繁,自行扩展图形管理类时建议直接抄这套事件分发,能省掉重复判 ID 的坑。
class="type">bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(CGStdGraphObj *obj) { this.m_list_all_graph_obj.Sort(); class="type">int index=this.m_list_all_graph_obj.Search(obj); class="kw">return this.MoveGraphObjToDeletedObjList(index); } CGStdGraphObj *CGraphElementsCollection::GetStdDelGraphObject(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id) { CArrayObj *list=this.GetListDel(GRAPH_OBJ_PROP_CHART_ID,class="num">0,chart_id); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,class="num">0,name,EQUAL); class="kw">return(list!=NULL && list.Total()>class="num">0 ? list.At(class="num">0) : NULL); }
◍ 对象改名后怎么在集合里同步
在 MT5 自定义图形对象管理类里,用名字去取对象若返回 NULL,说明图表上的对象已被改名、不在原集合清单中。此时先调用 FindMissingObj 在清单里找「图表上已不存在对应图形」的槽位,再调用 FindExtraObj 反查图表上多出来的新名对象。 若两个查找任一落空就直接 return,避免空指针继续跑。拿到旧名对象与新名后,用 EventChartCustom 把旧名发往主控制图(事件码 GRAPH_OBJ_EVENT_RENAME,lparam 传 obj.ChartID()),随后 obj.SetName(name_new) 把集合里的名字改成图表新名,保证两端一致。 标准图形事件走另一分支:idx 落在 (GRAPH_OBJ_EVENT_NO_EVENT, GRAPH_OBJ_EVENTS_NEXT_CODE) 区间内时,按 CREATE / CHANGE / RENAME / DELETE / DEL_CHART 五类用 switch 打印日志。其中前三种会经 GetStdGraphObject(sparam,lparam) 取回对象并 PrintShort(),DELETE 与 DEL_CHART 只打 ChartID 与 symbol。开 MT5 把这段挂到 OnChartEvent 里,改个手绘趋势线名字就能在专家日志看到 RENAME 同步过程。
class=class="str">"cmt">//--- If failed to get the object by its name, it is not on the list, class=class="str">"cmt">//--- which means its name has been changed if(obj==NULL) { class=class="str">"cmt">//--- Let&class="macro">#x27;s search the list for the object that is not on the chart obj=this.FindMissingObj(chart_id); class=class="str">"cmt">//--- If failed to find the object here as well, exit if(obj==NULL) class="kw">return; class=class="str">"cmt">//--- Get the name of the renamed graphical object on the chart, which is not in the collection list class="type">class="kw">string name_new=this.FindExtraObj(chart_id); class=class="str">"cmt">//--- Send an event with the old name of an object to the control program chart and class=class="str">"cmt">//--- set a new name for the collection list object, which does not correspond to any graphical object on the chart ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),class="num">0,obj.Name()); obj.SetName(name_new); } class=class="str">"cmt">//--- Update the properties of the obtained object class=class="str">"cmt">//--- and check their change obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } class=class="str">"cmt">//--- Handle standard graphical object events if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE) { class=class="str">"cmt">//--- Depending on the event type, display an appropriate message in the journal class="kw">switch(idx) { case GRAPH_OBJ_EVENT_CREATE : ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE)); obj=this.GetStdGraphObject(sparam,lparam); if(obj!=NULL) obj.PrintShort(); class="kw">break; case GRAPH_OBJ_EVENT_CHANGE : ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE)); obj=this.GetStdGraphObject(sparam,lparam); if(obj!=NULL) obj.PrintShort(); class="kw">break; case GRAPH_OBJ_EVENT_RENAME : ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME)); obj=this.GetStdGraphObject(sparam,lparam); if(obj!=NULL) obj.PrintShort(); class="kw">break; case GRAPH_OBJ_EVENT_DELETE : ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE)); class="kw">break; case GRAPH_OBJ_EVENT_DEL_CHART: ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": ChartID: ",lparam,", ChartSymbol: ",sparam);
「图表事件里的对象改名追踪」
CGraphElementsCollection::OnChartEvent 负责把 MT5 图表上的交互事件路由到对应的图形对象。它先根据事件 id 与 CHARTEVENT_CUSTOM 的差值算出 idx,再判断是否属于 OBJECT_CHANGE、OBJECT_DRAG 或 OBJECT_CLICK 这三类,命中才继续处理。 图表 ID 的取法有讲究:普通点击事件直接用 ChartID();用户自定义事件则从 lparam 取;其余情况先给 WRONG_VALUE,再按 lparam 是否为 0 回退到当前图表或 lparam 本身。 当用 sparam 里的名字在集合里找不到对象时,说明图形对象被改名了。代码会调 FindMissingObj 找集合中缺失图表的对象,再用 FindExtraObj 拿到图表上的新名字,随后通过 SetNamePrev 与 SetName 更新,并发 GRAPH_OBJ_EVENT_RENAME 自定义事件给主图。 这套机制意味着:你在 MT5 上手拖或重命名趋势线时,底层集合能无声同步,不会丢引用。外汇与贵金属图表波动快、事件密集,重命名逻辑若漏判可能让 EA 误以为对象被删,实盘前应在回测器里故意改几次对象名验证。
class="type">void CGraphElementsCollection::OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { CGStdGraphObj *obj=NULL; class="type">class="kw">ushort idx=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM); if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CLICK || idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG || idx==CHARTEVENT_OBJECT_CLICK) { class=class="str">"cmt">//--- Calculate the chart ID class=class="str">"cmt">//--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID class=class="str">"cmt">//--- If the event ID corresponds to a user event, the chart ID is received from lparam class=class="str">"cmt">//--- Otherwise, the chart ID is assigned to -class="num">1 class="type">long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE); class="type">long chart_id=(param==WRONG_VALUE ? (lparam==class="num">0 ? ::ChartID() : lparam) : param); class=class="str">"cmt">//--- Get the object, whose properties were changed or which was relocated, class=class="str">"cmt">//--- from the collection list by its name set in sparam obj=this.GetStdGraphObject(sparam,chart_id); class=class="str">"cmt">//--- If failed to get the object by its name, it is not on the list, class=class="str">"cmt">//--- which means its name has been changed if(obj==NULL) { class=class="str">"cmt">//--- Let&class="macro">#x27;s search the list for the object that is not on the chart obj=this.FindMissingObj(chart_id); class=class="str">"cmt">//--- If failed to find the object here as well, exit if(obj==NULL) class="kw">return; class=class="str">"cmt">//--- Get the name of the renamed graphical object on the chart, which is not in the collection list class="type">class="kw">string name_new=this.FindExtraObj(chart_id); class=class="str">"cmt">//--- set a new name for the collection list object, which does not correspond to any graphical object on the chart, class=class="str">"cmt">//--- and send an event with the new name of the object to the control program chart if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new)) ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name()); } class=class="str">"cmt">//--- Update the properties of the obtained object
对象改名记录与暂停逻辑的底层接口
图形对象集合类里,PrintRenameHistory 方法靠 GetStdGraphObject 按名称与图表 ID 拿到对象指针,空指针直接 return,非空才调用 obj.PrintRenameHistory() 把改名历史吐到日志。想看某个画线是不是被脚本偷偷改过名,直接传原 name 和 chart_id 调这个方法即可。 Pause 的实现很直白:先设等待毫秒数,再设起始时间(time_start 乘 1000 转成毫秒),随后 while 死循环卡到 PauseIsCompleted 为真或 IsStopped 触发。这种忙等待在 EA 里会占满当前线程,回测时若 pause_msc 设 5000 就可能让 tick 处理挂起 5 秒,实盘贵金属跳空时段要警惕卡死风险。 GetListDeletedObj 返回被删对象数组,TotalDeletedGraphObjects 就是它的 Total(),用来数删了几个。GraphGetSizeProperty 透传 m_graph_objects.GetSizeProperty,拿某对象某属性的数组长度——比如查一条趋势线的锚点数组大小,返回 0 往往意味着对象已失效。
class=class="str">"cmt">//--- and check their change obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display the history of renaming the object to the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGraphElementsCollection::PrintRenameHistory(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id) { CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id); if(obj==NULL) class="kw">return; obj.PrintRenameHistory(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Launch the new pause countdown class="type">void Pause(class="kw">const class="type">class="kw">ulong pause_msc,class="kw">const class="type">class="kw">datetime time_start=class="num">0) { this.PauseSetWaitingMSC(pause_msc); this.PauseSetTimeBegin(time_start*class="num">1000); class="kw">while(!this.PauseIsCompleted() && !::IsStopped()){} } class=class="str">"cmt">//--- Return the(class="num">1) collection of graphical objects and(class="num">2) the list of removed objects CGraphElementsCollection *GetGraphicObjCollection(class="type">void) { class="kw">return &this.m_graph_objects; } CArrayObj *GetListDeletedObj(class="type">void) { class="kw">return this.m_graph_objects.GetListDeletedObj(); } class=class="str">"cmt">//--- Return(class="num">1) the number of removed graphical objects and(class="num">2) the size of the class="kw">property array class="type">int TotalDeletedGraphObjects(class="type">void) { class="kw">return this.GetListDeletedObj().Total(); } class="type">int GraphGetSizeProperty(class="kw">const class="type">class="kw">string name,class="kw">const class="type">long chart_id,class="kw">const class="type">int prop) { class="kw">return this.m_graph_objects.GetSizeProperty(name,chart_id,prop); } class=class="str">"cmt">//--- Fill in the array with IDs of the charts opened in the terminal
◍ 把对象事件处理挪进 EA 主响应里跑一遍
验证图形对象集合的事件逻辑,最省事的做法是复用前一篇的 EA,放到 \MQL5\Experts\TestDoEasy\Part91\ 目录下存成 TestDoEasyPart91.mq5。核心改动只有一处:把原本写在图形对象集合类事件处理器里的代码块,整体剪切到 EA 的 OnChartEvent() 函数体内。 那段代码模块的逻辑之前已拆过,这里不重复。编译后挂到图表上,能看到对象被重命名之后状态仍保留在“记忆”里,批量打包删除也按预期生效。 有个坑得记着:当图表子窗口被删掉再还原时,随图表一起消失的图形对象里,并非每一个都能在集合列表里正确恢复。目前还没定位到被略过的对象原因,后续会修,但实盘前你最好自己手动核对一遍对象列表。外汇和贵金属图表对象受tick驱动,这类恢复遗漏可能引发误判,属于高风险操作环节。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- If working in the tester, exit if(MQLInfoInteger(MQL_TESTER)) class="kw">return; class=class="str">"cmt">//--- If the mouse is moved /* if(id==CHARTEVENT_MOUSE_MOVE) { CForm *form=NULL; class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int wnd=class="num">0; class=class="str">"cmt">//--- If Ctrl is not pressed, if(!IsCtrlKeyPressed()) { class=class="str">"cmt">//--- clear the list of created form objects, allow scrolling a chart with the mouse and show the context menu list_forms.Clear(); ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true); ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,true); class="kw">return; } class=class="str">"cmt">//--- If X and Y chart coordinates are successfully converted into time and price, if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,wnd,time,price)) { class=class="str">"cmt">//--- get the bar index the cursor is hovered over class="type">int index=iBarShift(Symbol(),PERIOD_CURRENT,time); if(index==WRONG_VALUE) class="kw">return; class=class="str">"cmt">//--- Get the bar index by index CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index); if(bar==NULL) class="kw">return; class=class="str">"cmt">//--- Convert the coordinates of a chart from the time/price representation of the bar object to the X and Y coordinates class="type">int x=(class="type">int)lparam,y=(class="type">int)dparam; if(!ChartTimePriceToXY(ChartID(),class="num">0,bar.Time(),(bar.Open()+bar.Close())/class="num">2.0,x,y)) class="kw">return; class=class="str">"cmt">//--- Disable moving a chart with the mouse and showing the context menu ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,class="kw">false); ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,class="kw">false); class=class="str">"cmt">//--- Create the form object name and hide all objects except one having such a name class="type">class="kw">string name="FormBar_"+(class="type">class="kw">string)index; HideFormAllExceptOne(name); class=class="str">"cmt">//--- If the form object with such a name does not exist yet, if(!IsPresentForm(name)) { class=class="str">"cmt">//--- create a new form object form=bar.CreateForm(index,name,x,y,class="num">114,class="num">16); if(form==NULL) class="kw">return;
「悬浮窗体渲染与点击取价实现」
在 MT5 自定义指标里做价格行为标注,常需要把鼠标所在 K 线的形态描述弹成一个不可拖动的悬浮窗。下面这段逻辑先把窗体激活并锁死移动(SetMovable(false)),透明度压到 200,背景取色板首色、边框写死暗绿 C'47,70,59',再开阴影:偏移 2 像素、模糊半径 3、不透明度 200。
阴影色不是写死的——先用 ChangeColorSaturation 把背景降饱和成单色,若勾了「用图表背景色」(InpUseColorBG) 就再降 20 亮度,否则取 InpColorForm3。背景用 Erase 铺垂直渐变,边缘 DrawRectangle 描框,最后 Done() 把外观固化进对象列表;加列表失败直接 delete 返出,避免野指针。
窗体存在时,用 TextOnBG 把 bar.BodyTypeDescription() 按中锚点写进窗体中心(字色 C'7,28,21')并 Show(),随后 ChartRedraw() 强制重绘。
点击交互另走 CHARTEVENT_CLICK:必须按住 Ctrl(IsCtrlKeyPressed)才往下走,否则直接 return。用 ChartXYToTimePrice 把鼠标 x/y 转成 time 与 price,再 engine.GraphGetArrayChartsID 拉出图表 ID 数组——这一步是后续把标注绑到具体品种子图的前提,外汇与贵金属波动大,误触可能在高杠杆下干扰判断,实盘前应在模拟盘验证命中精度。
form.SetActive(true); form.SetMovable(class="kw">false); form.SetOpacity(class="num">200); form.SetColorBackground(array_clr[class="num">0]); form.SetColorFrame(C&class="macro">#x27;class="num">47,class="num">70,class="num">59&class="macro">#x27;); form.SetShadow(true); class="type">class="kw">color clrS=form.ChangeColorSaturation(form.ColorBackground(),-class="num">100); class="type">class="kw">color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-class="num">20) : InpColorForm3); form.DrawShadow(class="num">2,class="num">2,clr,class="num">200,class="num">3); form.Erase(array_clr,form.Opacity()); form.DrawRectangle(class="num">0,class="num">0,form.Width()-class="num">1,form.Height()-class="num">1,form.ColorFrame(),form.Opacity()); if(!list_forms.Add(form)) { class="kw">delete form; class="kw">return; } form.Done(); if(form!=NULL) { form.TextOnBG(class="num">0,bar.BodyTypeDescription(),form.Width()/class="num">2,form.Height()/class="num">2-class="num">1,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">7,class="num">28,class="num">21&class="macro">#x27;); form.Show(); } ChartRedraw(); if(id==CHARTEVENT_CLICK) { if(!IsCtrlKeyPressed()) class="kw">return; class="type">class="kw">datetime time=class="num">0; class="type">class="kw">double price=class="num">0; class="type">int sw=class="num">0; if(ChartXYToTimePrice(ChartID(),(class="type">int)lparam,(class="type">int)dparam,sw,time,price)) { class="type">long array[]; engine.GraphGetArrayChartsID(array);
把自定义事件翻译回对象动作
在 MT5 的 EA 里接管图表事件时,自定义事件码要减掉 CHARTEVENT_CUSTOM 才能拿到真实索引。代码里用 ushort idx=ushort(id-CHARTEVENT_CUSTOM) 做这步偏移,再判断 idx 是否落在 GRAPH_OBJ_EVENT_NO_EVENT 与 GRAPH_OBJ_EVENTS_NEXT_CODE 之间,只有命中这段区间才往下处理图形对象事件。 创建与修改是两类高频事件。GRAPH_OBJ_EVENT_CREATE 分支直接 Print 创建提示,再用 GetStdGraphObject(sparam,lparam) 按图表名和对象 ID 取指针,非空就调 PrintShort 把新对象简况打到日志;GRAPH_OBJ_EVENT_CHANGE 分支同理,但额外根据 dparam 落在整数、双精度还是字符串属性区间,调 GetPropertyDescription 把被改的具体属性名也打印出来。 这套机制对外汇和贵金属图表上的手动画线、拖拽锚点特别有用——你改一条趋势线颜色或价位,EA 能立刻在日志看到是哪条线、哪个属性变了。外汇贵金属波动剧烈、杠杆风险高,用事件回显做人工干预审计,比盲目跟信号更稳。
class="type">class="kw">ushort idx=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM); CGStdGraphObj *obj=NULL; if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE) { CChartObjectsControl *chart_ctrl=NULL; class="type">int end=class="num">0; class="type">class="kw">string evn=""; class="kw">switch(idx) { case GRAPH_OBJ_EVENT_CREATE : Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE),":"); obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam); if(obj!=NULL) { obj.PrintShort(); } class="kw">break; case GRAPH_OBJ_EVENT_CHANGE : Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE),":"); obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam); if(obj!=NULL) { obj.PrintShort(); if(dparam<GRAPH_OBJ_PROP_INTEGER_TOTAL) evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_INTEGER)dparam); else if(dparam<GRAPH_OBJ_PROP_DOUBLE_TOTAL) evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_DOUBLE)dparam); else evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_STRING)dparam); } class="kw">break; } }
◍ 图形对象重命名与删除的事件捕获
在 MT5 的自定义引擎里,图形对象生命周期事件是通过 switch-case 分发处理的。重命名事件 GRAPH_OBJ_EVENT_RENAME 触发时,先用 sparam 和 lparam 从集合里取对象指针,再打印旧名与新名。 取对象后若指针非空,会输出最近一次名称(CurrSize-1 索引)与初始名称(0 索引),并调用 PrintRenameHistory 把整个改名链路写进日志。这类痕迹对复盘手动画线策略很有用。 删除分两种:单对象 GRAPH_OBJ_EVENT_DELETE 用 GetStdDelGraphObject 取已删对象并打印简况;随图表关闭的 GRAPH_OBJ_EVENT_DEL_CHART 则按 lparam 图表 ID 和 sparam 品种名批量处理。 循环边界用 engine.TotalDeletedGraphObjects() 减去 dparam 算 end,若 end<0 则归零,再从列表尾部向前遍历取对象,确保只处理本次事件新增的删除项。
Print(DFUN,evn); } class="kw">break; class=class="str">"cmt">//--- Graphical object renaming event case GRAPH_OBJ_EVENT_RENAME : class=class="str">"cmt">//--- Display the message about renaming the graphical object Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME)); class=class="str">"cmt">//--- Get the pointer to the object by chart name and ID passed to sparam and lparam, respectively obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam); if(obj!=NULL) { class=class="str">"cmt">//--- Display the previous and new object name, as well as its entire renaming history, in the journal Print(DFUN,obj.GetProperty(GRAPH_OBJ_PROP_NAME,obj.Properties().CurrSize(GRAPH_OBJ_PROP_NAME)-class="num">1)," >>> ",obj.GetProperty(GRAPH_OBJ_PROP_NAME,class="num">0)); obj.PrintRenameHistory(); } class="kw">break; class=class="str">"cmt">//--- Graphical object deletion event case GRAPH_OBJ_EVENT_DELETE : class=class="str">"cmt">//--- Display the message about removing the graphical object Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE),":"); class=class="str">"cmt">//--- Get the pointer to the removed object by chart name and ID passed to sparam and lparam, respectively class=class="str">"cmt">//--- and display a class="type">class="kw">short description of the removed object in the journal obj=engine.GetGraphicObjCollection().GetStdDelGraphObject(sparam,lparam); if(obj!=NULL) { obj.PrintShort(); } class="kw">break; class=class="str">"cmt">//--- Event of removing the graphical object together with the chart window case GRAPH_OBJ_EVENT_DEL_CHART: class=class="str">"cmt">//--- Display the message about removing graphical objects together with the chart window, whose ID and symbol are passed to lparam and sparam Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": #",lparam,", ",sparam,":"); class=class="str">"cmt">//--- Calculate the end value for the loop by the list of removed graphical objects end=engine.TotalDeletedGraphObjects()-(class="type">int)dparam; if(end<class="num">0) end=class="num">0; class=class="str">"cmt">//--- In the loop from the end of the removed graphical objects list up to the value calculated in the &class="macro">#x27;end&class="macro">#x27; variable, for(class="type">int i=engine.TotalDeletedGraphObjects()-class="num">1;i>=end;i--) { class=class="str">"cmt">//--- get the next removed graphical object from the list obj=engine.GetListDeletedObj().At(i);
「空对象跳过与简要日志输出」
在遍历图形对象集合时,若取到空指针直接 continue 是最省事也最安全的写法,能避免后续调用成员函数时触发运行时错误。
代码片段里 if(obj==NULL) continue; 之后紧接 obj.PrintShort();,意思是只对有效对象向终端日志打印一段简短描述,方便在 EA 调试阶段快速核对当前画布上的对象状态。
这种写法在外币与贵金属策略调试中很实用:MT5 客户端对象数上限约 2000 个,刷屏前先过滤空值,日志可读性会明显提升。外汇与贵金属杠杆高,任何对象句柄泄漏都可能让脚本在实盘前就崩,建议开 MT5 把这段贴进 OnInit 后的遍历逻辑里跑一遍。
if(obj==NULL) class="kw">continue; class=class="str">"cmt">//--- and display its brief description in the journal obj.PrintShort(); } class="kw">break; class=class="str">"cmt">//--- class="kw">default: class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+
画得少,看得清
这一节原本只是作者交代下一篇要做的对象属性历史存储,以及放出当前函数库压缩包(MQL5.zip,约 4181.64 KB)供下载验证。从第八十六到九十部分一路铺下来,核心落点其实就一句:把图形对象的属性变化和事件先管起来,后面才有办法回溯。 你在 MT5 里把这套 EA 和指标挂上,重点不是看它画了多少线,而是确认对象改动有没有被集合捕获、跨图表同不同步。能跑通这点,下一篇的「属性修改历史」才接得住。 外汇和贵金属图表对象受报价跳变影响大,这类跟踪逻辑在极端滑点下可能漏事件,验证时先用模拟环境跑。