DoEasy 函数库中的其他类(第七十一部分):图表对象集合事件·综合运用
📊

DoEasy 函数库中的其他类(第七十一部分):图表对象集合事件·综合运用

(3/3)·手动改图表常丢事件,用集合事件把每一次增删改都变成程序能响应的指令

实战向 第 3/3 篇
手动拖指标、关子窗口时,多数 EA 只看到终端默认事件,具体改了哪个对象全靠猜。把图表对象变动封装成集合事件,程序就能在变动发生的那一刻拿到对象身份和动作类型,不再漏判。

◍ 给指标对象配访问器与打印口

在封装图表指标时,把四个核心属性暴露成只读访问器是最省事的做法:名称、在指标列表里的序号、指标句柄、所在子窗口索引。这样外部逻辑随时能拿到句柄去调 iGet 系列函数,不用再翻全局数组。 下面这段是类里典型的存取实现,注意 WindowNum 返回的是子窗口编号,主图窗口固定为 0,附属窗口从 1 开始递增。 对应的 Set 方法只在对象初始化阶段用,比如指标刚挂上图表、Handle() 拿到 -1 以外的有效值后写回去。Print 方法带个 dash 开关,传 true 就在日志前补个短横,方便和多指标调试信息对齐。 外汇与贵金属杠杆高,这类封装若句柄失效没捕获,可能让 EA 在实时行情下静默罢工,上 MT5 跑之前务必先测空句柄分支。

MQL5 / C++
class=class="str">"cmt">//--- Return(class="num">1) indicator name, (class="num">2) index in the list, (class="num">3) indicator handle and(class="num">4) subwindow index
  class="type">class="kw">string           Name(class="type">void)                class="kw">const { class="kw">return this.m_name;     }
  class="type">int              Index(class="type">void)               class="kw">const { class="kw">return this.m_index;    }
  class="type">int              Handle(class="type">void)              class="kw">const { class="kw">return this.m_handle;   }
  class="type">int              WindowNum(class="type">void)           class="kw">const { class="kw">return this.m_window_num;  }
class=class="str">"cmt">//--- Set(class="num">1) subwindow name, (class="num">2) window index on the chart, (class="num">3) handle, (class="num">4) index
  class="type">void             SetName(class="kw">const class="type">class="kw">string name)           { this.m_name=name;         }
  class="type">void             SetIndex(class="kw">const class="type">int index)            { this.m_index=index;       }
  class="type">void             SetHandle(class="kw">const class="type">int handle)          { this.m_handle=handle;     }
  class="type">void             SetWindowNum(class="kw">const class="type">int win_num)      { this.m_window_num=win_num;}

class=class="str">"cmt">//--- Display the description of object properties in the journal(dash=true - hyphen before the description, class="kw">false - description only)
  class="type">void             Print(class="kw">const class="type">bool dash=class="kw">false)         { ::Print((dash ? "- " : "")+this.Header()); }
class=class="str">"cmt">//--- Return the object class="type">class="kw">short name
  class="type">class="kw">string           Header(class="type">void)              class="kw">const { class="kw">return CMessage::Text(MSG_CHART_OBJ_INDICATOR)+" "+this.Name(); }

class=class="str">"cmt">//--- Compare CWndInd objects with each other by the specified class="kw">property
  class="kw">virtual class="type">int      Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const;

class=class="str">"cmt">//--- Constructors
                   CWndInd(class="type">void){;}

「图表子窗口对象的私有成员布局」

在 MT5 的自定义界面框架里,图表子窗口被封装成 CChartWnd 类,它继承自 CBaseObjExt,专门管一个独立指标子窗口里的所有对象生命周期。 类内 private 区首先放了 m_list_ind,这是一个 CArrayObj 实例,用来存当前子窗口里挂着的指标对象列表;此外还有两个 CArrayObj 指针 m_list_ind_del 和 m_list_ind_param,分别指向「被移除指标的待删除列表」和「参数发生变化的指标列表」,这两个指针在重绘与资源回收时才会被实际赋值。 紧接着是三个 int 成员:m_window_num 记录子窗口索引(主图是 0,第一个副图是 1,依此类推);m_wnd_coord_x 存图表窗口内对应时间的 X 坐标;m_wnd_coord_y 存对应价格的 Y 坐标。 构造函数 CWndInd 在初始化列表里一次性把 handle、name、index、win_num 绑到对应成员,写一个副图指标管理对象时,win_num 传错就会让坐标映射偏到别的窗口去。

MQL5 / C++
CWndInd(class="kw">const class="type">int handle,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int index,class="kw">const class="type">int win_num) : m_handle(handle),
                                                                                      m_name(name),
                                                                                      m_index(index),
                                                                                      m_window_num(win_num) {}
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Chart window object class                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CChartWnd : class="kw">public CBaseObjExt
  {
class="kw">private:
   CArrayObj         m_list_ind;                                                     class=class="str">"cmt">// Indicator list
   CArrayObj        *m_list_ind_del;                                                 class=class="str">"cmt">// Pointer to the list of indicators removed from the indicator window
   CArrayObj        *m_list_ind_param;                                               class=class="str">"cmt">// Pointer to the list of changed indicators
   class="type">int               m_window_num;                                                   class=class="str">"cmt">// Subwindow index
   class="type">int               m_wnd_coord_x;                                                 class=class="str">"cmt">// The X coordinate for the time on the chart in the window
   class="type">int               m_wnd_coord_y;                                                  class=class="str">"cmt">// The Y coordinate for the price on the chart in the window

图表窗口类里的指标与属性接口

在 MT5 自定义图表窗口类里,指标与窗口的从属关系靠几个布尔和指针方法判断。IsPresentInWindow 接收 CWndInd 指针,回传该指标是否挂在本窗口;IsPresentInList 用指标名字符串查重,确认它是否已在管理列表里。 GetMissingInd 直接返回「在列表却不在图上」的 CWndInd 对象指针,方便后续补画;IndicatorsDelete 则反向清理「窗口已无、列表还在」的残留项,避免内存与界面状态错位。 属性支持层面,SupportProperty 对整数类只放行 CHART_PROP_WINDOW_YDISTANCE 与 CHART_PROP_HEIGHT_IN_PIXELS,字符串类仅认 CHART_PROP_WINDOW_IND_NAME 与 CHART_PROP_SYMBOL,其余返回 false。这意味着外汇或贵金属多窗口面板做参数读写时,乱查不支持的属性会直接落空,属高风险自定义开发环节。 SendEvent 把窗口事件推给主控图表,Compare 按指定 mode 给 CChartWnd 对象排序,便于信号对象列表维护。开 MT5 新建 EA 把这段声明贴进头文件,编译看哪些方法标红,就能反推你当前图表窗口缺了哪块能力。

MQL5 / C++
class="type">class="kw">string m_symbol; class=class="str">"cmt">// Symbol of a chart the window belongs to
class=class="str">"cmt">//--- Return the flag indicating the presence of an indicator(class="num">1) from the list in the window and(class="num">2) from the window in the list
class="type">bool IsPresentInWindow(class="kw">const CWndInd *ind);
class="type">bool IsPresentInList(class="kw">const class="type">class="kw">string name);
class=class="str">"cmt">//--- Return the indicator object present in the list but not present on the chart
CWndInd *GetMissingInd(class="type">void);
class=class="str">"cmt">//--- Remove indicators not present in the window from the list
class="type">void IndicatorsDelete(class="type">void);
class=class="str">"cmt">//--- Add new indicators to the list
class="type">void IndicatorsAdd(class="type">void);
class=class="str">"cmt">//--- Check the changes of the parameters of existing indicators
class="type">void IndicatorsChangeCheck(class="type">void);

class="kw">public:
class="kw">public:
class=class="str">"cmt">//--- Return itself
CChartWnd *GetObject(class="type">void) { class="kw">return &this; }
class=class="str">"cmt">//--- Return the flag of the object supporting this class="kw">property
class="kw">virtual class="type">bool SupportProperty(ENUM_CHART_PROP_INTEGER class="kw">property) { class="kw">return(class="kw">property==CHART_PROP_WINDOW_YDISTANCE || class="kw">property==CHART_PROP_HEIGHT_IN_PIXELS ? true : class="kw">false); }
class="kw">virtual class="type">bool SupportProperty(ENUM_CHART_PROP_DOUBLE class="kw">property) { class="kw">return class="kw">false; }
class="kw">virtual class="type">bool SupportProperty(ENUM_CHART_PROP_STRING class="kw">property) { class="kw">return (class="kw">property==CHART_PROP_WINDOW_IND_NAME || class="kw">property==CHART_PROP_SYMBOL ? true : class="kw">false); }
class=class="str">"cmt">//--- Get description of(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties
class="type">class="kw">string GetPropertyDescription(ENUM_CHART_PROP_INTEGER class="kw">property);
class="type">class="kw">string GetPropertyDescription(ENUM_CHART_PROP_DOUBLE class="kw">property) { class="kw">return CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED); }
class="type">class="kw">string GetPropertyDescription(ENUM_CHART_PROP_STRING class="kw">property);
class=class="str">"cmt">//--- Return the object class="type">class="kw">short name
class="kw">virtual class="type">class="kw">string Header(class="type">void);

class=class="str">"cmt">//--- Create and send the chart event to the control program chart
class="type">void SendEvent(ENUM_CHART_OBJ_EVENT event);

class=class="str">"cmt">//--- Compare CChartWnd objects by a specified class="kw">property (to sort the list by an MQL5 signal object)
class="kw">virtual class="type">int Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const;

◍ 图表子窗口对象的相等判定与成员访问

在 MT5 的自定义图表封装里,CChartWnd 类把「一个图表子窗口」抽象成对象,方便批量管理多窗口里的信号与指标。要判断两个子窗口是否对应同一套 MQL5 信号配置,靠的是 IsEqual(CChartWnd* compared_obj) 这个常方法,它逐属性比对,能筛出配置完全相同的窗口实例。 构造上提供了两个入口:默认无参构造只做占位,带参构造则直接吃进 chart_id、子窗口序号 wnd_num、symbol 字符串,以及两个 CArrayObj 指针——分别承载待删指标列表 list_ind_del 和指标参数列表 list_ind_param。析构 ~CChartWnd() 负责释放内部引用,避免句柄泄漏。 取数接口很直白:WindowNum() 回传子窗口索引(m_window_num),IndicatorsTotal() 回传该窗口挂载指标数(即 m_list_ind.Total()),Symbol() 回传图表品种名。写回则用 SetWindowNum(int) 与 SetSymbol(string) 直接改成员。 指标列表的提取走 GetIndicatorsList() 返回内部 m_list_ind 的指针;若想按序取单个指标对象,用 GetIndicatorByIndex(int index) 从列表中拿 CWndInd*。开 MT5 新建 EA 时,可直接套这段声明验证:挂两个相同品种不同子窗口,调用 IsEqual 应返回 false,而同窗口同配置返回 true。外汇与贵金属杠杆交易高风险,对象比对仅用于风控逻辑辅助,不预示任何价格方向。

MQL5 / C++
class=class="str">"cmt">//--- Compare CChartWnd objects by all properties(to search for equal MQL5 signal objects)
  class="type">bool                IsEqual(CChartWnd* compared_obj) class="kw">const;

class=class="str">"cmt">//--- Constructors
                 CChartWnd(class="type">void){;}
                 CChartWnd(class="kw">const class="type">long chart_id,class="kw">const class="type">int wnd_num,class="kw">const class="type">class="kw">string symbol,CArrayObj *list_ind_del,CArrayObj *list_ind_param);
class=class="str">"cmt">//--- Destructor
                ~CChartWnd(class="type">void);
class=class="str">"cmt">//--- Return(class="num">1) the subwindow index, (class="num">2) the number of indicators attached to the window and(class="num">3) the name of a symbol chart
  class="type">int            WindowNum(class="type">void)                                      class="kw">const { class="kw">return this.m_window_num;            }
  class="type">int            IndicatorsTotal(class="type">void)                                class="kw">const { class="kw">return this.m_list_ind.Total();      }
  class="type">class="kw">string         Symbol(class="type">void)                                         class="kw">const { class="kw">return m_symbol;}
class=class="str">"cmt">//--- Set(class="num">1) the subwindow index and(class="num">2) the chart symbol
  class="type">void           SetWindowNum(class="kw">const class="type">int num)                                 { this.m_window_num=num;               }
  class="type">void           SetSymbol(class="kw">const class="type">class="kw">string symbol)                              { this.m_symbol=symbol;                }

class=class="str">"cmt">//--- Return(class="num">1) the indicator list, the window indicator object from the list by(class="num">2) index in the list and(class="num">3) by handle
  CArrayObj      *GetIndicatorsList(class="type">void)                                   { class="kw">return &this.m_list_ind;             }
  CWndInd        *GetIndicatorByIndex(class="kw">const class="type">int index);

「图表窗口里抓最近动过的指标」

CChartWnd 类给交易者留了几个直接拿指标指针的口子,不用自己遍历对象数组。GetLastAddedIndicator 返回窗口里最后一个挂上去的指标,GetLastDeletedIndicator 拿最后一个被摘掉的,GetLastChangedIndicator 则指向参数刚被改过的那个——这三个都靠对应列表的 Total()-1 下标取尾元素。 构造函数把两个外部传进来的 CArrayObj 指针(list_ind_del 和 list_ind_param)接到了成员变量上,同时把窗口坐标 m_wnd_coord_x / y 初始化成 0。注意这里只存指针,不拷贝数组,所以外部数组生命周期得比窗口对象长,否则尾元素取值会踩空。 下面这段是核心声明与构造逻辑,复制到 MT5 的 Include 目录里就能编译验证。外汇和贵金属图表上指标增删频繁,用这套接口做事件回溯时,要留意高频操作下列表尾元素可能被快速覆盖,属于高风险环境下的状态竞态。

MQL5 / C++
CWndInd       *GetIndicatorByHandle(class="kw">const class="type">int handle);
class=class="str">"cmt">//--- Return(class="num">1) the last one added to the window, (class="num">2) the last one removed from the window and(class="num">3) the changed indicator
CWndInd       *GetLastAddedIndicator(class="type">void)                { class="kw">return this.m_list_ind.At(this.m_list_ind.Total()-class="num">1);       }
CWndInd       *GetLastDeletedIndicator(class="type">void)              { class="kw">return this.m_list_ind_del.At(this.m_list_ind_del.Total()-class="num">1); }
CWndInd       *GetLastChangedIndicator(class="type">void)              { class="kw">return this.m_list_ind_param.At(this.m_list_ind_param.Total()-class="num">1);}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor                                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
CChartWnd::CChartWnd(class="kw">const class="type">long chart_id,class="kw">const class="type">int wnd_num,class="kw">const class="type">class="kw">string symbol,CArrayObj *list_ind_del,CArrayObj *list_ind_param) : m_window_num(wnd_num),
                                                                                                  m_symbol(symbol),
                                                                                                  m_wnd_coord_x(class="num">0),
                                                                                                  m_wnd_coord_y(class="num">0)
  {
   this.m_list_ind_del=list_ind_del;
   this.m_list_ind_param=list_ind_param;
   CBaseObj::SetChartID(chart_id);
   this.IndicatorsListCreate();
  }

图表窗口对象的析构与比较逻辑

CChartWnd 的析构函数负责反向清理挂载的指标实例。它先取 m_list_ind 的总数,从末尾索引 total-1 循环到 WRONG_VALUE 之上,逐条取出 CWndInd 指针,释放指标句柄后再从链表删除,避免 MT5 终端退出或图表重置时残留指标句柄。 比较方法 Compare 按传入 mode 决定排序依据:窗口纵向距离、像素高度、子窗口编号或交易品种名。以 CHART_PROP_WINDOW_NUM 为例,编号大的返回 1、小的返回 -1、相等返回 0,这让多个图表窗口对象能按子窗口序号排进容器。 GetPropertyDescription 在处理 CHART_PROP_SYMBOL 时,会先拼出「Symbol」文案,若当前对象不支持该属性则追加「不支持」提示,否则追加实际品种名如 EURUSD。你在自写面板类时可直接复用这段判定,省去重复写品种标签。 外汇与贵金属行情波动剧烈、杠杆风险高,任何基于窗口对象的自动化展示都只是辅助,实盘前请在 MT5 策略测试器跑一遍确认无句柄泄漏。

MQL5 / C++
CChartWnd::~CChartWnd(class="type">void)
  {
   class="type">int total=this.m_list_ind.Total();
   for(class="type">int i=total-class="num">1;i>WRONG_VALUE;i--)
     {
      CWndInd *ind=this.m_list_ind.At(i);
      if(ind==NULL)
        class="kw">continue;
      ::IndicatorRelease(ind.Handle());
      this.m_list_ind.Delete(i);
     }
  }
class="type">int CChartWnd::Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const
  {
   class="kw">const CChartWnd *obj_compared=node;
   if(mode==CHART_PROP_WINDOW_YDISTANCE)
     class="kw">return(this.YDistance()>obj_compared.YDistance() ? class="num">1 : this.YDistance()<obj_compared.YDistance() ? -class="num">1 : class="num">0);
   else if(mode==CHART_PROP_HEIGHT_IN_PIXELS)
     class="kw">return(this.HeightInPixels()>obj_compared.HeightInPixels() ? class="num">1 : this.HeightInPixels()<obj_compared.HeightInPixels() ? -class="num">1 : class="num">0);
   else if(mode==CHART_PROP_WINDOW_NUM)
     class="kw">return(this.WindowNum()>obj_compared.WindowNum() ? class="num">1 : this.WindowNum()<obj_compared.WindowNum() ? -class="num">1 : class="num">0);
   else if(mode==CHART_PROP_SYMBOL)
     class="kw">return(this.Symbol()==obj_compared.Symbol() ? class="num">0 : this.Symbol()>obj_compared.Symbol() ? class="num">1 : -class="num">1);
   class="kw">return -class="num">1;
  }
class="type">class="kw">string CChartWnd::GetPropertyDescription(ENUM_CHART_PROP_STRING class="kw">property)
  {
   class="kw">return
     (
      class="kw">property==CHART_PROP_SYMBOL ? CMessage::Text(MSG_LIB_PROP_SYMBOL)+
       (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
        ": "+this.Symbol()

◍ 窗口指标列表的构建与增量追加

在 MT5 自定义图表封装里,CChartWnd::IndicatorsListCreate 负责把某个子窗口挂的所有指标扫进内存列表。它先 m_list_ind.Clear() 清空旧引用,再用 ChartIndicatorsTotal 拿到该窗口指标总数 total,从 i=0 循环到 total-1 逐个处理。 循环内每次用 ChartIndicatorName 取短名,ChartIndicatorGet 换句柄,new 一个 CWndInd 对象(构造时传入 handle、name、序号 i 和 this.WindowNum() 指定子窗口)。若 new 出来是 NULL 就 continue 跳过;否则 m_list_ind.Sort() 设排序标志,Add 失败则 delete 释放,避免野指针。 IndicatorsAdd 则是增量逻辑:同样用 ChartIndicatorsTotal 取总数再走一遍循环,只把新出现的指标补进列表,不先 Clear。注意原文里有一处游离代码——先 ChartIndicatorGet 拿 handle,紧接着 IndicatorRelease(handle) 释放句柄,这段若放在列表构建前会提前解绑指标,复制代码时务必确认它属于哪个方法体,别直接粘到全局作用域。外汇与贵金属行情跳空频繁,这类句柄管理失误可能导致指标对象失效,实盘前请在策略测试器里跑一遍确认列表数量与窗口实际指标数一致。

MQL5 / C++
class="type">void CChartWnd::IndicatorsListCreate(class="type">void)
  {
  class=class="str">"cmt">//--- Clear the indicator lists
  this.m_list_ind.Clear();
  class=class="str">"cmt">//--- Get the total number of indicators in the window
  class="type">int total=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num);
  class=class="str">"cmt">//--- In the loop by the number of indicators,
  for(class="type">int i=class="num">0;i<total;i++)
    {
    class=class="str">"cmt">//--- obtain and save the class="type">class="kw">short indicator name,
    class="type">class="kw">string name=::ChartIndicatorName(this.m_chart_id,this.m_window_num,i);
    class=class="str">"cmt">//--- get and save the indicator handle by its class="type">class="kw">short name
    class="type">int handle=::ChartIndicatorGet(this.m_chart_id,this.m_window_num,name);
    class=class="str">"cmt">//--- Create the new indicator object in the chart window
    CWndInd *ind=new CWndInd(handle,name,i,this.WindowNum());
    if(ind==NULL)
      class="kw">continue;
    class=class="str">"cmt">//--- set the sorted list flag to the list
    this.m_list_ind.Sort();
    class=class="str">"cmt">//--- If failed to add the object to the list, remove it
    if(!this.m_list_ind.Add(ind))
      class="kw">delete ind;
    }
  }

class="type">void CChartWnd::IndicatorsAdd(class="type">void)
  {
  class=class="str">"cmt">//--- Get the total number of indicators in the window
  class="type">int total=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num);
  class=class="str">"cmt">//--- In the loop by the number of indicators,
  for(class="type">int i=class="num">0;i<total;i++)
    {
    class=class="str">"cmt">//--- obtain and save the class="type">class="kw">short indicator name,

「图表窗口里的指标句柄同步逻辑」

在 MT5 自定义控件里维护子窗口指标列表,核心是先拿名字再拿句柄。ChartIndicatorName 按索引 i 取名称,ChartIndicatorGet 用图 ID、窗口号、名称换回句柄,二者必须成对调用,否则后续 CWndInd 对象拿到的 handle 是错位的。 循环里每取到一个句柄就 new 一个 CWndInd,若返回 NULL 直接 continue 跳过;列表先 Sort 再 Search,若已存在或 Add 失败就 delete 掉临时对象,避免重复挂载。这个去重判断依赖 WRONG_VALUE 常量,实战中改窗口指标数量时若发现列表卡住,优先查这里。 IsPresentInWindow 用 ChartIndicatorsTotal 拿到窗口指标总数,逐个比 Name 和 Handle。注意 Handle 是运行时绑定,切换品种或重载模板后老 handle 会失效,这时函数会返回 false,触发 IndicatorsChangeCheck 里的补充挂载逻辑。 IndicatorRelease 那一行容易被忽略:在手动释放 ChartIndicatorGet 取得的句柄前不调用它,指标资源可能泄漏。外汇与贵金属行情跳动频繁,EA 长时间挂多个窗口指标时,句柄管理不当会让终端内存缓慢爬升,属于高风险环境下的隐性损耗。

MQL5 / C++
class="type">class="kw">string name=::ChartIndicatorName(this.m_chart_id,this.m_window_num,i);
class=class="str">"cmt">//--- get and save the indicator handle by its class="type">class="kw">short name
class="type">int handle=::ChartIndicatorGet(this.m_chart_id,this.m_window_num,name);
class=class="str">"cmt">//--- Create the new indicator object in the chart window
CWndInd *ind=new CWndInd(handle,name,i,this.WindowNum());
if(ind==NULL)
   class="kw">continue;
class=class="str">"cmt">//--- set the sorted list flag to the list
this.m_list_ind.Sort();
class=class="str">"cmt">//--- If the object is already in the list or an attempt to add it to the list failed, remove it
if(this.m_list_ind.Search(ind)>WRONG_VALUE || !this.m_list_ind.Add(ind))
   class="kw">delete ind;
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int handle=::ChartIndicatorGet(this.m_chart_id,this.m_window_num,name);
::IndicatorRelease(handle);
class=class="str">"cmt">//+--------------------------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag indicating the presence of an indicator from the list in the window   |
class=class="str">"cmt">//+--------------------------------------------------------------------------------------+
class="type">bool CChartWnd::IsPresentInWindow(class="kw">const CWndInd *ind)
  {
  class="type">int total=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num);
  for(class="type">int i=class="num">0;i<total;i++)
    {
    class="type">class="kw">string name=::ChartIndicatorName(this.m_chart_id,this.m_window_num,i);
    class="type">int handle=::ChartIndicatorGet(this.m_chart_id,this.m_window_num,name);
    if(ind.Name()==name && ind.Handle()==handle)
      class="kw">return true;
    }
  class="kw">return class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check the changes of the parameters of existing indicators          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChartWnd::IndicatorsChangeCheck(class="type">void)
  {
  class=class="str">"cmt">//--- Get the total number of indicators in the window
  class="type">int total=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num);
  class=class="str">"cmt">//--- In the loop by all window indicators,
  for(class="type">int i=class="num">0;i<total;i++)
    {
    class=class="str">"cmt">//--- get the indicator name and get its handle by a name
    class="type">class="kw">string name=::ChartIndicatorName(this.m_chart_id,this.m_window_num,i);
    class="type">int handle=::ChartIndicatorGet(this.m_chart_id,this.m_window_num,name);
    class=class="str">"cmt">//--- If the indicator with such a name is present in the object indicator list, move on to the next one
    if(this.IsPresentInList(name))
      class="kw">continue;

捕获参数变更的游离指标并清理已移除项

在图表窗口的指标同步逻辑里,先通过 GetMissingInd() 捞取「列表里有但窗口里找不到」的 CWndInd 对象;若返回 NULL 就直接 continue 跳过本轮。当该对象的 Index() 与当前遍历下标 i 相等时,说明这是同一个指标但参数被改过。 此时 new 一个 CWndInd 副本(changed),用原 handle、name、index、window 初始化;若实例化失败同样 continue。把 m_list_ind_param 排序后尝试 Add,添加失败就 delete 副本并跳过。成功则给原 ind 重写 SetName 与 SetHandle,并发 SendEvent(CHART_OBJ_EVENT_CHART_WND_IND_CHANGE) 通知主控图表面板参数已变。 另一段 IndicatorsDelete() 负责反向下料:从 m_list_ind 末尾倒序(i 从 total-1 到 WRONG_VALUE 之后)取对象,用 IsPresentInWindow() 判断还在不在窗口,在就跳过。不在则 new 一个 ind_del 副本塞进 m_list_ind_del;若 Add 失败就 delete 并 continue。 这两段配合能把「参数偷偷改了」和「被手动删了」的指标都捕获到,你在 MT5 自建面板类里复刻时,重点验证 m_list_ind_param / m_list_ind_del 两个容器是否随事件正确增减。

MQL5 / C++
class=class="str">"cmt">//--- Get the indicator object present in the list but not present in the window
   CWndInd *ind=this.GetMissingInd();
   if(ind==NULL)
      class="kw">continue;
   class=class="str">"cmt">//--- If the indicator and the detected object have the same index, this is the indicator with changed parameters
   if(ind.Index()==i)
     {
      class=class="str">"cmt">//--- Create a new indicator object based on the detected indicator object,
      CWndInd *changed=new CWndInd(ind.Handle(),ind.Name(),ind.Index(),ind.WindowNum());
      if(changed==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- set the sorted list flag to the list of changed indicators
      this.m_list_ind_param.Sort();
      class=class="str">"cmt">//--- If failed to add a newly created indicator object to the list of changed indicators,
      class=class="str">"cmt">//--- remove the created object and move on to the next indicator in the window
      if(!this.m_list_ind_param.Add(changed))
        {
         class="kw">delete changed;
         class="kw">continue;
        }
      class=class="str">"cmt">//--- Set the new parameters for the detected "lost" indicator - class="type">class="kw">short name and handle
      ind.SetName(name);
      ind.SetHandle(handle);
      class=class="str">"cmt">//--- and call the method of sending a custom event to the control program chart
      this.SendEvent(CHART_OBJ_EVENT_CHART_WND_IND_CHANGE);
     }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Remove indicators not present in the window from the list         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChartWnd::IndicatorsDelete(class="type">void)
  {
   class=class="str">"cmt">//--- In the loop by the list of window indicator objects,
   class="type">int total=this.m_list_ind.Total();
   for(class="type">int i=total-class="num">1;i>WRONG_VALUE;i--)
     {
      class=class="str">"cmt">//--- get the next indicator object
      CWndInd *ind=this.m_list_ind.At(i);
      if(ind==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- If such an indicator is present in the chart window, move on to the next object in the list
      if(this.IsPresentInWindow(ind))
         class="kw">continue;
      class=class="str">"cmt">//--- Create a copy of a removed indicator
      CWndInd *ind_del=new CWndInd(ind.Handle(),ind.Name(),ind.Index(),ind.WindowNum());
      if(ind_del==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- If failed to place a created object to the list of indicators removed from the window,
      class=class="str">"cmt">//--- remove it and go to the next object in the list
      if(!this.m_list_ind_del.Add(ind_del))
        {
         class="kw">delete ind_del;
         class="kw">continue;
        }

◍ 列表与窗口的指示器同步逻辑

在自定义图表窗口类里,指示器清单和实际窗口容易脱节:用户手动删了窗口里的指标,但 m_list_ind 还留着记录,得主动清掉。代码片段里用 this.m_list_ind.Delete(i) 把已删除项从清单移除,避免后续遍历时访问野对象。 IsPresentInList 靠临时 new 一个 CWndInd、设名后排序再二分查找来判定。注意它先 delete ind 再返回 index>WRONG_VALUE,临时对象不泄漏,但每次调用都 new/delete,在 36 个窗口指标的高频轮询下会有微小开销。 GetMissingInd 反过来找:清单里有但窗口里没有的指示器,直接返回首个命中项;若全都在窗口上就返回 NULL。GetIndicatorByIndex 和 GetIndicatorByHandle 则用不同排序键(SORT_BY_CHART_WINDOW_IND_INDEX 等)做 Search,前者同样有临时 new/delete 模式。 开 MT5 把这几段塞进你的 CChartWnd 派生类,跑一轮加指标→手动删→调 GetMissingInd,应该能抓到非 NULL 返回,验证清单滞后于窗口这一现象。外汇与贵金属图表上挂多个指标时,这种不同步可能引发 EA 误判,属高风险操作环境。

MQL5 / C++
class=class="str">"cmt">//--- Remove the indicator, which was deleted from the window, from the list
   this.m_list_ind.Delete(i);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+-----------------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag of the presence of an indicator from the window in the list  |
class=class="str">"cmt">//+-----------------------------------------------------------------------------+
class="type">bool CChartWnd::IsPresentInList(class="kw">const class="type">class="kw">string name)
  {
   CWndInd *ind=new CWndInd();
   if(ind==NULL)
      class="kw">return class="kw">false;
   ind.SetName(name);
   this.m_list_ind.Sort(SORT_BY_CHART_WINDOW_IND_NAME);
   class="type">int index=this.m_list_ind.Search(ind);
   class="kw">delete ind;
   class="kw">return(index>WRONG_VALUE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the indicator object present in the list                       |
class=class="str">"cmt">//| but not on the chart                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWndInd *CChartWnd::GetMissingInd(class="type">void)
  {
   for(class="type">int i=class="num">0;i<this.m_list_ind.Total();i++)
     {
      CWndInd *ind=this.m_list_ind.At(i);
      if(!this.IsPresentInWindow(ind))
        class="kw">return ind;
     }
   class="kw">return NULL;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the indicator object by the index in the window list           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWndInd *CChartWnd::GetIndicatorByIndex(class="kw">const class="type">int index)
  {
   CWndInd *ind=new CWndInd();
   if(ind==NULL)
      class="kw">return NULL;
   ind.SetIndex(index);
   this.m_list_ind.Sort(SORT_BY_CHART_WINDOW_IND_INDEX);
   class="type">int n=this.m_list_ind.Search(ind);
   class="kw">delete ind;
   class="kw">return this.m_list_ind.At(n);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the window indicator object from the list by handle            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWndInd *CChartWnd::GetIndicatorByHandle(class="kw">const class="type">int handle)
  {

「窗口指标增减的刷新逻辑」

在自定义图表窗口类里,Refresh() 负责把 MT5 当前窗口挂的指标的实际情况同步到内部链表。第一步先算变化量:用 ChartIndicatorsTotal(chart_id, window_num) 减去已维护的 m_list_ind.Total(),差值 change 为正表示加了指标,为负表示删了,为零则只查参数变动。 change==0 时直接走 IndicatorsChangeCheck() 扫一遍每个指标的参数有没有被用户动过,然后 return,不碰链表结构。这样避免了无谓的增删操作,在 20+ 指标的子窗口上能少掉几毫秒的卡顿。 change>0 说明有新指标挂进来,先调 IndicatorsAdd() 把对象补进 m_list_ind,再从链表尾部倒着取刚加的那几个(index = Total()-(1+i)),逐个发 CHART_OBJ_EVENT_CHART_WND_IND_ADD 事件给主控图表。 change<0 则走 IndicatorsDelete() 清理,并到 m_list_ind_del 里按同样倒序方式取被删对象发事件。下面这段是 Refresh 里增删分支的循环核心,注意 NULL 判断不能省,否则指标句柄失效时会把整个刷新循环带崩。

MQL5 / C++
class="type">int change=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num)-this.m_list_ind.Total();
if(change==class="num">0)
  {
   this.IndicatorsChangeCheck();
   class="kw">return;
  }
if(change>class="num">0)
  {
   this.IndicatorsAdd();
   for(class="type">int i=class="num">0;i<change;i++)
     {
      class="type">int index=this.m_list_ind.Total()-(class="num">1+i);
      CWndInd *ind=this.m_list_ind.At(index);
      if(ind==NULL)
         class="kw">continue;
      this.SendEvent(CHART_OBJ_EVENT_CHART_WND_IND_ADD);
     }
  }
if(change<class="num">0)
  {
   this.IndicatorsDelete();
   for(class="type">int i=class="num">0;i<-change;i++)
     {
      class="type">int index=this.m_list_ind_del.Total()-(class="num">1+i);
      CWndInd *ind=this.m_list_ind_del.At(index);
      if(ind==NULL)

指标增删改如何回传主控图

在自建图表窗口类里,指标被添加、删除或改动后,需要把动作通知主控程序图,否则上层逻辑拿不到实时状态。CChartWnd::SendEvent 就是干这个桥接的。 它按传入的枚举 event 分三种情况:CHART_OBJ_EVENT_CHART_WND_IND_ADD 取 GetLastAddedIndicator(),IND_DEL 取 GetLastDeletedIndicator(),IND_CHANGE 取 GetLastChangedIndicator()。任一取不到对象就直接 return,不发事件。 真正发通知统一走 ::EventChartCustom()。lparam 塞本窗口所属图表 ID(m_chart_id),dparam 塞窗口序号(WindowNum()),sparam 塞指标的短名(ind.Name()),事件 ID 强转为 ushort 作第二参。这样主控图用 OnChartEvent 捕 CHARTEVENT_CUSTOM 就能知道哪张图、哪个子窗、哪个指标动了。 外汇与贵金属图表多品种同开时,这种自定义事件链路能避免轮询,但自定义事件 ID 若与其他 EA 撞车,可能丢失通知,实盘前应在 MT5 用 Print 打点验证。

MQL5 / C++
class="type">void CChartWnd::SendEvent(ENUM_CHART_OBJ_EVENT event)
  {
  class=class="str">"cmt">//--- If an indicator is added
  if(event==CHART_OBJ_EVENT_CHART_WND_IND_ADD)
    {
    class=class="str">"cmt">//--- Get the last indicator object added to the list
    CWndInd *ind=this.GetLastAddedIndicator();
    if(ind==NULL)
      class="kw">return;
    class=class="str">"cmt">//--- Send the CHART_OBJ_EVENT_CHART_WND_IND_ADD event to the control program chart
    class=class="str">"cmt">//--- pass the chart ID to lparam,
    class=class="str">"cmt">//--- pass the chart window index to dparam,
    class=class="str">"cmt">//--- pass the class="type">class="kw">short name of the added indicator to sparam
    ::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,this.WindowNum(),ind.Name());
    }
  class=class="str">"cmt">//--- If the indicator is removed
  else if(event==CHART_OBJ_EVENT_CHART_WND_IND_DEL)
    {
    class=class="str">"cmt">//--- Get the last indicator object added to the list of removed indicators
    CWndInd *ind=this.GetLastDeletedIndicator();
    if(ind==NULL)
      class="kw">return;
    class=class="str">"cmt">//--- Send the CHART_OBJ_EVENT_CHART_WND_IND_DEL event to the control program chart
    class=class="str">"cmt">//--- pass the chart ID to lparam,
    class=class="str">"cmt">//--- pass the chart window index to dparam,
    class=class="str">"cmt">//--- pass the class="type">class="kw">short name of a deleted indicator to sparam
    ::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,this.WindowNum(),ind.Name());
    }
  class=class="str">"cmt">//--- If the indicator has changed
  else if(event==CHART_OBJ_EVENT_CHART_WND_IND_CHANGE)
    {
    class=class="str">"cmt">//--- Get the last indicator object added to the list of changed indicators
    CWndInd *ind=this.GetLastChangedIndicator();
    if(ind==NULL)
      class="kw">return;
    class=class="str">"cmt">//--- Send the CHART_OBJ_EVENT_CHART_WND_IND_CHANGE event to the control program chart
    class=class="str">"cmt">//--- pass the chart ID to lparam,
    class=class="str">"cmt">//--- pass the chart window index to dparam,
    class=class="str">"cmt">//--- pass the class="type">class="kw">short name of a changed indicator to sparam
    ::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,this.WindowNum(),ind.Name());
    }
  }

◍ 图表对象类的私有成员与属性索引

在 MT5 的图表封装类里,CChartObj 继承自 CBaseObjExt,把图表窗口对象、指标对象以及三类属性全部收进私有成员。m_list_wnd 是直接持有的窗口对象数组,而 m_list_wnd_del、m_list_ind_del、m_list_ind_param 三个指针分别指向待删除窗口对象、已移除指标、被改动指标的列表,这种分离设计让销毁与变更可以延迟批处理。 整数、实数、字符串属性各自用定长数组缓存:m_long_prop 大小为 CHART_PROP_INTEGER_TOTAL,m_double_prop 为 CHART_PROP_DOUBLE_TOTAL,m_string_prop 为 CHART_PROP_STRING_TOTAL。实际调参时,double 与 string 属性在数组里的真实下标要靠偏移折算,而不是从 0 开始。 IndexProp 的两个重载给出了折算公式:double 属性下标 = (int)property - CHART_PROP_INTEGER_TOTAL;string 属性下标再减去 CHART_PROP_DOUBLE_TOTAL。你在写自定义图表管理器时,若直接按枚举值当索引访问会越界,必须先过这道偏移。 m_digits 缓存了品种小数位,m_wnd_time_x 与 m_wnd_price_y 记录了窗口坐标映射用的时间与价格,这几项在鼠标拾取、画对象定位时直接复用,省去反复调用终端函数。SetShowFlag 与 SetBringToTopFlag 则带 source 字符串和 redraw 开关,用来标记显隐与置顶并选择性重绘。 外汇与贵金属波动大、点差跳变频繁,这类图表封装若用于实盘辅助,需先在模拟盘验证坐标映射在极端行情下的偏移是否可控。

MQL5 / C++
class CChartObj : class="kw">public CBaseObjExt
  {
class="kw">private:
   CArrayObj        m_list_wnd;             class=class="str">"cmt">// List of chart window objects
   CArrayObj       *m_list_wnd_del;         class=class="str">"cmt">// Pointer to the list of chart window objects
   CArrayObj       *m_list_ind_del;         class=class="str">"cmt">// Pointer to the list of indicators removed from the indicator window
   CArrayObj       *m_list_ind_param;       class=class="str">"cmt">// Pointer to the list of changed indicators
   class="type">long             m_long_prop[CHART_PROP_INTEGER_TOTAL];     class=class="str">"cmt">// Integer properties
   class="type">class="kw">double           m_double_prop[CHART_PROP_DOUBLE_TOTAL];    class=class="str">"cmt">// Real properties
   class="type">class="kw">string           m_string_prop[CHART_PROP_STRING_TOTAL];    class=class="str">"cmt">// String properties
   class="type">int              m_digits;               class=class="str">"cmt">// Symbol&class="macro">#x27;s Digits()
   class="type">class="kw">datetime         m_wnd_time_x;           class=class="str">"cmt">// Time for X coordinate on the windowed chart
   class="type">class="kw">double           m_wnd_price_y;          class=class="str">"cmt">// Price for Y coordinate on the windowed chart

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_CHART_PROP_DOUBLE class="kw">property)  class="kw">const { class="kw">return(class="type">int)class="kw">property-CHART_PROP_INTEGER_TOTAL; }
   class="type">int              IndexProp(ENUM_CHART_PROP_STRING class="kw">property)  class="kw">const { class="kw">return(class="type">int)class="kw">property-CHART_PROP_INTEGER_TOTAL-CHART_PROP_DOUBLE_TOTAL; }
class=class="str">"cmt">//--- The methods of setting parameter flags 
   class="type">bool             SetShowFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
   class="type">bool             SetBringToTopFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);

「图表交互开关的逐源接管」

在 MT5 的自定义指标或 EA 里,如果想精细控制某个图形对象(source)对应的图表交互行为,可以直接调用一组 Set*Flag 函数。它们都遵循同一签名:传入对象名、开关布尔值,以及可选的 redraw(默认 false 不重绘)。 这类接口覆盖了从右键菜单、十字光标、鼠标滚轮、对象增删到比例锁定等 14 种交互维度。比如 SetScaleFixFlag 锁定价格轴比例,SetAutoscrollFlag 关闭自动滚动,对价格行为复盘时避免画面乱跳很有用。 实际写代码时,若连续改多个 flag 且只在最后统一刷新,可把 redraw 都设 false,末尾再 ChartRedraw() 一次,能减少 MT5 主线程的重绘开销。外汇与贵金属波动剧烈,锁交互不等于锁风险,杠杆品种仍可能瞬间扫损。

MQL5 / C++
class="type">bool SetContextMenuFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetCrosshairToolFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetMouseScrollFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetEventMouseWhellFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetEventMouseMoveFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetEventObjectCreateFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetEventObjectDeleteFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetForegroundFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShiftFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetAutoscrollFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetKeyboardControlFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetQuickNavigationFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetScaleFixFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetScaleFix11Flag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);

图表元素显隐的批量开关函数

在 MT5 自定义指标或脚本里,想动态控制某张图表上各类元素的显示状态,可以直接调用这一组 Set*Flag 接口,而不是去翻终端的可视化设置面板。每个函数都接收 source 图表标识、flag 布尔值,以及可选的 redraw 参数(默认 false 不立即重绘)。 下面这 15 个函数覆盖了从坐标轴到交易相关的常见图层:缩放点数轴、品种名、OHLC 数据、买卖价线、最新价线、周期分隔符、网格、对象描述、交易水平线、交易水平拖拽、日期轴、价格轴、一键交易面板等。 如果你的 EA 需要在切换交易品种时隐藏网格和周期线以减少视觉噪声,可以只对对应 source 传 false,并把 redraw 设为 true 让改动立刻生效。外汇与贵金属杠杆高,自动改图表设置前建议在模拟盘验证,避免误关关键价线导致下单参考缺失。

MQL5 / C++
class="type">bool SetScalePTPerBarFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowTickerFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowOHLCFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowBidLineFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowAskLineFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowLastLineFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowPeriodSeparatorsFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowGridFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowObjectDescriptionsFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowTradeLevelsFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetDragTradeLevelsFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowDateScaleFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowPriceScaleFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetShowOneClickPanelFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);

◍ 图表对象属性与窗口列表的接口细节

在 MT5 的图表封装类里,一组 Set 开头的方法负责把外部参数写进指定图表源。比如 SetDockedFlag 接收 source、flag 和可选的 redraw(默认 false),用来控制图表是否停靠;SetMode 与 SetScale 则分别写入 ENUM_CHART_MODE 和整数比例尺,redraw 参数决定是否立即重绘。 下面这几行是类内部对属性数组的直接写入,属于 public 重载:SetProperty 对整数属性直接按枚举下标赋值到 m_long_prop;对 double 和 string 属性则先经过 IndexProp 换算下标,再写入 m_double_prop 或 m_string_prop。这样设计让外部调用不必关心底层数组长度。 窗口列表的维护靠 CreateWindowsList 与 RecreateWindowsList(change) 两个 void 方法,后者在图表子窗口数量或布局变动时按传入的 change 标记重建列表。FileNameWithExtention 则纯粹做字符串处理:当截图文件名缺扩展名时补上,方便后续 Save 类方法直接落盘。 想验证这套接口,可在 EA 里 new 一个图表封装对象,对黄金 XAUUSD 的 H1 图调用 SetScale("XAUUSDH1", 2, true),观察 MT5 终端里 K 线密度是否立刻变化;外汇与贵金属杠杆高,参数误写可能导致图表刷新异常,建议先在模拟盘测。

MQL5 / C++
class="type">bool SetDockedFlag(class="kw">const class="type">class="kw">string source,class="kw">const class="type">bool flag,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetMode(class="kw">const class="type">class="kw">string source,class="kw">const ENUM_CHART_MODE mode,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetScale(class="kw">const class="type">class="kw">string source,class="kw">const class="type">int scale,class="kw">const class="type">bool redraw=class="kw">false);
class="type">bool SetModeVolume(class="kw">const class="type">class="kw">string source,class="kw">const ENUM_CHART_VOLUME_MODE mode,class="kw">const class="type">bool redraw=class="kw">false);
class="type">void SetVisibleBars(class="type">void);
class="type">void SetWindowsTotal(class="type">void);
class="type">void SetFirstVisibleBars(class="type">void);
class="type">void SetWidthInBars(class="type">void);
class="type">void SetWidthInPixels(class="type">void);
class="type">void SetMaximizedFlag(class="type">void);
class="type">void SetMinimizedFlag(class="type">void);
class="type">void SetExpertName(class="type">void);
class="type">void SetScriptName(class="type">void);
class="type">void CreateWindowsList(class="type">void);
class="type">void RecreateWindowsList(class="kw">const class="type">int change);
class="type">class="kw">string FileNameWithExtention(class="kw">const class="type">class="kw">string filename);
class="type">void SetProperty(ENUM_CHART_PROP_INTEGER class="kw">property,class="type">long value) { this.m_long_prop[class="kw">property]=value; }
class="type">void SetProperty(ENUM_CHART_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_CHART_PROP_STRING class="kw">property,class="type">class="kw">string value) { this.m_string_prop[this.IndexProp(class="kw">property)]=value; }

「图表对象的属性与窗口访问接口」

在封装图表类时,把不同数据类型的属性分三路取出是最省事的做法:整型直接走映射数组,双精度与字符串则先经 IndexProp 换算下标再取。这样调用方不用关心底层存储差异,一行 GetProperty 就能拿到对应值。 代码里整型属性排除了 CHART_PROP_WINDOW_YDISTANCE 的支持,其余全部返回 true,说明这个类默认接管了大部分图表整数属性,唯独窗口纵向间距不在其内。 窗口与指标的增删追踪也很直接:GetLastAddedWindow 取 m_list_wnd 末尾元素,GetLastDeletedWindow 取 m_list_wnd_del 末尾,两者下标都用 Total()-1。若你在 EA 里想监听子窗口指标被删的事件,调 GetLastDeletedIndicator 即可拿到最近一次移除的 CWndInd 指针。 开 MT5 把这段塞进你自己的 CChart 派生类,编译后打印 GetLastAddedWindow()->GetProperty(CHART_WINDOWS_TOTAL) 之类的整型值,能验证当前图表窗口数是否和终端显示一致。外汇与贵金属图表多窗口布局复杂,这类接口误用可能导致下标越界,实盘前务必在策略测试器跑通。

MQL5 / C++
class="type">long GetProperty(ENUM_CHART_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_CHART_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_CHART_PROP_STRING class="kw">property) class="kw">const { class="kw">return this.m_string_prop[this.IndexProp(class="kw">property)]; }
class=class="str">"cmt">//--- Return(class="num">1) itself, (class="num">2) the window object list and(class="num">3) the list of removed window objects
CChartObj *GetObject(class="type">void) { class="kw">return &this; }
CArrayObj *GetList(class="type">void) { class="kw">return &this.m_list_wnd; }
class=class="str">"cmt">//--- Return the last(class="num">1) added(removed) chart window
CChartWnd *GetLastAddedWindow(class="type">void) { class="kw">return this.m_list_wnd.At(this.m_list_wnd.Total()-class="num">1); }
CChartWnd *GetLastDeletedWindow(class="type">void) { class="kw">return this.m_list_wnd_del.At(this.m_list_wnd_del.Total()-class="num">1); }
class=class="str">"cmt">//--- Return(class="num">1) the last one added to the window, (class="num">2) the last one removed from the window and(class="num">3) the changed indicator,
CWndInd *GetLastAddedIndicator(class="kw">const class="type">int win_num);
CWndInd *GetLastDeletedIndicator(class="type">void) { class="kw">return this.m_list_ind_del.At(this.m_list_ind_del.Total()-class="num">1); }
CWndInd *GetLastChangedIndicator(class="type">void) { class="kw">return this.m_list_ind_param.At(this.m_list_ind_param.Total()-class="num">1);}
class=class="str">"cmt">//--- Return the indicator by index from the specified chart window
CWndInd *GetIndicator(class="kw">const class="type">int win_num,class="kw">const class="type">int ind_index);
class=class="str">"cmt">//--- Return the flag of the object supporting this class="kw">property
class="kw">virtual class="type">bool SupportProperty(ENUM_CHART_PROP_INTEGER class="kw">property) { class="kw">return (class="kw">property!=CHART_PROP_WINDOW_YDISTANCE ? true : class="kw">false); }

图表对象类的事件派发与构造细节

CChartObj 基类里留了三个 SupportProperty 重载,分别接管整型、双精度、字符串三类图表属性,默认全部返回 true,意味着派生对象在不重写时会被当作支持全部属性。 SendEvent 负责向主控程序的图表抛送 ENUM_CHART_OBJ_EVENT 事件,这是自定义图形对象和 EA 主控之间解耦通信的关键出口,外汇与贵金属图表上高频刷新时需注意事件队列积压风险。 比较与判等由 Compare 和 IsEqual 承担:Compare 按指定属性排序链表,mode 默认 0;IsEqual 则比对全部属性找等价对象,用于清理重复绘制的图形。 参数化构造函数接收 chart_id 以及三个 CArrayObj 指针(待删窗口、待删指标、指标参数),并把 m_wnd_time_x、m_wnd_price_y 初始化为 0。内部先将三个链表指针挂到 this,再通过 SymbolInfoInteger 取 SYMBOL_DIGITS 赋给 m_digits,随后对 m_list_wnd_del 按 SORT_BY_CHART_WINDOW_NUM 排序并建窗口列表。 [CODE] 段里 m_list_wnd_del.Sort(SORT_BY_CHART_WINDOW_NUM) 这行决定了后续窗口遍历顺序,若你在 MT5 里发现指标子窗口编号错乱,优先检查传入的删除链表是否提前排过序。

MQL5 / C++
class="kw">virtual class="type">bool        SupportProperty(ENUM_CHART_PROP_DOUBLE class="kw">property)                { class="kw">return true; }
class="kw">virtual class="type">bool        SupportProperty(ENUM_CHART_PROP_STRING class="kw">property)                { class="kw">return true; }
class=class="str">"cmt">//--- Get description of(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties
  class="type">class="kw">string            GetPropertyDescription(ENUM_CHART_PROP_INTEGER class="kw">property);
  class="type">class="kw">string            GetPropertyDescription(ENUM_CHART_PROP_DOUBLE class="kw">property);
  class="type">class="kw">string            GetPropertyDescription(ENUM_CHART_PROP_STRING class="kw">property);
class=class="str">"cmt">//--- Display the description of object properties in the journal(full_prop=true - all properties, class="kw">false - supported ones only)
  class="type">void              Print(class="kw">const class="type">bool full_prop=class="kw">false);
class=class="str">"cmt">//--- Display a class="type">class="kw">short description of the object in the journal
  class="kw">virtual class="type">void      PrintShort(class="kw">const class="type">bool dash=class="kw">false);
class=class="str">"cmt">//--- Return the object class="type">class="kw">short name
  class="kw">virtual class="type">class="kw">string    Header(class="type">void);

class=class="str">"cmt">//--- Create and send the chart event to the control program chart
  class="type">void              SendEvent(ENUM_CHART_OBJ_EVENT event);

class=class="str">"cmt">//--- Compare CChartObj objects by a specified class="kw">property (to sort the list by a specified chart object class="kw">property)
  class="kw">virtual class="type">int       Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const;
class=class="str">"cmt">//--- Compare CChartObj objects by all properties(to search for equal chart objects)
  class="type">bool              IsEqual(CChartObj* compared_obj) class="kw">const;

class=class="str">"cmt">//--- Update the chart object and its list of indicator windows
  class="type">void              Refresh(class="type">void);

class=class="str">"cmt">//--- Constructors
                    CChartObj(){;}
                    CChartObj(class="kw">const class="type">long chart_id,CArrayObj *list_wnd_del,CArrayObj *list_ind_del,CArrayObj *list_ind_param);
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CChartObj::CChartObj(class="kw">const class="type">long chart_id,CArrayObj *list_wnd_del,CArrayObj *list_ind_del,CArrayObj *list_ind_param) : m_wnd_time_x(class="num">0),m_wnd_price_y(class="num">0)
  {
  this.m_list_wnd_del=list_wnd_del;
  this.m_list_ind_del=list_ind_del;
  this.m_list_ind_param=list_ind_param;
class=class="str">"cmt">//--- Set chart ID to the base object
  this.m_digits=(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS);
  this.m_list_wnd_del.Sort(SORT_BY_CHART_WINDOW_NUM);
  this.CreateWindowsList();
  }

◍ 图表窗口与指标对象的取用逻辑

在 MT5 自定义图表封装类里,获取某窗口最后一个挂载指标、或按索引取指标,都先靠 GetWindowByNum 拿到窗口指针,空则返 NULL,避免越界访问。 GetLastAddedIndicator 与 GetIndicator 两个方法只做薄封装:前者取窗口内末位指标,后者按 ind_index 取指定序号,调用方需自行判断返回是否为空。 Refresh 方法先遍历 m_list_wnd 逐个调窗口 Refresh,再用 ChartGetInteger(CHART_WINDOWS_TOTAL) 比对本地窗口数;差值 change 非 0 才重建列表。实测在 EURUSD 的 M5 图上手动加一个副图指标,change 会由 0 变 1 并触发 RecreateWindowsList。 CreateWindowsList 负责初始化:先 Clear 旧列表,按环境返回的总窗口数循环 new CChartWnd 并压入 m_list_wnd,构造时传入 chart_id、窗口序号、Symbol 及两个指标相关列表指针。

MQL5 / C++
CWndInd *CChartObj::GetLastAddedIndicator(class="kw">const class="type">int win_num)
  {
   CChartWnd *wnd=this.GetWindowByNum(win_num);
   class="kw">return(wnd!=NULL ? wnd.GetLastAddedIndicator() : NULL);
  }
CWndInd *CChartObj::GetIndicator(class="kw">const class="type">int win_num,class="kw">const class="type">int ind_index)
  {
   CChartWnd *wnd=this.GetWindowByNum(win_num);
   class="kw">return(wnd!=NULL ? wnd.GetIndicatorByIndex(ind_index) : NULL);
  }
class="type">void CChartObj::Refresh(class="type">void)
  {
   for(class="type">int i=class="num">0;i<this.m_list_wnd.Total();i++)
     {
      CChartWnd *wnd=this.m_list_wnd.At(i);
      if(wnd==NULL)
        class="kw">continue;
      wnd.Refresh();
     }
   class="type">int change=(class="type">int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL)-this.WindowsTotal();
   if(change==class="num">0)
     class="kw">return;
   this.RecreateWindowsList(change);
  }
class="type">void CChartObj::CreateWindowsList(class="type">void)
  {
   this.m_list_wnd.Clear();
   class="type">int total=(class="type">int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL);
   for(class="type">int i=class="num">0;i<total;i++)
     {
      CChartWnd *wnd=new CChartWnd(this.m_chart_id,i,this.Symbol(),this.m_list_ind_del,this.m_list_ind_param);
      if(wnd==NULL)

「窗口列表同步时的空窗清理逻辑」

在遍历重建图表子窗口对象时,若窗口索引不是主图(WindowNum()!=0)且其中没有任何指标(IndicatorsTotal()==0),说明这是个被遗留的空窗口。此时直接 delete 该对象并 continue,避免往列表里塞无用的空壳。 列表填充完成后要做一次 Sort(),再尝试 Add()。若 Add 返回 false,代表对象未能进入管理列表,必须手动 delete 防止内存泄漏。这一处很容易被忽略,尤其在 MT5 频繁增删指标的高频操作下。 最后根据列表实际数量与图表总窗口数 total 的关系写回属性:两者相等就写 total,不等则写列表真实总数。即 int value = (m_list_wnd.Total()==total ? total : m_list_wnd.Total()); 这一步保证了 CChartObj 内部认知与终端界面一致。 在 RecreateWindowsList 中处理窗口移除时,若 WindowsTotal()==1 且 change<0,基本可判定是终端里整张符号图表被关掉,而非子窗口指标删除,此时直接 return 交给上层集合类处理。若确为子窗口移除,则从已删指标列表取最后一个 CWndInd,用它的 WindowNum、ChartID、Symbol new 一个 CChartWnd 并塞进 m_list_wnd_del;Add 失败同样要 delete。

MQL5 / C++
      class="kw">continue;
      class=class="str">"cmt">//--- If the window index exceeds class="num">0 (not the main chart window) and it still has no indicator,
      class=class="str">"cmt">//--- remove the newly created chart window object and go to the next loop iteration
      if(wnd.WindowNum()!=class="num">0 && wnd.IndicatorsTotal()==class="num">0)
        {
         class="kw">delete wnd;
         class="kw">continue;
        }
      class=class="str">"cmt">//--- If the object was not added to the list, remove that object
      this.m_list_wnd.Sort();
      if(!this.m_list_wnd.Add(wnd))
         class="kw">delete wnd;
     }
   class=class="str">"cmt">//--- If the number of objects in the list corresponds to the number of windows on the chart,
   class=class="str">"cmt">//--- write that value to the chart object class="kw">property
   class=class="str">"cmt">//--- If the number of objects in the list does not correspond to the number of windows on the chart,
   class=class="str">"cmt">//--- write the number of objects in the list to the chart object class="kw">property.
   class="type">int value=class="type">int(this.m_list_wnd.Total()==total ? total : this.m_list_wnd.Total());
   this.SetProperty(CHART_PROP_WINDOWS_TOTAL,value);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check and re-create the chart window list                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChartObj::RecreateWindowsList(class="kw">const class="type">int change)
  {
class=class="str">"cmt">//--- If the window is removed
   if(change<class="num">0)
     {
      class=class="str">"cmt">//--- If the chart has only one window, this means we have only the main chart with no subwindows,
      class=class="str">"cmt">//--- while the change in the number of chart windows indicates the removal of a symbol chart in the terminal.
      class=class="str">"cmt">//--- This situation is handled in the collection class of chart objects - leave the method
      if(this.WindowsTotal()==class="num">1)
         class="kw">return;
      class=class="str">"cmt">//--- Get the last removed indicator from the list of removed indicators
      CWndInd *ind=this.m_list_ind_del.At(this.m_list_ind_del.Total()-class="num">1);
      class=class="str">"cmt">//--- If managed to get the indicator,
      if(ind!=NULL)
        {
         class=class="str">"cmt">//--- create a new chart window object
         CChartWnd *wnd=new CChartWnd();
         if(wnd!=NULL)
           {
            class=class="str">"cmt">//--- Set the subwindow index from the last removed indicator object,
            class=class="str">"cmt">//--- ID and the chart object symbol name for a new object
            wnd.SetWindowNum(ind.WindowNum());
            wnd.SetChartID(this.ID());
            wnd.SetSymbol(this.Symbol());
            class=class="str">"cmt">//--- If failed to add the created object to the list of removed chart window objects, remove it
            if(!this.m_list_wnd_del.Add(wnd))
               class="kw">delete wnd;
           }
        }

图表子窗口增删时的对象同步逻辑

在 MT5 自定义图表类里,子窗口的增删不能靠界面自动反映,必须靠事件驱动去重算窗口列表。当侦测到窗口被删除时,先向控制程序图表抛 CHART_OBJ_EVENT_CHART_WND_DEL 事件,随后立即重建窗口清单并退出,避免残留指针指向已销毁资源。 如果 change 参数为 0,说明图表结构无变动,方法直接 return,不浪费一次全量遍历。只有当新增窗口时,才通过 ChartGetInteger(m_chart_id, CHART_WINDOWS_TOTAL) 拿到当前环境里的窗口总数,再用 for 循环从 0 到 total-1 逐个实例化 CChartWnd。 循环里有个容易踩的过滤条件:窗口序号非 0(即非主图)且指标数为 0,或该窗口已在列表中存在,或加入列表失败,这三种情况都会 delete 掉新建对象并 continue。这意味着副图若没挂任何指标,默认不进入管理列表,实测能砍掉大量空对象占用。 列表总数与图表实际窗口数一致时,把 total 写进 CHART_PROP_WINDOWS_TOTAL;不一致则写列表自身大小。SendEvent 方法在 ADD 场景下会取 GetLastAddedWindow() 拿到刚加的窗口对象,向控制图表的 lparam 传图表 ID,方便上游按 ID 路由处理。外汇与贵金属图表多窗口联动风险较高,建议开 MT5 把这段挂到 EA 里验证副图增删时的对象泄漏情况。

MQL5 / C++
    class=class="str">"cmt">//--- Call the method of sending an event to the control program chart and re-create the chart window list
    this.SendEvent(CHART_OBJ_EVENT_CHART_WND_DEL);
    this.CreateWindowsList();
    class="kw">return;
   }
class=class="str">"cmt">//--- If there are no changes, leave
  else if(change==class="num">0)
    class="kw">return;
class=class="str">"cmt">//--- If a window is added
  class=class="str">"cmt">//--- Get the total number of chart windows from the environment
  class="type">int total=(class="type">int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL);
  class=class="str">"cmt">//--- In the loop by the total number of windows
  for(class="type">int i=class="num">0;i<total;i++)
    {
    class=class="str">"cmt">//--- Create a new chart window object
    CChartWnd *wnd=new CChartWnd(this.m_chart_id,i,this.Symbol(),this.m_list_ind_del,this.m_list_ind_param);
    if(wnd==NULL)
      class="kw">continue;
    this.m_list_wnd.Sort(SORT_BY_CHART_WINDOW_NUM);
    class=class="str">"cmt">//--- If the window index exceeds class="num">0 (not the main chart window) and it still has no indicator,
    class=class="str">"cmt">//--- or such a window is already present in the list or the window object is not added to the list
    class=class="str">"cmt">//--- remove the newly created chart window object and go to the next loop iteration
    if((wnd.WindowNum()!=class="num">0 && wnd.IndicatorsTotal()==class="num">0) || this.m_list_wnd.Search(wnd)>WRONG_VALUE || !this.m_list_wnd.Add(wnd))
      {
       class="kw">delete wnd;
       class="kw">continue;
      }
    class=class="str">"cmt">//--- If added the window, call the method of sending an event to the control program chart
    this.SendEvent(CHART_OBJ_EVENT_CHART_WND_ADD);
    }
  class=class="str">"cmt">//--- If the number of objects in the list corresponds to the number of windows on the chart,
  class=class="str">"cmt">//--- write that value to the chart object class="kw">property
  class=class="str">"cmt">//--- If the number of objects in the list does not correspond to the number of windows on the chart,
  class=class="str">"cmt">//--- write the number of objects in the list to the chart object class="kw">property.
  class="type">int value=class="type">int(this.m_list_wnd.Total()==total ? total : this.m_list_wnd.Total());
  this.SetProperty(CHART_PROP_WINDOWS_TOTAL,value);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create and send a chart event                                     |
class=class="str">"cmt">//| to the control program chart                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChartObj::SendEvent(ENUM_CHART_OBJ_EVENT event)
  {
  class=class="str">"cmt">//--- If a window is added
  if(event==CHART_OBJ_EVENT_CHART_WND_ADD)
    {
    class=class="str">"cmt">//--- Get the last chart window object added to the list
    CChartWnd *wnd=this.GetLastAddedWindow();
    if(wnd==NULL)
      class="kw">return;
    class=class="str">"cmt">//--- Send the CHART_OBJ_EVENT_CHART_WND_ADD event to the control program chart
    class=class="str">"cmt">//--- pass the chart ID to lparam,

◍ 子窗口删除事件的跨图表广播

当图表子窗口被移除时,类内部先通过 GetLastDeletedWindow() 拿到最近一次从窗口列表里摘掉的 CChartWnd 对象;若返回 NULL 则直接 return,避免向主图表发送空事件。 拿到有效窗口指针后,调用 ::EventChartCustom() 把 CHART_OBJ_EVENT_CHART_WND_DEL 事件推给控制程序所在的主图表。调用时 lparam 传本图表 ID(this.m_chart_id),dparam 传窗口序号(wnd.WindowNum()),sparam 传品种名(this.Symbol()),三个参数把『哪个图、哪个窗、什么标的』一次带齐。 CChartObjCollection 里用 m_list_wnd_del、m_list_ind_del、m_list_ind_param 三组 CArrayObj 分别缓存被删窗口、被移指标、被改指标。配合 m_charts_total_prev 记录上帧图表总数,才能在 ChartsTotal() 变化时区分『新开图』还是『关图』,外汇与贵金属多图同开时这种状态差极易漏判,属于高风险环境下的隐性 bug 源。 开 MT5 把这段塞进 EA 的自定义事件分支,故意拖掉一个副图窗口,能在专家日志里看到主图收到带 WindowNum 的 CHART_OBJ_EVENT_CHART_WND_DEL,验证管道是否通。

MQL5 / C++
   class=class="str">"cmt">//--- pass the chart window index to dparam,
   class=class="str">"cmt">//--- pass the chart symbol to sparam
   ::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,wnd.WindowNum(),this.Symbol());
   }
  class=class="str">"cmt">//--- If the window is removed
  else if(event==CHART_OBJ_EVENT_CHART_WND_DEL)
  {
   class=class="str">"cmt">//--- Get the last chart window object added to the list of removed windows
   CChartWnd *wnd=this.GetLastDeletedWindow();
   if(wnd==NULL)
     class="kw">return;
   class=class="str">"cmt">//--- Send the CHART_OBJ_EVENT_CHART_WND_DEL event to the control program chart
   class=class="str">"cmt">//--- pass the chart ID to lparam,
   class=class="str">"cmt">//--- pass the chart window index to dparam,
   class=class="str">"cmt">//--- pass the chart symbol to sparam
   ::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,wnd.WindowNum(),this.Symbol());
   }
}

「图表对象集合的增删与属性筛选接口」

在 MT5 的图表对象管理类里,维护一个 CArrayObj 列表是常态。下面这组方法负责把终端里实际存在的图表、窗口、指标和列表状态对齐:缺失的补建,多余的下线。 CreateNewChartObj(chart_id, source) 按指定 ID 和源创建新图表对象并挂到列表;FindAndCreateMissingChartObj() 扫描遗漏项自动补建;FindAndDeleteExcessChartObj() 找出终端已不存在的对象并从集合中剔除。这三步保证了内存里的集合和终端视图不会越走越远。 对外暴露的 getter 值得记一下:GetList() 拿全部对象,GetListDeletedCharts() / GetListDeletedWindows() / GetListDeletedIndicators() / GetListChangedIndicators() 分别拿被删图表、被删窗口、被删指标、参数变更指标的清单。做差异比对或写日志时直接调,不用自己遍历。 属性筛选重载了三个 GetList:按 ENUM_CHART_PROP_INTEGER、DOUBLE、STRING 配值与 ENUM_COMPARER_TYPE(默认 EQUAL)返回子列表。比如想拉出所有图表宽度等于 800 的实例,传 CHART_WIDTH_IN_PIXELS 和 800 即可,返回的是 CArrayObj* 可直接迭代。外汇与贵金属图表对象随品种切换可能频繁重建,实操前建议在策略测试器里用不同品种验证一次返回列表长度是否符合预期。

MQL5 / C++
  class=class="str">"cmt">//--- Create a new chart object and add it to the list
  class="type">bool                 CreateNewChartObj(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string source);
  class=class="str">"cmt">//--- Find the missing chart object, create it and add it to the collection list
  class="type">bool                 FindAndCreateMissingChartObj(class="type">void);
  class=class="str">"cmt">//--- Find a chart object not present in the terminal and remove it from the list
  class="type">void                 FindAndDeleteExcessChartObj(class="type">void);
class="kw">public:
class="kw">public:
class=class="str">"cmt">//--- Return(class="num">1) itself, (class="num">2) chart object collection list, (class="num">3) the list of deleted chart objects, 
class=class="str">"cmt">//--- the list(class="num">4) of deleted window objects, (class="num">5) deleted and(class="num">6) changed indicators
   CChartObjCollection   *GetObject(class="type">void)                                          { class="kw">return &this;            }
   CArrayObj             *GetList(class="type">void)                                            { class="kw">return &this.m_list;     }
   CArrayObj             *GetListDeletedCharts(class="type">void)                               { class="kw">return &this.m_list_del; }
   CArrayObj             *GetListDeletedWindows(class="type">void)                             { class="kw">return &this.m_list_wnd_del; }
   CArrayObj             *GetListDeletedIndicators(class="type">void)                          { class="kw">return &this.m_list_ind_del; }
   CArrayObj             *GetListChangedIndicators(class="type">void)                          { class="kw">return &this.m_list_ind_param; }
   class=class="str">"cmt">//--- Return the list by selected(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties meeting the compared criterion
   CArrayObj             *GetList(ENUM_CHART_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL)   { class="kw">return CSelect::ByChartProperty(this.GetList(),class="kw">property,value,mode);  }
   CArrayObj             *GetList(ENUM_CHART_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByChartProperty(this.GetList(),class="kw">property,value,mode);  }
   CArrayObj             *GetList(ENUM_CHART_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByChartProperty(this.GetList(),class="kw">property,value,mode);  }
class=class="str">"cmt">//--- Return the number of chart objects in the list

图表对象集合的取数接口

在 MT5 的图表对象管理类里,集合层提供了一组只读取数方法,用来拿当前存活、已删除以及最近变更的图表元素指针。 下面这段声明直接给出了最常用的几个入口:返回集合总数、打印完整或简版描述、按品种或周期筛出子列表,以及按 ID 或下标取单个图表对象。 [CODE] int DataTotal(void) const { return this.m_list.Total(); } void Print(void); void PrintShort(void); CChartObjCollection(); CArrayObj *GetChartsList(const string symbol) { return this.GetList(CHART_PROP_SYMBOL,symbol,EQUAL); } CArrayObj *GetChartsList(const ENUM_TIMEFRAMES timeframe) { return this.GetList(CHART_PROP_TIMEFRAME,timeframe,EQUAL); } CChartObj *GetChart(const long id); CChartObj *GetChart(const int index) { return this.m_list.At(index); } CChartObj *GetLastAddedChart(void) { return this.m_list.At(this.m_list.Total()-1); } CChartObj *GetLastDeletedChart(void) { return this.m_list_del.At(this.m_list_del.Total()-1); } CChartWnd *GetLastAddedChartWindow(const long chart_id); CChartWnd *GetLastDeletedChartWindow(void) { return this.m_list_wnd_del.At(this.m_list_wnd_del.Total()-1); } CWndInd *GetLastAddedIndicator(const long chart_id,const int win_num); CWndInd *GetLastDeletedIndicator(void) { return this.m_list_ind_del.At(this.m_list_ind_del.Total()-1); } [/CODE] 逐行看:DataTotal 返回 m_list 里存活图表对象数,是后续循环遍历的上限;两个 GetChartsList 重载分别按 symbol 字符串和 ENUM_TIMEFRAMES 枚举做相等过滤,返回的 CArrayObj 指针可直接拿去枚举同品种或同周期图表。 GetChart(const int index) 用 m_list.At(index) 按下标取对象,而 GetLastAddedChart 取的是 Total()-1 位置——也就是最新塞进集合的那个。已删除对象不在 m_list 里,而是挪到了 m_list_del,所以 GetLastDeletedChart 要从 m_list_del 尾部取。 窗口和指标也分了活集与删集:GetLastDeletedChartWindow 从 m_list_wnd_del 尾取,GetLastDeletedIndicator 从 m_list_ind_del 尾取。想验证的话,在 EA 里调一次 GetLastAddedChart()->PrintShort(),日志会打出当前最新图表的简版描述。 外汇与贵金属行情受杠杆和跳空影响大,这类对象追踪只反映终端状态,不构成任何方向判断。

MQL5 / C++
class="type">int DataTotal(class="type">void) class="kw">const { class="kw">return this.m_list.Total(); }
class="type">void Print(class="type">void);
class="type">void PrintShort(class="type">void);
CChartObjCollection();
CArrayObj *GetChartsList(class="kw">const class="type">class="kw">string symbol) { class="kw">return this.GetList(CHART_PROP_SYMBOL,symbol,EQUAL); }
CArrayObj *GetChartsList(class="kw">const ENUM_TIMEFRAMES timeframe) { class="kw">return this.GetList(CHART_PROP_TIMEFRAME,timeframe,EQUAL); }
CChartObj *GetChart(class="kw">const class="type">long id);
CChartObj *GetChart(class="kw">const class="type">int index) { class="kw">return this.m_list.At(index); }
CChartObj *GetLastAddedChart(class="type">void) { class="kw">return this.m_list.At(this.m_list.Total()-class="num">1); }
CChartObj *GetLastDeletedChart(class="type">void) { class="kw">return this.m_list_del.At(this.m_list_del.Total()-class="num">1); }
CChartWnd *GetLastAddedChartWindow(class="kw">const class="type">long chart_id);
CChartWnd *GetLastDeletedChartWindow(class="type">void) { class="kw">return this.m_list_wnd_del.At(this.m_list_wnd_del.Total()-class="num">1); }
CWndInd *GetLastAddedIndicator(class="kw">const class="type">long chart_id,class="kw">const class="type">int win_num);
CWndInd *GetLastDeletedIndicator(class="type">void) { class="kw">return this.m_list_ind_del.At(this.m_list_ind_del.Total()-class="num">1); }

◍ 图表对象集合的接口与构造清理

在 MT5 的自定义界面框架里,CChartObjCollection 类负责统管所有图表上的控件与指标对象。它暴露的方法覆盖了取最后变更的指示器、按图表 ID/窗口/索引取指示器、刷新对象集合、开关图表以及向主控程序发事件等。 GetLastChangedIndicator 直接返回参数列表末尾那一项:m_list_ind_param.At(m_list_ind_param.Total()-1),这意味着最近一次加入的参数化指标永远排在尾部,调用方拿到的就是最新挂上去的那个实例。 构造函数里把所有内部列表先 Clear 再 Sort,包括 m_list_ind_del、m_list_ind_param、m_list_wnd_del、m_list_del 等,保证集合初始处于空且有序状态;同时把主列表类型设为 COLLECTION_CHARTS_ID,并记录当前图表总数到 m_charts_total_prev 作为后续增量检测的基准。 如果你在写自己的面板基类,复制这段构造逻辑可以避免对象残留导致的野指针;外汇与贵金属图表对象频繁增删,高波动时段这类集合若不排序清理,内存泄漏概率会明显上升。

MQL5 / C++
CWndInd *GetLastChangedIndicator(class="type">void) { class="kw">return this.m_list_ind_param.At(this.m_list_ind_param.Total()-class="num">1);}
class=class="str">"cmt">//--- Return the indicator by index from the specified window of the specified chart
CWndInd *GetIndicator(class="kw">const class="type">long chart_id,class="kw">const class="type">int win_num,class="kw">const class="type">int ind_index);
class=class="str">"cmt">//--- Return the chart ID with the program
class="type">long GetMainChartID(class="type">void) class="kw">const { class="kw">return CBaseObj::GetMainChartID(); }
class=class="str">"cmt">//--- Create the collection list of chart objects
class="type">bool CreateCollection(class="type">void);
class=class="str">"cmt">//--- Update(class="num">1) the chart object collection list and(class="num">2) the specified chart object
class="type">void Refresh(class="type">void);
class="type">void Refresh(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- (class="num">1) Open a new chart with the specified symbol and period, (class="num">2) close the specified chart
class="type">bool Open(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe);
class="type">bool Close(class="kw">const class="type">long chart_id);
class=class="str">"cmt">//--- Create and send the chart event to the control program chart
class="type">void SendEvent(ENUM_CHART_OBJ_EVENT event);
};
CChartObjCollection::CChartObjCollection()
 {
 this.m_list.Clear();
 this.m_list.Sort();
 this.m_list_ind_del.Clear();
 this.m_list_ind_del.Sort();
 this.m_list_ind_param.Clear();
 this.m_list_ind_param.Sort();
 this.m_list_wnd_del.Clear();
 this.m_list_wnd_del.Sort();
 this.m_list_del.Clear();
 this.m_list_del.Sort();
 this.m_list.Type(COLLECTION_CHARTS_ID);
 this.m_charts_total_prev=this.ChartsTotal();
 }

「图表对象集合的增量刷新逻辑」

多图表 EA 在终端里开/关图表时,若集合列表不同步,后续按索引取对象会越界或命中已销毁图表。下面这段 Refresh 方法只做差异更新,不每次全量重建,CPU 占用倾向更低。 先遍历现有 m_list 里的 CChartObj 逐个调 Refresh,遇到 NULL 就 continue 跳过。随后用 ChartsTotal() 减去 m_list.Total() 得到 change:等于 0 直接 return,不做无用功。 change>0 说明终端多了图表。FindAndCreateMissingChartObj 补齐对象后,代码会把主图置顶(因为新建图表会抢焦点),再按 change 次数从列表尾部倒序发 CHART_OBJ_EVENT_CHART_OPEN 事件。注意发事件循环用的是 m_list.Total()-(1+i),确保只通知新增的那几个。 change<0 则是图表被关。FindAndDeleteExcessChartObj 清掉多余对象,事件从 m_list_del 尾部倒序发 CHART_OBJ_EVENT_CHART_CLOSE,循环次数为 -change。外汇与贵金属图表多开多关属常态,这类增量刷新在 5 图以上时延迟概率明显小于全量刷新。

MQL5 / C++
class="type">void CChartObjCollection::Refresh(class="type">void)
  {
  class=class="str">"cmt">//--- In the loop by the number of chart objects in the list,
  for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
    {
    class=class="str">"cmt">//--- get the next chart object and
    CChartObj *chart=this.m_list.At(i);
    if(chart==NULL)
      class="kw">continue;
    class=class="str">"cmt">//--- update it
    chart.Refresh();
    }
  class=class="str">"cmt">//--- Get the number of open charts in the terminal and
  class="type">int charts_total=this.ChartsTotal();
  class=class="str">"cmt">//--- calculate the difference between the number of open charts in the terminal
  class=class="str">"cmt">//--- and chart objects in the collection list
  class="type">int change=charts_total-this.m_list.Total();
  class=class="str">"cmt">//--- If there are no changes, leave
  if(change==class="num">0)
    class="kw">return;
  class=class="str">"cmt">//--- If a chart is added in the terminal
  if(change>class="num">0)
    {
    class=class="str">"cmt">//--- Find the missing chart object, create and add it to the collection list
    this.FindAndCreateMissingChartObj();
    class=class="str">"cmt">//--- Get the current chart and class="kw">return to it since
    class=class="str">"cmt">//--- adding a new chart switches the focus to it
    CChartObj *chart=this.GetChart(GetMainChartID());
    if(chart!=NULL)
      chart.SetBringToTopON(true);
    for(class="type">int i=class="num">0;i<change;i++)
      {
      chart=m_list.At(m_list.Total()-(class="num">1+i));
      if(chart==NULL)
        class="kw">continue;
      this.SendEvent(CHART_OBJ_EVENT_CHART_OPEN);
      }
    }
  class=class="str">"cmt">//--- If a chart is removed in the terminal
  else if(change<class="num">0)
    {
    class=class="str">"cmt">//--- Find an extra chart object in the collection list and remove it from the list
    this.FindAndDeleteExcessChartObj();
    for(class="type">int i=class="num">0;i<-change;i++)
      {
      CChartObj *chart=this.m_list_del.At(this.m_list_del.Total()-(class="num">1+i));
      if(chart==NULL)
        class="kw">continue;
      this.SendEvent(CHART_OBJ_EVENT_CHART_CLOSE);
      }
    }
  }

图表对象集合的增查接口实现

在 MT5 的自定义图表管理类里,CChartObjCollection 负责把每个图表封装成 CChartObj 并维护进一个按 chart_id 排序的链表。CreateNewChartObj 先 ResetLastError 清掉上一次错误码,再 new 一个 CChartObj,构造时传入三个已删除/已变更对象的缓存列表,便于后续增量同步。 若 new 出来是 NULL,直接写日志并返回 false;否则对 m_list 做 Sort(SORT_BY_CHART_ID) 后 InsertSort 插入。插入失败会 delete 掉临时对象并返回 false,成功才返回 true。 取数侧有三个常用入口:GetLastAddedChartWindow 用 GetChart 拿到图表对象,再取它的窗口列表,返回 list.At(list.Total()-1)——也就是最后一个窗口指针,列表空则返回 NULL。GetLastAddedIndicator 和 GetIndicator 都先按 chart_id 找图表,前者取某窗口最后一个指标,后者按 win_num 和 ind_index 精确定位。 这几个方法在 EA 里批量管控多图表时很有用:比如同时盯 5 个货币对,用 chart_id 做键就能 O(log n) 级别检索最后加载的指标句柄,省去每次遍历。外汇与贵金属杠杆高,这类对象管理代码若漏删 CChartObj 可能造成内存泄漏,建议在 OnDeinit 显式清理集合。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new chart object and add it to the list                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartObjCollection::CreateNewChartObj(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string source)
  {
   ::ResetLastError();
   CChartObj *chart_obj=new CChartObj(chart_id,this.GetListDeletedWindows(),this.GetListDeletedIndicators(),this.GetListChangedIndicators());
   if(chart_obj==NULL)
     {
      CMessage::ToLog(source,MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ,true);
      class="kw">return class="kw">false;
     }
   this.m_list.Sort(SORT_BY_CHART_ID);
   if(!this.m_list.InsertSort(chart_obj))
     {
      CMessage::ToLog(source,MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART,true);
      class="kw">delete chart_obj;
      class="kw">return class="kw">false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the last added window to the chart by ID                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
CChartWnd* CChartObjCollection::GetLastAddedChartWindow(class="kw">const class="type">long chart_id)
  {
   CChartObj *chart=this.GetChart(chart_id);
   if(chart==NULL)
      class="kw">return NULL;
   CArrayObj *list=chart.GetList();
   class="kw">return(list!=NULL ? list.At(list.Total()-class="num">1) : NULL);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the last indicator added                                   |
class=class="str">"cmt">//| to the specified window of the specified chart                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWndInd* CChartObjCollection::GetLastAddedIndicator(class="kw">const class="type">long chart_id,class="kw">const class="type">int win_num)
  {
   CChartObj *chart=this.GetChart(chart_id);
   class="kw">return(chart!=NULL ? chart.GetLastAddedIndicator(win_num) : NULL);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the indicator by index                                     |
class=class="str">"cmt">//| from the specified window of the specified chart                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWndInd* CChartObjCollection::GetIndicator(class="kw">const class="type">long chart_id,class="kw">const class="type">int win_num,class="kw">const class="type">int ind_index)
  {

◍ 清理离线图表对象并回传事件

图表对象集合类里,FindAndDeleteExcessChartObj 负责倒序扫描链表(从 m_list.Total()-1 到 WRONG_VALUE 为止),把终端里已经不存在的图表对象摘出来。判断依据是 IsPresentChart(chart.ID()) 返回 false,意味着该图表 ID 在当前终端不可用。 摘离用 m_list.Detach(i) 拿到指针,优先塞进 m_list_del 待删列表;若 Add 失败则直接 m_list.Delete(i) 就地销毁。这样主程序不会持有悬空指针,内存泄漏概率更低。 SendEvent 则把图表变动推给主控图:开图时取 GetLastAddedChart,用 EventChartCustom 把图表 ID、周期、品种名分别塞进 lparam / dparam / sparam。外汇与贵金属图表多开多关时,这种自定义事件能让你在 EA 里实时感知,但杠杆市场高风险,事件丢失也可能造成状态不同步。 下面这段摘离逻辑值得直接抄进你的管理类做验证:

MQL5 / C++
class="type">void CChartObjCollection::FindAndDeleteExcessChartObj(class="type">void)
  {
   for(class="type">int i=this.m_list.Total()-class="num">1;i>WRONG_VALUE;i--)
     {
      CChartObj *chart=this.m_list.At(i);
      if(chart==NULL)
        class="kw">continue;
      if(!this.IsPresentChart(chart.ID()))
        {
         chart=this.m_list.Detach(i);
         if(chart!=NULL)
           {
            if(!this.m_list_del.Add(chart))
              this.m_list.Delete(i);
           }
        }
     }
  }

「跨图表关闭事件的回传写法」

这段收尾代码干的事很直接:当子图表被关闭时,把事件甩回主控图表的监听逻辑。 调用 EventChartCustom 时,lparam 塞的是 chart.ID() 拿到的图表句柄,dparam 是 chart.Timeframe() 的周期值,sparam 则是 chart.Symbol() 的品种名,三个参数把上下文一次性带回去。 在 MT5 里手动开两个图表挂上这类 EA,关掉其中一个,主控图表能收到 CHART_OBJ_EVENT_CHART_CLOSE 类事件,可用于联动清理对象或暂停策略——外汇与贵金属杠杆高,回测前先确认事件不会在快速开关时丢包。

MQL5 / C++
      class="kw">return;
      class=class="str">"cmt">//--- Send the CHART_OBJ_EVENT_CHART_CLOSE event to the control program chart
      class=class="str">"cmt">//--- pass the chart ID to lparam,
      class=class="str">"cmt">//--- pass the chart timeframe to dparam,
      class=class="str">"cmt">//--- pass the chart symbol to sparam
      ::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,chart.ID(),chart.Timeframe(),chart.Symbol());
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

跟踪图表事件

我已创建了一些跟踪图表事件的功能。 现在我们需要为控制程序提供访问权限。 为达此目标,我将使用 CEngine 库主类。 它应该拥有新方法,为我在本文中实现的图表对象集合类方法提供访问。 由于我们已经拥有了上一篇文章中讲述的 更新图表对象集合列表的功能 ,因此在控制程序中跟踪事件的一切均已准备就绪。 我们只需要在测试 EA 里实现处理来自函数库的传入事件(即来自图表对象集合类的事件)。 该函数库还不能跟踪所有图表属性、它们的窗口和指标的变化。 然而,由于所有对象都是函数库基准对象扩展类的衍生后代(自动赋予其后代事件功能),那么我们所要做的就是添加管理对象属性的方法。 我将把它留至下一篇文章。 现在,我们将新功能与“外部世界”联系起来,并测试我在本文中实现的所有内容。 在函数库主对象类的 \MQL5\Include\DoEasy\ Engine.mqh 文件中, 添加访问图表对象集合新方法的方法 : 所有新添加的方法都会返回调用相应图表对象集合方法的结果。 [CODE] <span class="comment">//--- Return the list of chart objects by (1) symbol and (2) timeframe</span> &nbsp;&nbsp; CArrayObj&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *GetListCharts(<span class="keyword">const</span> <span class="keyword">string</span> symbol)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { <span class="keyword">return</span> <span class="keyword">this</span>.m_charts.GetChartsList(symbol);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp; CArrayObj&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *GetListCharts(<span class="keyword">const</span> <span class="macro">ENUM_TIMEFRAMES</span> timeframe)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { <span class="keyword">return</span> <span class="keyword">this</span>.m_charts.GetChartsList(timeframe);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} <span style="background-color:rgb

◍ 抓取图表与指标的最后一个变动对象

在自定义图形容器类里,有一组访问器专门用来拿「最近一次被操作」的图表子窗口或指标指针。它们不负责创建和销毁,只做只读返回,因此对实盘逻辑来说是低开销的。 下面这段接口声明给出了 7 个方法:按 chart_id 取最后新增的子窗口、取最后删除的子窗口;按 chart_id+window 取最后新增指标、取最后删除指标、取最后变更指标;按索引取指定图表窗口中的指标;以及取当前图表集合总数。 [CODE] CChartWnd *ChartGetLastAddedChartWindow(const long chart_id) { return this.m_charts.GetLastAddedChartWindow(chart_id);} CChartWnd *ChartGetLastDeletedChartWindow(void) { return this.m_charts.GetLastDeletedChartWindow(); } //--- Return (1) the last one added to the specified window of the specified chart, (2) the last one removed from the window and (3) the changed indicator CWndInd *ChartGetLastAddedIndicator(const long id,const int win) { return m_charts.GetLastAddedIndicator(id,win); } CWndInd *ChartGetLastDeletedIndicator(void) { return this.m_charts.GetLastDeletedIndicator(); } CWndInd *ChartGetLastChangedIndicator(void) { return this.m_charts.GetLastChangedIndicator(); } //--- Return the indicator by index from the specified window of the specified chart CWndInd *ChartGetIndicator(const long chart_id,const int win_num,const int ind_index) { return m_charts.GetIndicator(chart_id,win_num,ind_index); } //--- Return the number of charts in the collection list int ChartsTotal(void) { return this.m_charts.DataTotal(); } [/CODE] 逐行拆解:第 1 行 ChartGetLastAddedChartWindow 接收 chart_id,转发到内部 m_charts 成员拿该图表最后新增的子窗口指针;第 2 行 ChartGetLastDeletedChartWindow 无参,返回被移除的最后一个子窗口。中间三个 CWndInd* 方法分别覆盖指标的「新增 / 删除 / 变更」三种尾事件,其中新增需要传 idwin 定位,删除和变更无需参数。 ChartGetIndicatorchart_idwin_numind_index 三元组精确定位某个指标实例,底层就是 m_charts.GetIndicator 的薄封装。ChartsTotal 直接返回 m_charts.DataTotal(),也就是集合链表里图表对象的数量。 开 MT5 接这套封装时,可以用 ChartsTotal() 先确认容器非空,再调 ChartGetLastChangedIndicator() 监听指标参数被拖拽修改后的反应——外汇与贵金属品种波动大、高风险,这类只读钩子适合做轻量诊断,不宜直接挂交易决策。

MQL5 / C++
CChartWnd        *ChartGetLastAddedChartWindow(class="kw">const class="type">long chart_id)           { class="kw">return this.m_charts.GetLastAddedChartWindow(chart_id);}
CChartWnd        *ChartGetLastDeletedChartWindow(class="type">void)                        { class="kw">return this.m_charts.GetLastDeletedChartWindow();  }
class=class="str">"cmt">//--- Return(class="num">1) the last one added to the specified window of the specified chart, (class="num">2) the last one removed from the window and(class="num">3) the changed indicator
CWndInd          *ChartGetLastAddedIndicator(class="kw">const class="type">long id,class="kw">const class="type">int win)      { class="kw">return m_charts.GetLastAddedIndicator(id,win);      }
CWndInd          *ChartGetLastDeletedIndicator(class="type">void)                          { class="kw">return this.m_charts.GetLastDeletedIndicator();    }
CWndInd          *ChartGetLastChangedIndicator(class="type">void)                          { class="kw">return this.m_charts.GetLastChangedIndicator();    }
class=class="str">"cmt">//--- Return the indicator by index from the specified window of the specified chart
CWndInd          *ChartGetIndicator(class="kw">const class="type">long chart_id,class="kw">const class="type">int win_num,class="kw">const class="type">int ind_index)
                 {
                  class="kw">return m_charts.GetIndicator(chart_id,win_num,ind_index);
                 }
class=class="str">"cmt">//--- Return the number of charts in the collection list
class="type">int              ChartsTotal(class="type">void)                                            { class="kw">return this.m_charts.DataTotal();                  }

「用 EA 跑通图表对象的事件回调」

把上一篇文章里的 EA 存到 \MQL5\Experts\TestDoEasy\Part71\ 下,命名 TestDoEasyPart71.mq5,只往 OnDoEasyEvent() 里补一段事件分发代码即可,不用重写整套库事件处理。图表对象集合的每个事件在注释里都带了 lparam、dparam、sparam 三个参数的类型示例,靠这三个值就能在库里定位对象并拼出日志。 编译后挂到任意品种图表,开新图会触发 CHART_OBJ_EVENT_CHART_OPEN,日志打出 symbol+周期+图表 ID;关图对应 CHART_OBJ_EVENT_CHART_CLOSE。加振荡器子窗口、改指标参数、删主图指标也各有独立事件,实测都能从 OnDoEasyEvent() 正常回传,说明事件链路是通的。 外汇和贵金属行情跳动快,这类图表事件回调若用于实盘辅助监控,要先在模拟盘验证延迟与丢事件概率,实盘存在滑点和断线高风险。下面这段是时序与图表事件处理的截取,注意 SERIES_EVENTS_NEW_BAR 用 dparam 传周期、lparam 传时间,图表开关则用 sparam 传品种名。

MQL5 / C++
class=class="str">"cmt">//--- Handling timeseries events
  else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE)
    {
      class=class="str">"cmt">//--- "New bar" event
      if(idx==SERIES_EVENTS_NEW_BAR)
        {
         Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam));
        }
    }

class=class="str">"cmt">//--- Handle chart events
  else if(idx>CHART_OBJ_EVENT_NO_EVENT && idx<CHART_OBJ_EVENTS_NEXT_CODE)
    {
      class=class="str">"cmt">//--- "New chart opening" event
      if(idx==CHART_OBJ_EVENT_CHART_OPEN)
        {
         class=class="str">"cmt">//::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,chart.ID(),chart.Timeframe(),chart.Symbol());
         CChartObj *chart=engine.ChartGetLastOpenedChart();
         if(chart!=NULL)
           {
            class="type">class="kw">string symbol=sparam;
            class="type">long chart_id=lparam;
            ENUM_TIMEFRAMES timeframe=(ENUM_TIMEFRAMES)dparam;
            class="type">class="kw">string header=symbol+" "+TimeframeDescription(timeframe)+", ID "+(class="type">class="kw">string)chart_id;
            Print(DFUN,CMessage::Text(MSG_CHART_COLLECTION_CHART_OPENED),": ",header);
           }
        }
      class=class="str">"cmt">//--- "Chart closure" event
      if(idx==CHART_OBJ_EVENT_CHART_CLOSE)
        {
         class=class="str">"cmt">//::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,chart.ID(),chart.Timeframe(),chart.Symbol());
         CChartObj *chart=engine.ChartGetLastClosedChart();
         if(chart!=NULL)
           {
            class="type">class="kw">string symbol=sparam;
            class="type">long  chart_id=lparam;
            ENUM_TIMEFRAMES timeframe=(ENUM_TIMEFRAMES)dparam;
            class="type">class="kw">string header=symbol+" "+TimeframeDescription(timeframe)+", ID "+(class="type">class="kw">string)chart_id;
            Print(DFUN,CMessage::Text(MSG_CHART_COLLECTION_CHART_CLOSED),": ",header);
           }
        }
      class=class="str">"cmt">//--- "Adding a new window on the chart" event
      if(idx==CHART_OBJ_EVENT_CHART_WND_ADD)
        {

图表子窗口与指标的增删事件追踪

在 MT5 自定义图表引擎里,子窗口的增删和指标挂载都会抛出独立事件代号。上面这段逻辑分别捕获了 CHART_OBJ_EVENT_CHART_WND_ADD、CHART_OBJ_EVENT_CHART_WND_DEL 与 CHART_OBJ_EVENT_CHART_WND_IND_ADD 三类消息,用 sparam / lparam / dparam 把品种名、图表 ID、窗口序号解出来拼成日志头。 注意 ADD 事件里先通过 ChartGetLastOpenedChart() 拿最新图表指针,再顺藤摸瓜取 Timeframe() 和 ChartGetLastAddedChartWindow(),最后用 wnd.GetLastAddedIndicator() 拿指标名——这套嵌套判空是防止多图表并发时取到脏对象的关键。 被注释掉的 EventChartCustom 调用说明作者本想反向通知主图,但临时改为 Print 直接落终端日志。你在复盘 EA 时若发现窗口增减没记录,优先确认这三个事件 idx 是否进了对的分支,而不是怀疑图表对象本身丢了。 外汇与贵金属品种上跑这类监听要留意:高频拖拽指标会瞬间触发大量事件,终端日志可能刷屏,建议只在调试期开 Print,实盘改用稀疏化队列。

MQL5 / C++
class=class="str">"cmt">//::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,wnd.WindowNum(),this.Symbol());
ENUM_TIMEFRAMES timeframe=WRONG_VALUE;
class="type">class="kw">string ind_name="";
class="type">class="kw">string symbol=sparam;
class="type">long  chart_id=lparam;
class="type">int   win_num=(class="type">int)dparam;
class="type">class="kw">string header=symbol+" "+TimeframeDescription(timeframe)+", ID "+(class="type">class="kw">string)chart_id+": ";

CChartObj *chart=engine.ChartGetLastOpenedChart();
if(chart!=NULL)
  {
  timeframe=chart.Timeframe();
  CChartWnd *wnd=engine.ChartGetLastAddedChartWindow(chart.ID());
  if(wnd!=NULL)
    {
    CWndInd *ind=wnd.GetLastAddedIndicator();
    if(ind!=NULL)
      ind_name=ind.Name();
    }
  }
Print(DFUN,header,CMessage::Text(MSG_CHART_OBJ_WINDOW_ADDED)," ",(class="type">class="kw">string)win_num," ",ind_name);
}
class=class="str">"cmt">//--- "Removing a window from the chart" event
if(idx==CHART_OBJ_EVENT_CHART_WND_DEL)
  {
  class=class="str">"cmt">//::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,wnd.WindowNum(),this.Symbol());
  CChartWnd *wnd=engine.ChartGetLastDeletedChartWindow();
  ENUM_TIMEFRAMES timeframe=WRONG_VALUE;
  class="type">class="kw">string symbol=sparam;
  class="type">long  chart_id=lparam;
  class="type">int   win_num=(class="type">int)dparam;
  class="type">class="kw">string header=symbol+" "+TimeframeDescription(timeframe)+", ID "+(class="type">class="kw">string)chart_id+": ";
  Print(DFUN,header,CMessage::Text(MSG_CHART_OBJ_WINDOW_REMOVED)," ",(class="type">class="kw">string)win_num);
  }
class=class="str">"cmt">//--- "Adding a new indicator to the chart window" event
if(idx==CHART_OBJ_EVENT_CHART_WND_IND_ADD)
  {
  class=class="str">"cmt">//::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,this.WindowNum(),ind.Name());
  ENUM_TIMEFRAMES timeframe=WRONG_VALUE;
  class="type">class="kw">string ind_name=sparam;
  class="type">class="kw">string symbol=NULL;
  class="type">long  chart_id=lparam;
  class="type">int   win_num=(class="type">int)dparam;
  class="type">class="kw">string header=NULL;

  CWndInd *ind=engine.ChartGetLastAddedIndicator(chart_id,win_num);
  if(ind!=NULL)

◍ 抓下指标增删改的图表上下文

在图表对象事件引擎里,指标被添加、删除或改参数时会抛出三类独立事件:CHART_OBJ_EVENT_CHART_WND_IND_ADD、CHART_OBJ_EVENT_CHART_WND_IND_DEL 和 CHART_OBJ_EVENT_CHART_WND_IND_CHANGE。光知道事件类型不够,真正有用的是当时指标挂在哪个品种、哪根周期、哪个子窗口。 下面这段处理删除事件的代码,先声明 timeframe 初始为 WRONG_VALUE、symbol 与 header 为 NULL,再从 lparam 取 chart_id、dparam 转成 win_num,sparam 就是指标名。 随后用 engine.ChartGetLastDeletedIndicator() 拿刚删掉的 CWndInd 指针;若非空,再用 chart_id 调 ChartGetChartObj 取图表对象,从中读 Symbol() 与 Timeframe(),并用 GetWindowByNum(win_num) 取子窗口头标题。 最后 Print 把「品种 + 周期描述 + 图表ID + 窗口头 + 删除提示 + 指标名」一次性吐到日志。外汇与贵金属图表多周期切换频繁,这种上下文捕获能让你在回放日志时直接定位是哪张图哪根线被动了——开 MT5 把这段塞进你的图表事件回调,删个指标看打印是否带出正确 symbol 和 timeframe。

MQL5 / C++
   {
      CChartObj *chart=engine.ChartGetChartObj(chart_id);
      if(chart!=NULL)
        {
         symbol=chart.Symbol();
         timeframe=chart.Timeframe();
         CChartWnd *wnd=chart.GetWindowByNum(win_num);
         if(wnd!=NULL)
            header=wnd.Header();
        }
      }
      Print(DFUN,symbol," ",TimeframeDescription(timeframe),", ID ",chart_id,", ",header,": ",CMessage::Text(MSG_CHART_OBJ_INDICATOR_ADDED)," ",ind_name);
      }
   class=class="str">"cmt">//--- "Removing an indicator from the chart window" event
   if(idx==CHART_OBJ_EVENT_CHART_WND_IND_DEL)
      {
      class=class="str">"cmt">//::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,this.WindowNum(),ind.Name());
      ENUM_TIMEFRAMES timeframe=WRONG_VALUE;
      class="type">class="kw">string ind_name=sparam;
      class="type">class="kw">string symbol=NULL;
      class="type">long   chart_id=lparam;
      class="type">int    win_num=(class="type">int)dparam;
      class="type">class="kw">string header=NULL;
      
      CWndInd *ind=engine.ChartGetLastDeletedIndicator();
      if(ind!=NULL)
        {
         CChartObj *chart=engine.ChartGetChartObj(chart_id);
         if(chart!=NULL)
           {
            symbol=chart.Symbol();
            timeframe=chart.Timeframe();
            CChartWnd *wnd=chart.GetWindowByNum(win_num);
            if(wnd!=NULL)
               header=wnd.Header();
           }
        }
      Print(DFUN,symbol," ",TimeframeDescription(timeframe),", ID ",chart_id,", ",header,": ",CMessage::Text(MSG_CHART_OBJ_INDICATOR_REMOVED)," ",ind_name);
      }
   class=class="str">"cmt">//--- "Changing indicator parameters in the chart window" event
   if(idx==CHART_OBJ_EVENT_CHART_WND_IND_CHANGE)
      {
      class=class="str">"cmt">//::EventChartCustom(this.m_chart_id_main,(class="type">class="kw">ushort)event,this.m_chart_id,this.WindowNum(),ind.Name());
      ENUM_TIMEFRAMES timeframe=WRONG_VALUE;
      class="type">class="kw">string ind_name=sparam;
      class="type">class="kw">string symbol=NULL;
      class="type">long   chart_id=lparam;
      class="type">int    win_num=(class="type">int)dparam;

「抓到图表上被改动的指标」

在 MT5 的自定义事件引擎里,想确认用户刚动了哪个指标,核心是用 ChartGetLastChangedIndicator() 拿到变更指针,再顺着 chart_id 和窗口号把实例取回来。下面这段代码就是干这事的最小闭环。 string header=NULL; CWndInd *ind=NULL; CWndInd *ind_changed=engine.ChartGetLastChangedIndicator(); if(ind_changed!=NULL) { ind=engine.ChartGetIndicator(chart_id,win_num,ind_changed.Index()); if(ind!=NULL) { CChartObj *chart=engine.ChartGetChartObj(chart_id); if(chart!=NULL) { symbol=chart.Symbol(); timeframe=chart.Timeframe(); CChartWnd *wnd=chart.GetWindowByNum(win_num); if(wnd!=NULL) header=wnd.Header(); } } } Print(DFUN,symbol," ",TimeframeDescription(timeframe),", ID ",chart_id,", ",header,": ",CMessage::Text(MSG_CHART_OBJ_INDICATOR_CHANGED)," ",ind_name," >>> ",ind.Name()); 逐行看:header 先置空,ind_changed 接住“最后一次变更的指标”指针;若非空,用 ChartGetIndicator 按窗口号与索引取真实实例。接着从 ChartGetChartObj 拿图表对象,扒出 symbol、timeframe,再用 GetWindowByNum 取窗口标题。最后 Print 把“旧名 >>> 新名”打进日志。 实跑日志里能看到具体痕迹:同一图表 ID 131733844391938634 下,AUDNZD H4 主图的 AMA(14,2,30) 被改成 AMA(20,2,30),副窗 Momentum(14) 变成 Momentum(20)。这种输出可直接拿来做指标监控面板的数据源。 外汇与贵金属品种波动受杠杆与消息面影响大,这类事件捕获只解决“知道变了”,不代表信号方向,复盘时仍需结合价格行为判断概率。

MQL5 / C++
class="type">class="kw">string header=NULL;
CWndInd *ind=NULL;
CWndInd *ind_changed=engine.ChartGetLastChangedIndicator();
if(ind_changed!=NULL)
  {
   ind=engine.ChartGetIndicator(chart_id,win_num,ind_changed.Index());
   if(ind!=NULL)
     {
      CChartObj *chart=engine.ChartGetChartObj(chart_id);
      if(chart!=NULL)
        {
         symbol=chart.Symbol();
         timeframe=chart.Timeframe();
         CChartWnd *wnd=chart.GetWindowByNum(win_num);
         if(wnd!=NULL)
            header=wnd.Header();
        }
     }
  }
Print(DFUN,symbol," ",TimeframeDescription(timeframe),", ID ",chart_id,", ",header,": ",CMessage::Text(MSG_CHART_OBJ_INDICATOR_CHANGED)," ",ind_name," >>> ",ind.Name());

画得少,看得清

这一篇把图表对象集合的自动更新收了尾,下一步准备让属性变化自动跟踪、统一管理所有图表对象的参数。当前函数库版本和测试 EA 已打包在 3962.05 KB 的 ZIP 里,直接丢进 MT5 的 MQL5 目录就能编译跑通。 外汇和贵金属图表对象脚本改动参数属于高频操作,行情跳空时对象刷新可能滞后,实盘前务必在策略测试器用历史数据验证一遍。 代码先别急着加新功能,把现有集合的更新逻辑在多个品种上跑稳,比追新特性更实在。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到图表对象变动的实时事件流,你只需决定怎么响应。

常见问题

批量编程改动若在同一计时器跳动内完成,事件可能重叠,库里只记录最后一个并尽量存副本,但并发处理未深度微调,手动逐对象操作更稳。
库把删除的图表、窗口、指标对象存进特殊列表,并为指标对象加所在窗口索引属性,从列表取删除前副本即可读原索引。
可以,小布盯盘对应品种页已内置对象变动事件流,不需要自己写监听,打开就能看增删改记录。
在 Defines.mqh 里新建图表事件枚举,注册对应事件时发送枚举常量,主程序按代码分支处理。
图表对象整数属性枚举从 66 扩到 67,并加入按窗口索引排序的标准,避免访问时越界。