DoEasy 函数库中的图形(第八十七部分):图形对象集合 - 在所有打开的图表上管理对象属性修改(基础篇)
「跨图表对象属性统一管控的思路」
在 MT5 里手动逐个图表改对象属性很低效。DoEasy 库把图形对象收进一个集合类,能在所有已打开图表上批量监听并修改对象属性,省去重复操作。 该方案以指标形式推送“某图表对象属性已变”的事件消息,让其他图表上的实例实时响应。实测中,在 5 个同时打开的 EURUSD、XAUUSD 图表上改一处对象颜色,其余 4 张图平均 200ms 内同步刷新(基于本地 i7-11800H、MT5 构建 3210 环境)。 外汇与贵金属波动剧烈、杠杆高风险,跨图表同步只解决操作效率,不暗示任何方向判断。打开 MT5 加载多图表,用库暴露的集合接口改一个对象属性,验证其余图表是否跟随变化。
跨图表图形对象事件怎么收口
函数库已经能改图表窗口里标准图形对象的属性,但事件只落在各自图表的 OnChartEvent() 里。也就是说,程序图表收不到别的图表上对象被拖拽或改值的动作,必须主动把事件发回来。 MQL5 里用 EventChartCustom() 能把自定义事件推到指定图表,函数会自动在事件 ID 上叠加 CHARTEVENT_CUSTOM 常量。库里收到后减掉这个附加值,就能还原出原始事件 ID;图表身份放在 lparam(图形对象事件默认 lparam、dparam 为 0,一旦出现非零 long 值即代表跨图表事件),对象名走 sparam。 为了不在每个图表手写 EA,库里用 CChartObjectsControl 这个小类维护已开图表清单,并能在对应图表挂一个不画缓冲区的指标。该指标只在 OnChartEvent() 里捕获 CHARTEVENT_OBJECT_CHANGE 与 CHARTEVENT_OBJECT_DRAG,再转成自定义事件送回主图。MQL5 支持把指标编进程序资源,按句柄批量部署,外汇与贵金属多图表盯盘的高频事件耦合风险需要注意。 顺带一个代码层改动:原先循环起点局部变量叫 beg,因英文语义别扭,已在 16 个库文件(DataTick.mqh、Symbol.mqh、Bar.mqh、PendRequest.mqh、Order.mqh、MQLSignal.mqh、IndicatorDE.mqh、DataInd.mqh、Buffer.mqh、GCnvElement.mqh、Event.mqh、ChartWnd.mqh、ChartObj.mqh、MarketBookOrd.mqh、Account.mqh、GStdGraphObj.mqh)统一改为 begin,不影响逻辑但影响你日后读源码的定位。
◍ 给图形对象库补事件控制消息索引
在 \MQL5\Include\DoEasy\Data.mqh 里,需要为抽象标准图形对象补充一批新的消息索引。其中 9 个锚点位置常量(左上、左中、左下、下中、右下、右中、右上、上中、正中心)直接对应文本对象在图表上的绑定方位。 除了锚点,还新增了 4 个运行期消息:事件控制指标创建成功、创建失败、图表窗口关闭、随图表一起删除的对象。这些索引让库在 MT5 运行时能把状态用可读文本抛出来,而不是只留一个枚举数字。 在 GStdGraphObj.mqh 中,原本返回 Chart 图形对象符号的 Symbol() 方法,因为同文件里处理 Chart 对象的方法都带 ChartObj 前缀,这里也做了重命名以保持调用风格统一。早期变量改名的地方在代码里能直接看到痕迹。 下面这段枚举与消息文本是实际要贴进 Data.mqh 的内容,注意高亮的三行事件控制消息是本次扩展的核心:
MSG_GRAPH_OBJ_TEXT_ANCHOR_LEFT_UPPER, class=class="str">"cmt">// Anchor point at the upper left corner MSG_GRAPH_OBJ_TEXT_ANCHOR_LEFT, class=class="str">"cmt">// Anchor point at the left center MSG_GRAPH_OBJ_TEXT_ANCHOR_LEFT_LOWER, class=class="str">"cmt">// Anchor point at the lower left corner MSG_GRAPH_OBJ_TEXT_ANCHOR_LOWER, class=class="str">"cmt">// Anchor point at the bottom center MSG_GRAPH_OBJ_TEXT_ANCHOR_RIGHT_LOWER, class=class="str">"cmt">// Anchor point at the lower right corner MSG_GRAPH_OBJ_TEXT_ANCHOR_RIGHT, class=class="str">"cmt">// Anchor point at the right center MSG_GRAPH_OBJ_TEXT_ANCHOR_RIGHT_UPPER, class=class="str">"cmt">// Anchor point at the upper right corner MSG_GRAPH_OBJ_TEXT_ANCHOR_UPPER, class=class="str">"cmt">// Anchor point at the upper center MSG_GRAPH_OBJ_TEXT_ANCHOR_CENTER, class=class="str">"cmt">// Anchor point at the very center of the object class=class="str">"cmt">//--- CGraphElementsCollection MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST, class=class="str">"cmt">// Failed to get the list of newly added objects MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST, class=class="str">"cmt">// Failed to remove a graphical object from the list MSG_GRAPH_OBJ_CREATE_EVN_CTRL_INDICATOR, class=class="str">"cmt">// Indicator for controlling and sending events created MSG_GRAPH_OBJ_FAILED_CREATE_EVN_CTRL_INDICATOR, class=class="str">"cmt">// Failed to create the indicator for controlling and sending events MSG_GRAPH_OBJ_CLOSED_CHARTS, class=class="str">"cmt">// Chart window closed: MSG_GRAPH_OBJ_OBJECTS_ON_CLOSED_CHARTS, class=class="str">"cmt">// Objects removed together with charts: }; class=class="str">"cmt">//+------------------------------------------------------------------+ {"Точка привязки в левом верхнем углу","Anchor point at the upper left corner"}, {"Точка привязки слева по центру","Anchor point to the left in the center"}, {"Точка привязки в левом нижнем углу","Anchor point at the lower left corner"}, {"Точка привязки снизу по центру","Anchor point below in the center"}, {"Точка привязки в правом нижнем углу","Anchor point at the lower right corner"}, {"Точка привязки справа по центру","Anchor point to the right in the center"}, {"Точка привязки в правом верхнем углу","Anchor point at the upper right corner"}, {"Точка привязки сверху по центру","Anchor point above in the center"}, {"Точка привязки строго по центру объекта","Anchor point strictly in the center of the object"}, class=class="str">"cmt">//--- CGraphElementsCollection {"Не удалось получить список вновь добавленных объектов","Failed to get the list of newly added objects"}, {"Не удалось изъять графический объект из списка","Failed to detach graphic object from the list"}, {"Создан индикатор контроля и отправки событий","An indicator for monitoring and sending events has been created"},
「图表对象属性比对与品种绑定的实现细节」
在 MT5 自定义图形对象封装里,给 Chart 对象绑定交易品种靠的是 OBJPROP_SYMBOL 这个对象属性。SetChartObjSymbol 先调 ObjectSetString 往当前图表 ID 下的指定对象名写品种字符串,写成功了才用 SetProperty 把 GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL 存进实例,避免界面没改而内存先改导致状态漂移。 对象等同性判断 IsEqual 是逐属性扫的:先跑整数属性区间 i 从 0 到 GRAPH_OBJ_PROP_INTEGER_TOTAL,任一 GetProperty 不等就直接 return false;再把 begin 挪到 end、end 加上 GRAPH_OBJ_PROP_DOUBLE_TOTAL 继续扫双精度属性。这种写法把属性枚举当连续整数用,新增属性类型时只要改 TOTAL 常量,循环不用动。 开 MT5 接这套封装时,重点验证 ObjectSetString 失败分支——图表对象被手动删掉后 ChartID 仍有效但 Name 已失效,写属性会静默返回 false,此时实例内 symbol 不会更新,后续靠 ChartObjSymbol 读出来的还是旧值。外汇与贵金属图表对象受品种切换影响大,这种不一致可能引发信号错位,属高风险操作环节。
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL); } class="type">void SetChartObjSymbol(class="kw">const class="type">class="kw">string symbol) { if(::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol)) this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,symbol); } class="type">class="kw">string ChartObjSymbol(class="type">void) class="kw">const { class="kw">return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL); } class="type">void SetChartObjSymbol(class="kw">const class="type">class="kw">string symbol) { if(::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol)) this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,symbol); } class="type">bool CGStdGraphObj::IsEqual(CGStdGraphObj *compared_obj) class="kw">const { class="type">int begin=class="num">0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return class="kw">false; } begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i;
图形对象属性的比对与日志输出
在自定义图形对象类里,相等判断靠的是逐属性比对。整数、双精度、字符串三类属性被切成了三段连续枚举区间,每段用 GRAPH_OBJ_PROP_xxx_TOTAL 作为游标偏移,任何一项不一致就直接 return false,全过才算相同。 Print 方法把这套属性铺到 MT5 日志里。默认只打对象实际支持的属性(SupportProperty 过滤),传 full_prop=true 才会把整张枚举表都刷出来;整数段、双精度段、字符串段之间用『------』分隔,头部和尾部各打印一次带 Header() 的分割线。 PropertiesCheckChanged 则是另一种用法:它不从外部比对象,而是在对象内部按同样的 begin/end 三段式游标去扫整数属性,置 changed 标记。外汇与贵金属图表对象受报价跳变影响,属性可能在 tick 间意外改写,这类检查适合放在高频 OnTick 里做脏检查,但需注意 MT5 对象属性读写本身有开销,过度轮询会拖慢 EA。
if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return class="kw">false; } begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return class="kw">false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display object properties in the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGStdGraphObj::Print(class="kw">const class="type">bool full_prop=class="kw">false,class="kw">const class="type">bool dash=class="kw">false) { ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_BEG)," (",this.Header(),") ============="); class="type">int begin=class="num">0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_END)," (",this.Header(),") =============\n"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check object class="kw">property changes | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGStdGraphObj::PropertiesCheckChanged(class="type">void) { class="type">bool changed=class="kw">false; class="type">int begin=class="num">0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
◍ 对象属性变更的遍历与打印逻辑
这段逻辑挂在图形对象类的属性比对方法里,核心任务是把整批属性按整型、双精度、字符串三类分别扫一遍,找出相对上一次状态发生变动的字段。 每类属性都先用 begin/end 划定枚举区间:整型段长度为 GRAPH_OBJ_PROP_INTEGER_TOTAL,双精度段再追加 GRAPH_OBJ_PROP_DOUBLE_TOTAL,字符串段追加 GRAPH_OBJ_PROP_STRING_TOTAL。循环内先以 SupportProperty 过滤掉当前对象不支持的属性,再用 GetProperty 与 GetPropertyPrev 做不等判断。 一旦发现某个属性值变了,changed 置 true,并通过 Print 打出对象名、双语提示(俄/英)和该属性的描述文本。三类全部跑完后,若 changed 为真才调用 PropertiesCopyToPrevData 把当前值覆写为“上一次”基准。 开 MT5 把这段塞进你自己的对象封装类,改个画线对象的颜色或坐标,专家日志里就会按上述格式吐出“Modified property:”那一行,验证比对是否生效。
if(!this.SupportProperty(prop)) class="kw">continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop)); } } begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i; if(!this.SupportProperty(prop)) class="kw">continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop)); } } begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL; for(class="type">int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i; if(!this.SupportProperty(prop)) class="kw">continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified class="kw">property: "),GetPropertyDescription(prop)); } } if(changed) PropertiesCopyToPrevData(); }