DoEasy 函数库中的图形(第八十七部分):图形对象集合 - 在所有打开的图表上管理对象属性修改(基础篇)
📘

DoEasy 函数库中的图形(第八十七部分):图形对象集合 - 在所有打开的图表上管理对象属性修改(基础篇)

第 1/3 篇

「跨图表对象属性统一管控的思路」

在 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 的内容,注意高亮的三行事件控制消息是本次扩展的核心:

MQL5 / C++
  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 读出来的还是旧值。外汇与贵金属图表对象受品种切换影响大,这种不一致可能引发信号错位,属高风险操作环节。

MQL5 / C++
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。

MQL5 / C++
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:”那一行,验证比对是否生效。

MQL5 / C++
   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();
}

常见问题

用跨图表对象集合统一遍历所有打开图表的同名对象,批量写属性即可,不必手动逐个图表改。
事件收口没做全,需要在对象库里补事件控制消息索引,把改动广播到各图表绑定对象。
可以,小布能按品种绑定关系比对对象属性,发现跨图表不一致直接提示你哪张图哪根线变了。
日志里按品种名和对象ID做前缀输出,先筛品种再盯具体对象,比盲翻全文快得多。
多半是每查一个属性就重绘一次,改成先收完差异再统一打印,能少掉大半无效刷新。