DoEasy 库中的其他类(第七十部分):扩展功能并自动更新图表对象集合·进阶篇
🗂️

DoEasy 库中的其他类(第七十部分):扩展功能并自动更新图表对象集合·进阶篇

(2/3)· 从修复主窗口添加漏洞到自动追踪图表增删,打通对象集合实时同步

偏理论 第 2/3 篇
手动维护图表对象集合时,最容易漏掉的是终端里临时新建或关闭的品种图表,集合状态悄无声息就失准了。本篇把事件跟踪接进库内部,让对象集合自己跟着终端走,省掉你反复刷新的功夫。

图表窗口坐标与指标挂载的底層封装

在 MT5 自定义类里,把时间/价格换算成窗口像素坐标是高频需求。CChartWnd 用 TimePriceToXY() 直接调系统函数 ChartTimePriceToXY,失败返回 false,成功就把 x/y 写进成员变量 m_wnd_coord_x、m_wnd_coord_y,初始值均为 0。 YFromTimePrice() 返回光标在窗口内的绝对 Y 坐标(m_wnd_coord_y),而 YFromTimePriceRelative() 用 m_wnd_coord_y 减去 YDistance() 得到相对坐标,这对画偏移标签很有用。 Refresh() 里有一行关键判断:用 ChartIndicatorsTotal(chart_id, wnd_num) 减去已挂载列表 m_list_ind.Total(),若差值 change 不为 0,就先 IndicatorsDelete() 再 IndicatorsAdd() 重建。也就是说,当子窗口指标数量变动时,类内部会自动同步,省去手动维护。 开 MT5 建个子类继承这套逻辑,挂 EURUSD 的副图指标,改一个指标再调 Refresh(),能在日志里看到 Total 差值触发的重建动作。外汇与贵金属杠杆高,这类底层封装仅降低编码错误概率,不预示任何行情方向。

MQL5 / C++
class="type">int YFromTimePrice(class="type">void) class="kw">const { class="kw">return this.m_wnd_coord_y; }
class=class="str">"cmt">//--- Return the relative Y coordinate of the cursor location in the window
class="type">int YFromTimePriceRelative(class="type">void) class="kw">const { class="kw">return this.m_wnd_coord_y-this.YDistance(); }

};
class=class="str">"cmt">//+------------------------------------------------------------------+
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) : m_window_num(wnd_num),
                     m_wnd_coord_x(class="num">0),
                     m_wnd_coord_y(class="num">0)
  {
  CBaseObj::SetChartID(chart_id);
  this.IndicatorsListCreate();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convert chart coordinates from the time/price representation      |
class=class="str">"cmt">//| to X and Y coordinates                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartWnd::TimePriceToXY(class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price)
  {
  ::ResetLastError();
  if(!::ChartTimePriceToXY(this.m_chart_id,this.m_window_num,time,price,this.m_wnd_coord_x,this.m_wnd_coord_y))
     {
     class=class="str">"cmt">//CMessage::ToLog(DFUN,::GetLastError(),true);
     class="kw">return false;
     }
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update data on attached indicators                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CChartWnd::Refresh(class="type">void)
  {
  class="type">int change=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num)-this.m_list_ind.Total();
  if(change!=class="num">0)
     {
     this.IndicatorsDelete();
     this.IndicatorsAdd();
  }
  }

◍ 图表类里的窗口坐标与时间锚点

CChartObj 在继承 CBaseObj 之后,用三个定长数组分别缓存整型、双精度与字符串类的图表属性,下标直接对应 CHART_PROP_INTEGER_TOTAL 等枚举总量,避免每次读属性都走终端接口。 私有成员里有两个被高亮处理的字段值得注意:m_wnd_time_x 存的是窗口图表上 X 坐标对应的时间(datetime 类型),m_wnd_price_y 存的是 Y 坐标对应的价格(double 类型)。这两个值就是把鼠标位置反推成时间—价格坐标的关键中间量,做框选区间回测或手动标注时直接用。 顺带看一眼文件头,#property strict 被注明是为 MQL4 兼容准备的,而版本号固定在 1.00、版权归 MetaQuotes Ltd.(2021)。如果你在 MT5 里 include 这套 ChartObj.mqh,路径里 ..\..\Services\Select.mqh 必须存在,否则编译期就报找不到文件。 外汇与贵金属杠杆品种波动剧烈,用这类坐标转换做自动标注时,滑点和点值误差可能让 Y 坐标换算偏离真实成交价,实盘前务必在策略测试器用历史数据校验。

MQL5 / C++
class="type">int WindowsTotal(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CHART_PROP_WINDOWS_TOTAL); }

class="macro">#include "..\..\Objects\BaseObj.mqh"
class="macro">#include "..\..\Services\Select.mqh"
class="macro">#include "ChartWnd.mqh"

class CChartObj : class="kw">public CBaseObj
  {
class="kw">private:
   CArrayObj         m_list_wnd;            class=class="str">"cmt">// List of chart window objects
   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
  };

「图表窗口对象的接口封装细节」

这段类声明把多窗口图表的访问与操作收口到一个 CChart 派生结构里。私有的 CreateWindowsList 负责在初始化阶段把当前图表的所有子窗口枚举进链表,FileNameWithExtention 则在截图保存时补上缺失的后缀名,避免 MT5 写出无格式文件。 对外暴露的接口里,IsMainChart 用一行 return 比对 m_chart_id 与基类拿到的主图表 ID,判断对象是否挂在程序所在主图;GetWindowByIndex 直接走 m_list_wnd.At(index) 取指针,索引越界会返回 NULL,调用方必须自己判空。 按指标名抓窗口的 GetWindowByIndicator 在实盘脚本里很实用——比如你想对 loaded 的 MACD 子窗做截图或参数打印,不必 hardcode 窗口号。PrintWndIndicators 和 PrintWndParameters 会把全部子窗指标数据刷到专家日志,调试多指标布局时省得肉眼核对。 导航与截图一组方法覆盖了常见需求:Navigate 按 ENUM_CHART_POSITION 做相对位移,NavigateLeft/Right 是简化封装;ScreenShotWndSize 默认 800×600、另带 750×562 预设,对齐模式默认 ALIGN_CENTER。外汇与贵金属波动剧烈,用脚本批量截图画线复盘时仍要留意滑点导致的历史重绘风险。

MQL5 / C++
class=class="str">"cmt">//--- Create the list of chart windows
   class="type">void                 CreateWindowsList(class="type">void);
class=class="str">"cmt">//--- Add an extension to the screenshot file if it is missing
   class="type">class="kw">string               FileNameWithExtention(class="kw">const class="type">class="kw">string filename);

class="kw">public:
class=class="str">"cmt">//--- Return the flag indicating that the chart object belongs to the program chart
   class="type">bool                 IsMainChart(class="type">void)                                      class="kw">const { class="kw">return(this.m_chart_id==CBaseObj::GetMainChartID()); }
class=class="str">"cmt">//--- Return the chart window specified by index
   CChartWnd            *GetWindowByIndex(class="kw">const class="type">int index)                     class="kw">const { class="kw">return this.m_list_wnd.At(index); }
class=class="str">"cmt">//--- Return the window object by its subwindow index
   CChartWnd            *GetWindowByNum(class="kw">const class="type">int win_num)                     class="kw">const;
class=class="str">"cmt">//--- Return the window object by the indicator name in it
   CChartWnd            *GetWindowByIndicator(class="kw">const class="type">class="kw">string ind_name)           class="kw">const;

class=class="str">"cmt">//--- Display data of all indicators of all chart windows in the journal
   class="type">void                 PrintWndIndicators(class="type">void);
class=class="str">"cmt">//--- Display the properties of all chart windows in the journal
   class="type">void                 PrintWndParameters(class="type">void);
class=class="str">"cmt">//--- Shift the chart by the specified number of bars relative to the specified chart position
   class="type">bool                 Navigate(class="kw">const class="type">int shift,class="kw">const ENUM_CHART_POSITION position);
class=class="str">"cmt">//--- Shift the chart(class="num">1) to the left and(class="num">2) to the right by the specified number of bars
   class="type">bool                 NavigateLeft(class="kw">const class="type">int shift);
   class="type">bool                 NavigateRight(class="kw">const class="type">int shift);
class=class="str">"cmt">//--- Shift the chart(class="num">1) to the beginning and(class="num">2) to the end of the history data
   class="type">bool                 NavigateBegin(class="type">void);
   class="type">bool                 NavigateEnd(class="type">void);
class=class="str">"cmt">//--- Create the chart screenshot
   class="type">bool                 ScreenShot(class="kw">const class="type">class="kw">string filename,class="kw">const class="type">int width,class="kw">const class="type">int height,class="kw">const ENUM_ALIGN_MODE align);
class=class="str">"cmt">//--- Create the screenshot of the(class="num">1) chart window, (class="num">2) 800х600 and(class="num">3) 750х562 pixels
   class="type">bool                 ScreenShotWndSize(class="kw">const class="type">class="kw">string filename=NULL,class="kw">const ENUM_ALIGN_MODE align=ALIGN_CENTER);

图表对象里的截图与坐标换算接口

CChartObj 类封装了一组直接操作 MT5 图表的方法,其中截图函数支持两种固定分辨率:ScreenShot800x600 输出 800×600 像素,ScreenShot750x562 输出 750×562 像素,对齐方式默认 ALIGN_CENTER,可通过 align 参数改为左上或右下。 模板存取由 SaveTemplate 与 ApplyTemplate 完成,二者均接受 filename 参数,缺省传 NULL 时按图表默认名处理。做批量复盘时,可先 SaveTemplate 存一套带自定义指标的设置,再在其它品种图上 ApplyTemplate 一键还原。 坐标反查是这类封装的实用点。XYToTimePrice 接收屏幕 x、y 像素坐标,内部写入 m_wnd_time_x 与 m_wnd_price_y;随后 TimeFromXY 返回对应 datetime,PriceFromXY 返回对应 double 价格。 构造函数 CChartObj::CChartObj 仅接收 chart_id,初始化列表将 m_wnd_time_x 与 m_wnd_price_y 置 0,意味着未调用坐标换算前读出的时间价格无效。GetWindowByIndicator 则依据指标名调 ChartWindowFind 定位子窗口,当自身是指标且 ind_name 为 NULL 时用无参版本,否则带 chart_id 查找。 外汇与贵金属市场波动剧烈、杠杆风险高,用上述接口做自动截图或坐标标注时,建议先在模拟账户验证像素映射是否因 DPI 缩放偏移。

MQL5 / C++
class="type">bool ScreenShot800x600(class="kw">const class="type">class="kw">string filename=NULL,class="kw">const ENUM_ALIGN_MODE align=ALIGN_CENTER);
class="type">bool ScreenShot750x562(class="kw">const class="type">class="kw">string filename=NULL,class="kw">const ENUM_ALIGN_MODE align=ALIGN_CENTER);

class=class="str">"cmt">//--- Save the chart class="kw">template with the current settings
class="type">bool SaveTemplate(class="kw">const class="type">class="kw">string filename=NULL);
class=class="str">"cmt">//--- Apply the specified class="kw">template to the chart
class="type">bool ApplyTemplate(class="kw">const class="type">class="kw">string filename=NULL);

class=class="str">"cmt">//--- Convert X and Y chart window coordinates into time and price
class="type">int XYToTimePrice(class="kw">const class="type">long x,class="kw">const class="type">class="kw">double y);
class=class="str">"cmt">//--- Return(class="num">1) time and(class="num">2) price from XY coordinates
class="type">class="kw">datetime TimeFromXY(class="type">void) class="kw">const { class="kw">return this.m_wnd_time_x; }
class="type">class="kw">double PriceFromXY(class="type">void) class="kw">const { class="kw">return this.m_wnd_price_y; }

};

CChartObj::CChartObj(class="kw">const class="type">long chart_id) : m_wnd_time_x(class="num">0),m_wnd_price_y(class="num">0)
  {
  }

CChartWnd *CChartObj::GetWindowByIndicator(class="kw">const class="type">class="kw">string ind_name) class="kw">const
  {
  class="type">int index=(this.m_program==PROGRAM_INDICATOR && ind_name==NULL ? ::ChartWindowFind() : ::ChartWindowFind(this.m_chart_id,ind_name));
  class="kw">return this.GetWindowByIndex(index);
  }

class="type">bool CChartObj::Navigate(class="kw">const class="type">int shift,class="kw">const ENUM_CHART_POSITION position)
  {
  ::ResetLastError();

◍ 图表平移与截图的封装逻辑

在 MT5 自定义图表类里,左移、右移、跳到历史起点或终点,本质都收敛到同一个私有 Navigate 方法,只是传入的 position 参数和 shift 符号不同。左移用正 shift 配 CHART_CURRENT_POS,右移把 shift 取负,起点用 CHART_BEGIN、终点用 CHART_END,且 shift 填 0。 每次平移前都会先调 SetAutoscrollOFF(),否则自动滚动会把你的定位瞬间扯回最新 K 线,这在回测截图或人工复核形态时尤其烦人。外汇与贵金属品种的高波动特性下,错位的图表可能造成误判,属高风险操作环境。 截图方法 ScreenShot 先 ResetLastError() 清掉旧错误码,再调 ChartScreenShot;若失败,下方代码段会进错误分支写日志。宽度高度由调用方传,align 控制对齐模式,实盘里常用 800×600 抓 EURUSD 的 M15 结构。 让小布替你跑这套 把 NavigateLeft(50) 接在 OnTimer 里,每 30 秒推一次,肉眼看黄金 H1 的左侧供给区比手动拖省事得多。

MQL5 / C++
class="type">bool res=::ChartNavigate(m_chart_id,position,shift);
if(!res)
   CMessage::ToLog(DFUN,::GetLastError(),true);
class="kw">return res;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Shift the chart to the left by the specified number of bars       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartObj::NavigateLeft(class="kw">const class="type">int shift)
  {
   this.SetAutoscrollOFF();
   class="kw">return this.Navigate(shift,CHART_CURRENT_POS);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Shift the chart to the right by the specified number of bars      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartObj::NavigateRight(class="kw">const class="type">int shift)
  {
   this.SetAutoscrollOFF();
   class="kw">return this.Navigate(-shift,CHART_CURRENT_POS);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Shift the chart to the beginning of the history data              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartObj::NavigateBegin(class="type">void)
  {
   this.SetAutoscrollOFF();
   class="kw">return this.Navigate(class="num">0,CHART_BEGIN);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Shift the chart to the end of the history data                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartObj::NavigateEnd(class="type">void)
  {
   this.SetAutoscrollOFF();
   class="kw">return this.Navigate(class="num">0,CHART_END);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create the chart screenshot                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CChartObj::ScreenShot(class="kw">const class="type">class="kw">string filename,class="kw">const class="type">int width,class="kw">const class="type">int height,class="kw">const ENUM_ALIGN_MODE align)
  {
   ::ResetLastError();
   if(!::ChartScreenShot(m_chart_id,filename,width,height,align))
    {

「按窗口分辨率截图与固定尺寸截图的差异」

CChartObj 里提供了两种截图路径:ScreenShotWndSize 按图表窗口实际像素生成,ScreenShot800x600 则强制输出 800x600。前者在保留价格轴和时间轴的前提下,宽度会加上 56 像素(若显示价格轴)、高度加上 15 像素(若显示时间轴),实际尺寸随品种窗口布局浮动。 看 ScreenShotWndSize 的取窗口逻辑:它拿 m_list_wnd.Total()-1 作为序号去取最后一个窗口对象,若返回 NULL 就直接打印失败并返回 false,这里依赖列表非空,空图表可能直接截不到。 尺寸计算那两行值得在 MT5 里实测:width=this.WidthInPixels()+(IsShowPriceScale()?56:0),height=wnd.YDistance()+wnd.HeightInPixels()+(this.IsShowDateScale()?15:0)。也就是说价格轴关掉能省 56 像素宽,时间轴关掉省 15 像素高,做批量截图脚本时这个偏移量容易被人忽略。 ScreenShot800x600 就简单粗暴,写死 width=800、height=600,不理会坐标轴占用,适合做统一缩略图但会裁掉边缘。外汇与贵金属波动剧烈、截图仅作复盘留痕,不构成任何方向判断,实盘高风险自负。

MQL5 / C++
class="type">bool CChartObj::ScreenShotWndSize(class="kw">const class="type">class="kw">string filename=NULL,class="kw">const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
class=class="str">"cmt">//--- Create the file name or use the one passed to the method
   class="type">class="kw">string name=
     (filename==NULL || filename=="" ?
       SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :
       this.FileNameWithExtention(filename)
     );
class=class="str">"cmt">//--- Get the chart window having the largest number of all windows
   CChartWnd *wnd=this.GetWindowByNum(this.m_list_wnd.Total()-class="num">1);
   if(wnd==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ),class="type">class="kw">string(this.m_list_wnd.Total()-class="num">1));
      class="kw">return false;
     }
class=class="str">"cmt">//--- Calculate the screenshot width and height considering the size of the price and time scales
   class="type">int width=this.WidthInPixels()+(IsShowPriceScale() ? class="num">56 : class="num">0);
   class="type">int height=wnd.YDistance()+wnd.HeightInPixels()+(this.IsShowDateScale() ? class="num">15 : class="num">0);
class=class="str">"cmt">//--- Create a screenshot and class="kw">return the result of the ScreenShot() method
   class="type">bool res=this.ScreenShot(name,width,height,align);
   if(res)
     ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(class="type">class="kw">string)width," x ",(class="type">class="kw">string)height,")");
   class="kw">return res;
  }

class="type">bool CChartObj::ScreenShot800x600(class="kw">const class="type">class="kw">string filename=NULL,class="kw">const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
   class="type">class="kw">string name=
     (filename==NULL || filename=="" ?
       SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :
       this.FileNameWithExtention(filename)
     );
   class="type">int width=class="num">800;
   class="type">int height=class="num">600;
   class="type">bool res=this.ScreenShot(name,width,height,align);
   if(res)
     ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(class="type">class="kw">string)width," x ",(class="type">class="kw">string)height,")");
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到图表对象与窗口的实时同步状态,把重复劳动交给小布,你专注决策。

常见问题

上篇测试暴露了主窗口新增窗口时的引用更新缺失,本篇在集合类里修复了窗口列表的添加与删除逻辑,保证主窗口始终至少存在一个窗口对象。
覆盖品种图表的新建与删除、图表内指标窗口的增删,以及窗口内指标的添加移除,全部由集合类在事件层自动监听并同步。
截图落于 MQL5/Files/DoEasy/ScreenShots,模板存到 MQL5/Profiles/Templates/DoEasy,宏替换明确了各自目录与默认扩展名。
可以,小布盯盘的品种页已内置这类 AIGC 诊断,能呈现图表对象与窗口的自动追踪状态,不必自己跑库代码验证。
服务函数拼接程序名、传入前缀和本地 PC 时间,空格转下划线、冒号转句点,返回唯一文件名,替换失败会在日志报错。