DoEasy 库中的其他类(第七十部分):扩展功能并自动更新图表对象集合·进阶篇
(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 差值触发的重建动作。外汇与贵金属杠杆高,这类底层封装仅降低编码错误概率,不预示任何行情方向。
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 坐标换算偏离真实成交价,实盘前务必在策略测试器用历史数据校验。
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。外汇与贵金属波动剧烈,用脚本批量截图画线复盘时仍要留意滑点导致的历史重绘风险。
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 缩放偏移。
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 的左侧供给区比手动拖省事得多。
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,不理会坐标轴占用,适合做统一缩略图但会裁掉边缘。外汇与贵金属波动剧烈、截图仅作复盘留痕,不构成任何方向判断,实盘高风险自负。
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,")");