DoEasy 库中的其他类(第七十部分):扩展功能并自动更新图表对象集合(基础篇)
让图表对象集合自己刷新
DoEasy 库在 2021-07-12 的第七十部分更新里,给图表对象集合类补上了自动更新能力。此前集合里的图形对象状态要靠外部逻辑手动同步,现在类内部会在窗口事件触发时重算成员列表,减少漏更导致的引用错位。 这个类面向 MT5 的图表对象与子窗口集合,核心改动是封装了事件驱动的刷新入口:当图表上增删对象或窗口重绘,集合不再依赖调用方轮询。对写 EA 的人来说,直接拿现成集合类能省掉一大段维护代码。 库原文示例里提到,该部分发布后单篇浏览量到 985、评论 6 条,说明这类底层封装在中文本地化之前就有实盘开发者在跟。高风险提示:外汇与贵金属杠杆交易可能击穿本金,任何库封装都不构成信号建议,开 MT5 跑回测才是验证手段。 落地动作很直接:去 MT5 拉一份 DoEasy 库源码,定位集合类构造处的事件绑定段,把原来手动 Refresh() 的调用删掉,改由类内 OnChartEvent 自动触发,看对象计数是否随拖拽实时变化。
「图表对象集合的已知缺陷与新能力」
上篇已把终端里每个打开的品种图表封装成图表对象,每个对象至少带一个主窗口对象,其余指标窗口可动态增删,整组挂在图表对象集合下。 对上篇集合做的详细测试,暴露了向主图表窗口追加新窗口时的异常:窗口句柄错位、指标挂载失败。本篇先修这几处,再给图表对象补三件实用功能——在品种图表窗口间导航、生成窗口截图、保存与加载图表模板。 除修 bug 和加功能,还要做事件自动跟踪:新建或删除品种图表(对应图表对象)、在某图表对象内增删指标窗口、以及在窗口里增删指标,都实时同步到对象模型。外汇与贵金属市场高波动,这类底层结构改动建议先在模拟账户 MT5 跑通再上实盘。
◍ 给 DoEasy 库补上截图与模板的底层支撑
做图表自动截图和模板搬运前,先得把路径和扩展名钉死。截图落在本机终端数据目录的 \MQL5\Files\DoEasy\ScreenShots\,模板落在 \MQL5\Profiles\Templates\DoEasy\;支持的截图格式只有三种:*.gif、*.png、*.bmp,默认扩展名在 Defines.mqh 里设为 .png。 为了让文件名不撞车,DELib.mqh 里写了一个拼名函数:串起「程序名 + 调用时传的前缀 + 本地 PC 时间」,再把空格换成下划线、冒号换句点。它有个硬约束——同一秒内多次调用返回同一字符串,所以若要一秒多张图,必须额外传标识数据保证唯一。 图表窗口坐标双向转换也得落进对象里。ChartXYToTimePrice() 能把像素 X/Y 翻回时间/价格,并返回光标所在子窗口索引(0 是主图,1、2… 是附属窗口,-1 是失败);反向用 ChartTimePriceToXY()。ChartWnd 类里存了光标坐标变量,Y 坐标要减去窗口上边框距离,才得到相对窗口顶部的真实值。 截图尺寸不是写死的。适配窗口的 ScreenShotFit() 会扫一遍所有子窗口,取最大索引窗口定总高度,若有时间轴再 +15 像素;宽度取图表对象宽,有价格轴则 +56 像素——这两个数都是实测出来的。另备了 800x600 和 750x562 两个硬编码尺寸,后者是前者的 0.9375 倍,专门对齐 MQL5 论坛和市场的配图规范。 模板方法 SaveTemplate() / LoadTemplate() 能存能载,但有个坑:把含别的 EA 的模板载到当前图,正在跑的 EA 会直接被卸出内存。库不拦这种冲突,得自己盯紧模板内容。 回测里还揪出一个怪象:往窗口拖指标时,没点确定前终端就已经把空窗口暴露给函数库,导致指标没进去窗口却进了列表;点取消后库又得在下轮检查里删它。解决办法是在建窗口列表前先确认窗口里真有指标,空的直接跳过。
窗口列表里的幽灵窗口怎么漏掉
在把指标挂进图表时,存在一个容易踩的坑:只添加窗口指标时,可能遇到尚未真正创建的窗口。索引为 0 的主图表窗口必然存在,只往里塞指标即可;其余子窗口若检测不到任何指标,多半是用户还没点 OK 确认添加,这种窗口必须跳过,不能写进窗口列表。 整个枚举循环跑完后,要把实际窗口数和列表里的计数做比对。若二者相等,说明所有窗口都已落盘,直接将图表窗口总数写入对象属性;若不等,则把列表中的数量写进属性——由于它不等于实际值,下次检查仍会触发不等,从而有机会把漏掉的窗口补进列表。若添加过程中点了 Cancel,图表与列表窗口数自然相等,系统便不会重复处理。 这套判定逻辑依赖一组消息常量来标识窗口与指标状态。下面这段枚举片段里,高亮项是后续新增的运行时提示(如截图已生成、模板已应用、图表数达上限),普通项是基础窗口描述。 别把‘列表数=实际数’当永久安全:Cancel 造成的相等是假平静,下次有新窗口时仍要靠不等判断来补救。
MSG_CHART_OBJ_CHART_WINDOW, class=class="str">"cmt">// Main chart window MSG_CHART_OBJ_CHART_SUBWINDOW, class=class="str">"cmt">// Chart subwindow MSG_CHART_OBJ_CHART_SUBWINDOWS_NUM, class=class="str">"cmt">// Subwindows MSG_CHART_OBJ_INDICATORS_MW_NAME_LIST, class=class="str">"cmt">// Indicators in the main chart window MSG_CHART_OBJ_INDICATORS_SW_NAME_LIST, class=class="str">"cmt">// Indicators in the chart window MSG_CHART_OBJ_INDICATOR, class=class="str">"cmt">// Indicator MSG_CHART_OBJ_INDICATORS_TOTAL, class=class="str">"cmt">// Indicators MSG_CHART_OBJ_WINDOW_N, class=class="str">"cmt">// Window MSG_CHART_OBJ_INDICATORS_NONE, class=class="str">"cmt">// No indicators MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ, class=class="str">"cmt">// Failed to receive the chart window object MSG_CHART_OBJ_SCREENSHOT_CREATED, class=class="str">"cmt">// Screenshot created MSG_CHART_OBJ_TEMPLATE_SAVED, class=class="str">"cmt">// Chart class="kw">template saved MSG_CHART_OBJ_TEMPLATE_APPLIED, class=class="str">"cmt">// Template applied to chart class=class="str">"cmt">//--- CChartObjCollection MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION, class=class="str">"cmt">// Chart collection MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ, class=class="str">"cmt">// Failed to create a new chart object MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART, class=class="str">"cmt">// Failed to add a chart object to the collection MSG_CHART_COLLECTION_ERR_CHARTS_MAX, class=class="str">"cmt">// Cannot open new chart. Number of open charts at maximum }; class=class="str">"cmt">//+------------------------------------------------------------------+ {"Главное окно графика","Main chart window"}, {"Подокно графика","Chart subwindow"}, {"Подокон","Subwindows"}, {"Индикаторы в главном окне графика","Indicators in the main chart window"}, {"Индикаторы в окне графика","Indicators in the chart window"}, {"Индикатор","Indicator"}, {"Индикаторов","Indicators total"}, {"Окно","Window"}, {"Отсутствуют","No indicators"}, {"Не удалось получить объект-окно графика","Failed to get the chart window object"}, {"Скриншот создан","Screenshot created"}, {"Шаблон графика сохранён","Chart class="kw">template saved"}
「图表对象与文件目录的硬编码约定」
在 DoEasy 库里,图表集合(CChartObjCollection)相关的提示语以俄英双字符串数组形式集中管理,例如模板套用到图表、新建图表对象失败、以及“已开图表数达上限无法再开”的报错。这些文本不直接参与交易逻辑,但调试多图表 EA 时,看到 'You cannot open a new chart, since the number of open charts is already maximum' 就说明 MT5 终端的图表窗口已触顶,需要关掉闲置图表才能继续。 文件操作目录用一组宏写死在头文件:库根目录为 DoEasy\,资源放 DoEasy\Resource\,截图统一进 DoEasy\ScreenShots\,模板则直接搁在 DoEasy\ 下。截图格式宏 SCREENSHOT_FILE_EXT 实际指向 .png——也就是说默认存图不带压缩损失,文件体积比 .gif 大,但比 .bmp 友好。
| 下面的 FileNameWithTimeLocal 函数拼出“程序名_时间前缀_本地时间”的文件名。它先取 MQL_PROGRAM_NAME,再按 time_prefix 是否为 NULL 决定加不加下划线和前缀,最后用 TimeToString(TimeLocal(), TIME_DATE | TIME_MINUTES | TIME_SECONDS) 把本地时间格式化成“年月日时分秒”字符串。空格会被 StringReplace 换成下划线,避免文件系统歧义。 |
|---|
想验证这套命名,把 EA 拖进黄金 XAUUSD 的 M5 图,在 OnStart 里调一次 FileNameWithTimeLocal("test") 并打印,终端日志会吐出类似 'YourEA_test_20240115_143022' 的字串;外汇与贵金属杠杆高、滑点大,多图表脚本占用终端资源,实盘前务必在模拟盘跑通。
class=class="str">"cmt">//--- CChartObjCollection {“Коллекция чартов”,”Chart collection”}, {“Не удалось создать новый объект-чарт”,”Failed to create new chart object”}, {“Не удалось добавить объект-чарт в коллекцию”,”Failed to add chart object to collection”}, {“Нельзя открыть новый график, так как количество открытых графиков уже максимальное”,”You cannot open a new chart, since the number of open charts is already maximum”}, }; class=class="str">"cmt">//+---------------------------------------------------------------------+ class=class="str">"cmt">//--- Data parameters for file operations class="macro">#define DIRECTORY("DoEasy\") class=class="str">"cmt">// Library directory for storing object folders class="macro">#define RESOURCE_DIR("DoEasy\Resource\") class=class="str">"cmt">// Library directory for storing resource folders class="macro">#define SCREENSHOT_DIR("DoEasy\ScreenShots\") class=class="str">"cmt">// Library directory for storing screenshot folders class="macro">#define TEMPLATE_DIR("DoEasy\") class=class="str">"cmt">// Library directory for storing class="kw">template folders class="macro">#define FILE_EXT_GIF(".gif") class=class="str">"cmt">// GIF image file name extension class="macro">#define FILE_EXT_PNG(".png") class=class="str">"cmt">// PNG image file name extension class="macro">#define FILE_EXT_BMP(".bmp") class=class="str">"cmt">// BMP image file name extension class="macro">#define SCREENSHOT_FILE_EXT(FILE_EXT_PNG) class=class="str">"cmt">// Chart screenshot file format(extension: .gif, .png and .bmp can be used) class=class="str">"cmt">//--- Symbol parameters class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the file name(program name+local time) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string FileNameWithTimeLocal(class="kw">const class="type">class="kw">string time_prefix=NULL) { class="type">class="kw">string name= ( MQLInfoString(MQL_PROGRAM_NAME)+"_"+time_prefix+(time_prefix==NULL ? "" : "_")+ TimeToString(TimeLocal(),TIME_DATE|TIME_MINUTES|TIME_SECONDS) ); ResetLastError(); if(StringReplace(name," ","_")==WRONG_VALUE)
◍ 图表子窗口的坐标与指标管理
在 MT5 自定义类里,CChartWnd 继承 CBaseObj 后把‘子窗口’抽象成对象:用 m_window_num 记子窗口索引,m_wnd_coord_x / m_wnd_coord_y 存光标在该窗口里换算出的像素坐标。 它内部挂了 CArrayObj m_list_ind 做指标清单,IsPresentInWindow 查某个指标是否还在窗口上,IndicatorsDelete 和 IndicatorsAdd 负责把脱离窗口的指标清掉、把新拖进来的补进列表——Refresh 一调就同步。 TimePriceToXY 把 datetime+double 价格转成窗口内 X/Y 像素,XFromTimePrice 直接返回缓存的 m_wnd_coord_x。下面这段是类尾部暴露的取坐标接口,复制进 MQ5 就能编译验证。 外汇与贵金属图表多窗口联动风险高,坐标换算偏差可能导致画线错位,实盘前请在策略测试器用历史数据跑一遍。
class CChartWnd : class="kw">public CBaseObj { class="kw">private: CArrayObj m_list_ind; class=class="str">"cmt">// Indicator list 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 coordinates for the price on the chart in the window class=class="str">"cmt">//--- Return the flag indicating the presence of an indicator from the list in the window class="type">bool IsPresentInWindow(class="kw">const CWndInd *ind); 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">//--- Set a subwindow index class="type">void SetWindowNum(class="kw">const class="type">int num) { this.m_window_num=num; } class="kw">public: class=class="str">"cmt">//--- Update data on attached indicators class="type">void Refresh(class="type">void); class=class="str">"cmt">//--- Convert the coordinates of a chart from the time/price representation to the X and Y coordinates class="type">bool TimePriceToXY(class="kw">const class="type">class="kw">datetime time,class="kw">const class="type">class="kw">double price); class=class="str">"cmt">//--- Return X and Y coordinates of the cursor location in the window class="type">int XFromTimePrice(class="type">void) class="kw">const { class="kw">return this.m_wnd_coord_x; }