图形对象集合管理基础:用事件模型接管属性修改追踪(基础篇)
📊

图形对象集合管理基础:用事件模型接管属性修改追踪(基础篇)

(1/3)·手动轮询对象属性烧 CPU 还漏事件?这篇先把单图表事件追踪地基打牢

新手友好 第 1/3 篇

很多人在 MQL5 里习惯用定时器不断调用 ObjectGetXXX 去读图形对象属性,以为这样最稳。实际上这些函数是同步的,对象一多就卡图表,而且测试器里根本跑不起来。换个思路,让事件自己告诉你哪个对象变了,才是省资源的做法。

◍ 盯住图形对象的属性变动

在 MT5 里做价格行为分析,经常要感知图表上手动画的对象(趋势线、矩形、斐波那契)被改了参数或被删了。DoEasy 函数库把这类事件收进一个图形对象集合,专门管属性修改和删除的跟踪,不用自己在 EA 里写一大堆 ObjectGetInteger 轮询。 实际跑起来,对象集合会在终端事件里捕获 OBJPROP 变更和 OBJDELETE,把旧值新值都留痕。这样你写辅助脚本时,能直接拿到「哪根线、哪个属性、从多少变到多少」三要素,复盘手绘支撑阻力区特别省事。 外汇和贵金属波动快、滑点大,这类手动标注对象若被误删或挪动,策略信号可能瞬间失真,属于高风险场景下的细节坑。开 MT5 加载 DoEasy 的图形集合示例,拖一根趋势线改个颜色,就能在日志里看到属性跟踪记录。

「为什么用事件模型盯图形对象变更」

上一版已经能把新画的标准图形对象塞进集合类:检测到新对象、按类型建类、加进列表。但这只覆盖“出生”,对象属性的改动、改名、删除全都没管。 读属性靠 ObjectGetXXX 系列函数,它们是同步的——发命令后干等结果返回。要是挂定时器轮询每张图的每个属性,对象一多就疯狂吃资源,实盘卡顿概率很高。 摆在面前的路有两条:要么定时器里同步穷举所有对象所有属性;要么在 OnChartEvent() 里吃事件模型。后者代码清爽,但策略测试器里 OnChartEvent 不触发(测试器靠 OnTick/OnCalculate 顶替定时器),图形对象目前根本进不了测试器窗口。 权衡下来我选事件模型,只在“活”图表上跑。本文先只做当前图表(挂 EA 的那张)的对象事件处理测试版;验证稳了,再给每个已开图表写成熟 handler,把事件甩回主图,由函数库统一收进集合处理。外汇/贵金属自动化对象管理本身高风险,测试器限制意味着回测无法覆盖这部分逻辑,只能实盘验证。

用三套旧值数组盯住对象属性漂移

MT5 的 OnChartEvent() 里,CHARTEVENT_OBJECT_CHANGE 只回传被改对象的名称,不告诉你具体哪个属性变了。想在 EA 里精确感知趋势线被拖拽、斐波那契被改名,就得自己留底——把整数、实数、字符串三类属性各存一份“改动前”的快照。 具体做法是在抽象标准图形对象类 GStdGraphObj 的 private 段加三个数组:m_long_prop_prev、m_double_prop_prev、m_string_prop_prev,长度分别与现用的 GRAPH_OBJ_PROP_INTEGER_TOTAL 等常量对齐。事件进来后,用 PropertiesRefresh() 一次性刷全部当前属性,再跑 CheckPropertyChanges() 把三类数组逐元素和 prev 版比对,不等就置“已变”标志并临时打印日志。 改名操作在终端里实际触发三个事件:删除、创建、属性变更,且 CHARTEVENT_OBJECT_CHANGE 总是最后到。由于对象按名称+图表 ID 选取,可以在集合里扫一遍哪个名称不存在,从而把新名字补进跟踪列表,不必硬解三事件时序。 构造函数现已瘦身:原先在构造时直接读所有属性,现改为进构造先读名称,其余搬进三个 ReceiveXxxProps() 方法由 PropertiesRefresh() 统一调,最后调 PropertiesCopyToPrevData() 把当前值拷进 prev 数组,下次比对就有基准。下面是被抽出的私有段新增部分,黄底即 prev 数组声明。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| The class of the abstract standard graphical object                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CGStdGraphObj : class="kw">public CGBaseObj
  {
class="kw">private:
   class="type">long              m_long_prop[GRAPH_OBJ_PROP_INTEGER_TOTAL];       class=class="str">"cmt">// Integer properties
   class="type">class="kw">double            m_double_prop[GRAPH_OBJ_PROP_DOUBLE_TOTAL];      class=class="str">"cmt">// Real properties
   class="type">class="kw">string            m_string_prop[GRAPH_OBJ_PROP_STRING_TOTAL];      class=class="str">"cmt">// String properties

   class="type">long              m_long_prop_prev[GRAPH_OBJ_PROP_INTEGER_TOTAL];  class=class="str">"cmt">// Integer properties before change
   class="type">class="kw">double            m_double_prop_prev[GRAPH_OBJ_PROP_DOUBLE_TOTAL]; class=class="str">"cmt">// Real properties before change
   class="type">class="kw">string            m_string_prop_prev[GRAPH_OBJ_PROP_STRING_TOTAL]; class=class="str">"cmt">// String properties before change

class=class="str">"cmt">//--- Return the index of the array the(class="num">1) class="type">class="kw">double and(class="num">2) class="type">class="kw">string properties are actually located at
   class="type">int               IndexProp(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property)  class="kw">const { class="kw">return(class="type">int)class="kw">property-GRAPH_OBJ_PROP_INTEGER_TOTAL; }
   class="type">int               IndexProp(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property)  class="kw">const { class="kw">return(class="type">int)class="kw">property-GRAPH_OBJ_PROP_INTEGER_TOTAL-GRAPH_OBJ_PROP_DOUBLE_TOTAL; }
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

◍ 图形对象属性的存取与历史快照

在 MT5 自定义图形类里,属性被拆成整数、双精度、字符串三类分别存进 m_long_prop / m_double_prop / m_string_prop 三个数组。整数属性直接用枚举下标写入,而双精度和字符串要先经 IndexProp() 做一次映射再落库,这点差异在改写对象管理器时容易踩坑。 下面这组重载方法覆盖了当前值与上一帧值的读写。SetProperty 写当前态,SetPropertyPrev 写历史态,GetProperty / GetPropertyPrev 对应读出;外汇与贵金属图表上做对象状态比对(如线段端点位移)时,prev 数组能让你免去找前一根 K 线再算差的麻烦。 [CODE] 里的代码可直接粘进你的 CGraphObject 派生类。开 MT5 新建 EA 附加这类对象,连续改两次坐标,分别调 GetProperty 和 GetPropertyPrev,能验证历史快照确实隔离存储、互不覆盖。

MQL5 / C++
class="type">void SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">long value) { this.m_long_prop[class="kw">property]=value; }
class="type">void SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">class="kw">double value){ this.m_double_prop[this.IndexProp(class="kw">property)]=value; }
class="type">void SetProperty(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">class="kw">string value){ this.m_string_prop[this.IndexProp(class="kw">property)]=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
class="type">long GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property) class="kw">const { class="kw">return this.m_long_prop[class="kw">property]; }
class="type">class="kw">double GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property) class="kw">const { class="kw">return this.m_double_prop[this.IndexProp(class="kw">property)]; }
class="type">class="kw">string GetProperty(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property) class="kw">const { class="kw">return this.m_string_prop[this.IndexProp(class="kw">property)]; }

class=class="str">"cmt">//--- Set object&class="macro">#x27;s previous(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property,class="type">long value) { this.m_long_prop_prev[class="kw">property]=value; }
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property,class="type">class="kw">double value){ this.m_double_prop_prev[this.IndexProp(class="kw">property)]=value;}
class="type">void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property,class="type">class="kw">string value){ this.m_string_prop_prev[this.IndexProp(class="kw">property)]=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 previous properties array
class="type">long GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER class="kw">property) class="kw">const { class="kw">return this.m_long_prop_prev[class="kw">property]; }
class="type">class="kw">double GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE class="kw">property) class="kw">const { class="kw">return this.m_double_prop_prev[this.IndexProp(class="kw">property)]; }
class="type">class="kw">string GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING class="kw">property) class="kw">const { class="kw">return this.m_string_prop_prev[this.IndexProp(class="kw">property)]; }

「图形对象属性的存取与刷新接口」

CGStdGraphObj 这个类把 MT5 图形对象的常见属性做了封装,对外暴露的是一组简化读写方法。通过 Number() 和 SetNumber() 可以拿到或设定对象在列表里的索引,底层走的是 GRAPH_OBJ_PROP_NUM 这个属性标识。 ObjectID() 返回对象的唯一 ID,而 SetObjectID() 不止调了基类的 CGBaseObj::SetObjectID(),还同步写了 GRAPH_OBJ_PROP_ID 的当前值与前值(SetPropertyPrev),这样后续做变更比对时才有基准。 PropertiesRefresh() 会重写全部图形对象属性,PropertiesCheckChanged() 则负责检查哪些属性被改过。这两个方法配合私有区的 GetAndSaveINT / DBL / STR,分别抓取整型、双精度、字符串三类属性并落盘保存,是对象状态同步的关键路径。 在 MT5 里新建一个继承 CGStdGraphObj 的 EA 或指标,断点跟一下 SetObjectID 调用前后 GRAPH_OBJ_PROP_ID 与前值的变化,就能确认这套属性机制是否按预期工作。外汇与贵金属图表对象受 tick 驱动,属性刷新频率高,实盘验证时需注意高性能消耗风险。

MQL5 / C++
CGStdGraphObj      *GetObject(class="type">void)                                                          { class="kw">return &this;}
class="kw">public:
class=class="str">"cmt">//+--------------------------------------------------------------------+ 
class=class="str">"cmt">//|Methods of simplified access and setting graphical object properties|
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//--- Object index in the list
   class="type">int               Number(class="type">void)                class="kw">const { class="kw">return (class="type">int)this.GetProperty(GRAPH_OBJ_PROP_NUM);                    }
   class="type">void              SetNumber(class="kw">const class="type">int number)       { this.SetProperty(GRAPH_OBJ_PROP_NUM,number);                        }
class=class="str">"cmt">//--- Object ID
   class="type">long              ObjectID(class="type">void)              class="kw">const { class="kw">return this.GetProperty(GRAPH_OBJ_PROP_ID);                          }
   class="type">void              SetObjectID(class="kw">const class="type">long obj_id)
                     {
                       CGBaseObj::SetObjectID(obj_id);
                       this.SetProperty(GRAPH_OBJ_PROP_ID,obj_id);
                       this.SetPropertyPrev(GRAPH_OBJ_PROP_ID,obj_id);
                     }
class=class="str">"cmt">//--- Graphical object type
class=class="str">"cmt">//--- Return the description of the object visibility on timeframes
   class="type">class="kw">string            VisibleOnTimeframeDescription(class="type">void);
class=class="str">"cmt">//--- Re-write all graphical object properties
   class="type">void              PropertiesRefresh(class="type">void);
class=class="str">"cmt">//--- Check object class="kw">property changes
   class="type">void              PropertiesCheckChanged(class="type">void);

class="kw">private:
class=class="str">"cmt">//--- Get and save(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties
   class="type">void              GetAndSaveINT(class="type">void);
   class="type">void              GetAndSaveDBL(class="type">void);
   class="type">void              GetAndSaveSTR(class="type">void);
class=class="str">"cmt">//--- Copy the current data to the previous one

受保护构造器里的属性落盘细节

CGStdGraphObj 的受保护参数化构造器接收四个入参:对象类型枚举、归属枚举、分组枚举、图表 ID 与名称字符串。它在初始化列表外先写死 m_type,再借 CGBaseObj 基类方法把图表 ID、图形对象类型、元素类型、归属与分组逐一落位,并调用 SetSubwindow 绑定子窗口。 Digits 的获取走 SymbolInfoInteger(ChartSymbol(chart_id), SYMBOL_DIGITS) 并强转 int,这一步决定了后续价格标签的小数精度,做贵金属 XAUUSD 时通常是 2,外汇 EURUSD 多为 5,写错会令画线锚点偏移。 整数属性分两块存:第一块是「所有图形对象共有但对象自身不携带」的 7 个字段,如 CHART_ID、WND_NUM、TYPE、ELEMENT_TYPE、BELONG、GROUP 以及默认置 0 的 ID 与 NUM;第二块是「共有且对象自身携带」的字段,直接 ObjectGetInteger 拉取 CREATETIME、TIMEFRAMES、BACK 等运行时属性。 把这两类属性在构造阶段一次性拷进 m_long_prop 数组,后面回读就不用反复调终端 API,对 MT5 上同时挂几十条对象的脚本,可能明显减轻主线程抖动。外汇与贵金属杠杆高,任何对象创建逻辑异常都可能引发错单,建议先在策略测试器用历史数据验证构造器行为。

MQL5 / C++
class="type">void PropertiesCopyToPrevData(class="type">void);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Protected parametric constructor                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGStdGraphObj::CGStdGraphObj(class="kw">const ENUM_OBJECT_DE_TYPE obj_type,
                             class="kw">const ENUM_GRAPH_OBJ_BELONG belong,
                             class="kw">const ENUM_GRAPH_OBJ_GROUP group,
                             class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name)
  {
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;
   CGBaseObj::SetChartID(chart_id);
   CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type));
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD);
   CGBaseObj::SetBelong(belong);
   CGBaseObj::SetGroup(group);
   CGBaseObj::SetSubwindow(chart_id,name);
   CGBaseObj::SetDigits((class="type">int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS));

class=class="str">"cmt">//--- Save integer properties
   class=class="str">"cmt">//--- properties inherent in all graphical objects but not present in a graphical object
   this.m_long_prop[GRAPH_OBJ_PROP_CHART_ID]     = CGBaseObj::ChartID();           class=class="str">"cmt">// Chart ID
   this.m_long_prop[GRAPH_OBJ_PROP_WND_NUM]      = CGBaseObj::SubWindow();         class=class="str">"cmt">// Chart subwindow index
   this.m_long_prop[GRAPH_OBJ_PROP_TYPE]         = CGBaseObj::TypeGraphObject();   class=class="str">"cmt">// Graphical object type(ENUM_OBJECT)
   this.m_long_prop[GRAPH_OBJ_PROP_ELEMENT_TYPE]= CGBaseObj::TypeGraphElement(); class=class="str">"cmt">// Graphical element type(ENUM_GRAPH_ELEMENT_TYPE)
   this.m_long_prop[GRAPH_OBJ_PROP_BELONG]       = CGBaseObj::Belong();            class=class="str">"cmt">// Graphical object affiliation
   this.m_long_prop[GRAPH_OBJ_PROP_GROUP]        = CGBaseObj::Group();             class=class="str">"cmt">// Graphical object group
   this.m_long_prop[GRAPH_OBJ_PROP_ID]           = class="num">0;                              class=class="str">"cmt">// Object ID
   this.m_long_prop[GRAPH_OBJ_PROP_NUM]          = class="num">0;                              class=class="str">"cmt">// Object index in the list
   class=class="str">"cmt">//--- Properties inherent in all graphical objects and present in a graphical object
   this.m_long_prop[GRAPH_OBJ_PROP_CREATETIME]   = ::ObjectGetInteger(chart_id,name,OBJPROP_CREATETIME);  class=class="str">"cmt">// Object creation time
   this.m_long_prop[GRAPH_OBJ_PROP_TIMEFRAMES]   = ::ObjectGetInteger(chart_id,name,OBJPROP_TIMEFRAMES);  class=class="str">"cmt">// Object visibility on timeframes
   this.m_long_prop[GRAPH_OBJ_PROP_BACK]         = ::ObjectGetInteger(chart_id,name,OBJPROP_BACK);        class=class="str">"cmt">// Background object
把对象变更监听交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象创建与属性变更提示,你不用自己写 OnChartEvent 也能先观察规律。

常见问题

ObjectGetXXX 系列是同步函数,会等待命令执行,对象数量大时占用大量资源,且在策略测试器中无法处理图形对象事件。
可以,小布盯盘的 AIGC 已内置对象变更诊断,打开对应品种页就能看到创建与属性修改的提示,省去自己写事件处理的前期成本。
前者是通过属性对话框更改对象属性时触发,后者是用鼠标拖动图形对象时触发,二者都需在 OnChartEvent 中响应。
作者先实现启动程序图表的测试版本确认逻辑正常,后续才会扩展为每个打开图表的成熟事件处理并汇总到主图表。
测试器不允许添加图形对象到窗口并改属性,因此只能在实时图表的事件处理程序中处理,测试器内无法验证对象追踪。