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

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

第 1/3 篇

让图表对象集合自己刷新

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 造成的相等是假平静,下次有新窗口时仍要靠不等判断来补救。

MQL5 / C++
  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_DATETIME_MINUTESTIME_SECONDS) 把本地时间格式化成“年月日时分秒”字符串。空格会被 StringReplace 换成下划线,避免文件系统歧义。

想验证这套命名,把 EA 拖进黄金 XAUUSD 的 M5 图,在 OnStart 里调一次 FileNameWithTimeLocal("test") 并打印,终端日志会吐出类似 'YourEA_test_20240115_143022' 的字串;外汇与贵金属杠杆高、滑点大,多图表脚本占用终端资源,实盘前务必在模拟盘跑通。

MQL5 / C++
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 就能编译验证。 外汇与贵金属图表多窗口联动风险高,坐标换算偏差可能导致画线错位,实盘前请在策略测试器用历史数据跑一遍。

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

常见问题

给对象集合类加一个定时或事件触发的更新方法,每次新对象出现就重扫一遍图表,别只在初始化时读一次。
旧逻辑只按窗口下标遍历,没校验窗口是否真实存在;改成先取有效窗口句柄再比对,就能把幽灵项过滤掉。
可以,小布能按你设定的品种页持续扫描图表对象变化并提示漏检项,把重复检查交给它就行。
把子窗口坐标与挂在其上的指标实例做成绑定结构,统一用窗口ID检索,避免硬编码下标错位。
换图表或重命名就会找不到资源;改成从配置读前缀和路径,或用相对目录约定,迁移时不报错。