DoEasy 库中的其他类(第七十部分):扩展功能并自动更新图表对象集合
Artyom Trishkin | 12 七月, 2021
内容
概述
在上一篇文章中,我已创建了图表对象集合。 现在,在终端中打开的每个品种图表均由图表对象来表述。 每个图表对象都有一组窗口对象,其内包含窗口指标对象。 任何图表对象都至少有一个窗口对象 — 这是主图表窗口。 所有其余的指标窗口都可以添加到图表窗口列表之中,或从其中删除。 我已将整个对象集合置于图表对象集合当中。
来自上篇文章中图表对象集合的详细测试,暴露了一些向主图表窗口添加新窗口时会出现的问题。 我会在这里修复它们。 此外,我将为图表对象添加新功能 — 在交易品种图表窗口内导航、创建窗口屏幕截图、以及为图表保存和上传模板。
除了预期的改进之外,我还将实现自动跟踪客户端图表和图表对象窗口中发生的一切事件 — 添加新建/删除存在的品种图表(图表对象),添加新建/删除一个图表对象中的现有指标窗口,以及从图表窗口添加新建/删除现有指标。
改进库类
如同以往,文件 \MQL5\Include\DoEasy\Data.mqh 接收新消息的索引:
MSG_CHART_OBJ_CHART_WINDOW, // Main chart window MSG_CHART_OBJ_CHART_SUBWINDOW, // Chart subwindow MSG_CHART_OBJ_CHART_SUBWINDOWS_NUM, // Subwindows MSG_CHART_OBJ_INDICATORS_MW_NAME_LIST, // Indicators in the main chart window MSG_CHART_OBJ_INDICATORS_SW_NAME_LIST, // Indicators in the chart window MSG_CHART_OBJ_INDICATOR, // Indicator MSG_CHART_OBJ_INDICATORS_TOTAL, // Indicators MSG_CHART_OBJ_WINDOW_N, // Window MSG_CHART_OBJ_INDICATORS_NONE, // No indicators MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ, // Failed to receive the chart window object MSG_CHART_OBJ_SCREENSHOT_CREATED, // Screenshot created MSG_CHART_OBJ_TEMPLATE_SAVED, // Chart template saved MSG_CHART_OBJ_TEMPLATE_APPLIED, // Template applied to chart //--- CChartObjCollection MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION, // Chart collection MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ, // Failed to create a new chart object MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART, // Failed to add a chart object to the collection MSG_CHART_COLLECTION_ERR_CHARTS_MAX, // Cannot open new chart. Number of open charts at maximum }; //+------------------------------------------------------------------+
以及与新添加索引相对应的消息文本:
{"Главное окно графика","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 template saved"}, {"Шаблон применён к графику","Template applied to the chart"}, //--- 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"}, }; //+---------------------------------------------------------------------+
由于我打算实现图表对象的附加功能,包括创建截图和处理模板的能力,我们需要为截图和模板指定存储文件夹,以及默认的文件扩展名(这也意味着保存的格式图像文件)用于屏幕截图。 用于保存屏幕截图的可能文件类型是 *.gif、*.png 和 *.bmp。
将这些新的宏替换添加到 \MQL5\Include\DoEasy\Defines.mqh:
//--- Data parameters for file operations #define DIRECTORY ("DoEasy\\") // Library directory for storing object folders #define RESOURCE_DIR ("DoEasy\\Resource\\") // Library directory for storing resource folders #define SCREENSHOT_DIR ("DoEasy\\ScreenShots\\") // Library directory for storing screenshot folders #define TEMPLATE_DIR ("DoEasy\\") // Library directory for storing template folders #define FILE_EXT_GIF (".gif") // GIF image file name extension #define FILE_EXT_PNG (".png") // PNG image file name extension #define FILE_EXT_BMP (".bmp") // BMP image file name extension #define SCREENSHOT_FILE_EXT (FILE_EXT_PNG) // Chart screenshot file format (extension: .gif, .png and .bmp can be used) //--- Symbol parameters
终端中存放截图和模板的文件夹是不同。
截图保存到文件夹(终端数据文件夹)\MQL5\Files\
模板保存到(终端数据文件夹)\MQL5\Profiles\Templates\
因此,把指定的宏替换添加到文件名,以便存储函数库文件时目标更加明确。
屏幕截图将保存到 \MQL5\Files\DoEasy\ScreenShots\,而模板将保存到 MQL5\Profiles\Templates\DoEasy\。
为了能便捷地保存截图文件,在服务函数 \MQL5\Include\DoEasy\Services\DELib.mqh 文件中实现这些函数。 该函数返回由启动它的程序名称、传递给函数参数的前缀和本地 PC 时间组成的文件名:
//+------------------------------------------------------------------+ //| Return the file name (program name+local time) | //+------------------------------------------------------------------+ string FileNameWithTimeLocal(const string time_prefix=NULL) { 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) CMessage::ToLog(DFUN,GetLastError(),true); if(StringReplace(name,":",".")==WRONG_VALUE) CMessage::ToLog(DFUN,GetLastError(),true); return name; } //+------------------------------------------------------------------+
函数中的字符串格式由《程序名称+函数参数中的传递值+本地 PC 时间日期/小时-分钟/秒》组成。 然后用下划线 (_) 替换所有空格,而用句点 (.) 替换所有冒号,并返回结果字符串。 如果替换未能激活,则在日志中显示相应的消息。
该函数在一秒钟内返回相同的文件名。 即,如果该函数每秒被调用多次,它总会在秒内返回相同的字符串。 因此,我们在此实现了一个函数输入,允许传递额外的文件数据,以便其标识符唯一,且信息量更大。
我们能够为每个图表窗口指定像素为单位的坐标。 这些坐标对应于 ChartXYToTimePrice() 函数的时间/价格坐标。 此外,我们可用 ChartTimePriceToXY() 执行反向转换。 我们将此功能添加到对象当中。 第一个函数是在图表对象中操作,而第二个函数 — 在图表窗口对象中操作。 ChartXYToTimePrice() 函数拥有包含光标子窗口的索引。 从函数返回链接指向的索引。 这可在任何图表窗口里操作。 当我们调用该函数时,在嵌入在函数参数中的变量之中设置包含光标的窗口索引。 与之相对,窗口索引(我们要从其屏幕坐标中获取价格/时间坐标)应手动传递给第二个函数,用于将图表窗口中的时间和价格转换为相应的屏幕坐标(以像素为单位)。 将函数(即使操控它的方法)置于图表窗口对象中以便从中获取相应坐标更为合理。
在 \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh 类的私密部分中,添加存储窗口中光标坐标的变量:
//+------------------------------------------------------------------+ //| Chart window object class | //+------------------------------------------------------------------+ class CChartWnd : public CBaseObj { private: CArrayObj m_list_ind; // Indicator list int m_window_num; // Subwindow index int m_wnd_coord_x; // The X coordinate for the time on the chart in the window int m_wnd_coord_y; // The Y coordinates for the price on the chart in the window //--- Return the flag indicating the presence of an indicator from the list in the window bool IsPresentInWindow(const CWndInd *ind); //--- Remove indicators not present in the window from the list void IndicatorsDelete(void); //--- Add new indicators to the list void IndicatorsAdd(void); //--- Set a subwindow index void SetWindowNum(const int num) { this.m_window_num=num; } public:
在类的公开部分,声明把图表坐标从时间/价格转换为 X 和 Y 坐标表示的方法,并编写两个方法将已获得的坐标赋值给变量,以及返回相对窗口 Y 坐标的方法:
//--- Update data on attached indicators void Refresh(void); //--- Convert the coordinates of a chart from the time/price representation to the X and Y coordinates bool TimePriceToXY(const datetime time,const double price); //--- Return X and Y coordinates of the cursor location in the window int XFromTimePrice(void) const { return this.m_wnd_coord_x; } int YFromTimePrice(void) const { return this.m_wnd_coord_y; } //--- Return the relative Y coordinate of the cursor location in the window int YFromTimePriceRelative(void) const { return this.m_wnd_coord_y-this.YDistance();} }; //+------------------------------------------------------------------+
由于所有窗口 Y 坐标的值都是相对于坐标系原点(图表主窗口的左上角)指定的,所以我们需要用 Y 坐标减去距窗口上边框的距离来得到相对于上窗口边框的坐标。 我们在最后的方法中完成它。
在 类的参数型构造函数的初始化清单中,采用默认值初始化新变量:
//+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+ CChartWnd::CChartWnd(const long chart_id,const int wnd_num) : m_window_num(wnd_num), m_wnd_coord_x(0), m_wnd_coord_y(0) { CBaseObj::SetChartID(chart_id); this.IndicatorsListCreate(); } //+------------------------------------------------------------------+
在类主体之外,实现把图表坐标从时间/价格表示转换为 X 和 Y 轴坐标的方法:
//+------------------------------------------------------------------+ //| Convert chart coordinates from the time/price representation | //| to X and Y coordinates | //+------------------------------------------------------------------+ bool CChartWnd::TimePriceToXY(const datetime time,const 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)) { //CMessage::ToLog(DFUN,::GetLastError(),true); return false; } return true; } //+------------------------------------------------------------------+
在此,我们将所有必要的数值传递给 ChartTimePriceToXY() 函数,并简单地返回其操作结果。 我已经注释掉了日志中的错误消息,因为若是光标位于图表字段框之外,但仍在图表窗口内,我们会在日志中收到太多这样的消息。
该方法将获取的结果填入新加的变量中,而 XFromTimePrice() 和 YFromTimePrice() 方法返回变量的数值。 因此,我们应该首先调用 TimePriceToXY() 方法。 当它返回 true 之后,我们就可接收到所需坐标的数值了。
我们来改进窗口附加指标的数据更新方法。 为了避免不断重建指标列表,我们首先取窗口中的指标数量,并与列表中的数量进行比较。 如果有任何变化,则重建指标列表:
//+------------------------------------------------------------------+ //| Update data on attached indicators | //+------------------------------------------------------------------+ void CChartWnd::Refresh(void) { int change=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num)-this.m_list_ind.Total(); if(change!=0) { this.IndicatorsDelete(); this.IndicatorsAdd(); } } //+------------------------------------------------------------------+
在 \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh 中改进图表对象类。 在上一篇文章中,我编排了 WindowsTotal() 方法,如此我们即可从环境中获取数值,并在一次调用后在对象属性里设置该数值。 然而,在代码逻辑构造的清晰性和对环境的引用数量方面被证明如此做不是很实用,所以我决定放弃这个思路。 现在该方法简单地返回对象属性值:
int WindowsTotal(void) const { return (int)this.GetProperty(CHART_PROP_WINDOWS_TOTAL); }
来自环境的数值会被设置给真正需要它的对象属性。
图表对象将接收在当前文章里预期安排的其余附加功能:导航图表、创建图表屏幕截图、操控图表模板、以及把图表 X 和 Y 坐标转换为时间和价格值。
在 CChartObj 类文件里包含 CSelect 类文件:
//+------------------------------------------------------------------+ //| ChartObj.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\..\Objects\BaseObj.mqh" #include "..\..\Services\Select.mqh" #include "ChartWnd.mqh" //+------------------------------------------------------------------+
它可按属性为图表对象列表进行排序。
在类的私密部分,添加两个新的类成员变量,存储图表上 X 坐标的时间和 Y 坐标的价格:
//+------------------------------------------------------------------+ //| Chart object class | //+------------------------------------------------------------------+ class CChartObj : public CBaseObj { private: CArrayObj m_list_wnd; // List of chart window objects long m_long_prop[CHART_PROP_INTEGER_TOTAL]; // Integer properties double m_double_prop[CHART_PROP_DOUBLE_TOTAL]; // Real properties string m_string_prop[CHART_PROP_STRING_TOTAL]; // String properties int m_digits; // Symbol's Digits() datetime m_wnd_time_x; // Time for X coordinate on the windowed chart double m_wnd_price_y; // Price for Y coordinate on the windowed chart
此处,在类的私密部分中,声明为截图文件附加扩展名(如果缺少)的方法:
//--- Create the list of chart windows void CreateWindowsList(void); //--- Add an extension to the screenshot file if it is missing string FileNameWithExtention(const string filename); public:
在类主体列表的末尾声明新方法 :
//--- Return the flag indicating that the chart object belongs to the program chart bool IsMainChart(void) const { return(this.m_chart_id==CBaseObj::GetMainChartID()); } //--- Return the chart window specified by index CChartWnd *GetWindowByIndex(const int index) const { return this.m_list_wnd.At(index); } //--- Return the window object by its subwindow index CChartWnd *GetWindowByNum(const int win_num) const; //--- Return the window object by the indicator name in it CChartWnd *GetWindowByIndicator(const string ind_name) const; //--- Display data of all indicators of all chart windows in the journal void PrintWndIndicators(void); //--- Display the properties of all chart windows in the journal void PrintWndParameters(void); //--- Shift the chart by the specified number of bars relative to the specified chart position bool Navigate(const int shift,const ENUM_CHART_POSITION position); //--- Shift the chart (1) to the left and (2) to the right by the specified number of bars bool NavigateLeft(const int shift); bool NavigateRight(const int shift); //--- Shift the chart (1) to the beginning and (2) to the end of the history data bool NavigateBegin(void); bool NavigateEnd(void); //--- Create the chart screenshot bool ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align); //--- Create the screenshot of the (1) chart window, (2) 800х600 and (3) 750х562 pixels bool ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER); bool ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER); bool ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER); //--- Save the chart template with the current settings bool SaveTemplate(const string filename=NULL); //--- Apply the specified template to the chart bool ApplyTemplate(const string filename=NULL); //--- Convert X and Y chart window coordinates into time and price int XYToTimePrice(const long x,const double y); //--- Return (1) time and (2) price from XY coordinates datetime TimeFromXY(void) const { return this.m_wnd_time_x; } double PriceFromXY(void) const { return this.m_wnd_price_y; } }; //+------------------------------------------------------------------+
在参数型构造函数的初始化清单中,采用默认值初始化新的类成员变量:
//+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+ CChartObj::CChartObj(const long chart_id) : m_wnd_time_x(0),m_wnd_price_y(0) { }
我们来研究新方法的实现。
按照其内指标名称返回窗口对象的方法:
//+------------------------------------------------------------------+ //| Return the window object by the indicator name in it | //+------------------------------------------------------------------+ CChartWnd *CChartObj::GetWindowByIndicator(const string ind_name) const { int index=(this.m_program==PROGRAM_INDICATOR && ind_name==NULL ? ::ChartWindowFind() : ::ChartWindowFind(this.m_chart_id,ind_name)); return this.GetWindowByIndex(index); } //+------------------------------------------------------------------+
如果启动的程序是基于函数库的一个指标,则调用不带参数的 ChartWindowFind() 函数,从而令指标知道它是在哪个窗口启动的。 如果我们需要查找其它指标的窗口,ChartWindowFind() 应接收一个图表 ID,其为依据指标名称查找到的窗口索引。
因此,在此,我们首先检查程序的类型,如果这是一个指标,且所传递名称为 NULL,则调用不带参数的 ChartWindowFind() 函数 — 这是来自指标的请求,搜索自定义窗口。
否则,调用 ChartWindowFind(),它接收属于图表对象的图表 ID,和一个指标短名,这是要传递给方法,据其查找窗口索引。
结果就是,为了返回特定指标所处的窗口对象,我们调用方法按照指标检测窗口索引,并返回属于图表对象的窗口对象。
该方法相对于指定图表位置,按指定柱线数量平移图表:
//+------------------------------------------------------------------+ //| Move the chart by the specified number of bars | //| relative to the specified chart position | //+------------------------------------------------------------------+ bool CChartObj::Navigate(const int shift,const ENUM_CHART_POSITION position) { ::ResetLastError(); bool res=::ChartNavigate(m_chart_id,position,shift); if(!res) CMessage::ToLog(DFUN,::GetLastError(),true); return res; } //+------------------------------------------------------------------+
该方法简单地调用 ChartNavigate() 函数,并依据传递给该方法的平移参数 — 柱线数量(shift)和执行平移的相对图表位置(position)。 如果函数未能执行成功,该方法会在日志中显示错误消息。 返回调用 ChartNavigate() 函数的结果。 在调用该方法之前,在图表对象里禁用图表右边框的自动滚动,以便该方法能正常工作。
该方法按指定的柱线数量向左平移图表:
//+------------------------------------------------------------------+ //| Shift the chart to the left by the specified number of bars | //+------------------------------------------------------------------+ bool CChartObj::NavigateLeft(const int shift) { this.SetAutoscrollOFF(); return this.Navigate(shift,CHART_CURRENT_POS); } //+------------------------------------------------------------------+
在此,我们先禁用图表对象的图表右边框自动滚动,并返回 Navigate() 方法的操作结果。 该方法接收传递给该方法的图表平移值(以柱线为单位) 。
从当前位置平移图表。
该方法按指定的柱线数量向右平移图表:
//+------------------------------------------------------------------+ //| Shift the chart to the right by the specified number of bars | //+------------------------------------------------------------------+ bool CChartObj::NavigateRight(const int shift) { this.SetAutoscrollOFF(); return this.Navigate(-shift,CHART_CURRENT_POS); } //+------------------------------------------------------------------+
在此,我们先禁用图表对象的图表右边框自动滚动,并返回 Navigate() 方法的操作结果。 该方法接收传递给该方法的图表平移 负数值(以柱线为单位) 。
从当前位置平移图表。
该方法将图表平移至历史数据开头处:
//+------------------------------------------------------------------+ //| Shift the chart to the beginning of the history data | //+------------------------------------------------------------------+ bool CChartObj::NavigateBegin(void) { this.SetAutoscrollOFF(); return this.Navigate(0,CHART_BEGIN); } //+------------------------------------------------------------------+
在此,我们先禁用图表对象的图表右边框自动滚动,并返回 Navigate() 方法的操作结果。 该方法接收图表的零平移值(以柱线为单位)。
该方法将图表平移至历史数据开头。
该方法将图表平移至历史数据末尾(至当前时间):
//+------------------------------------------------------------------+ //| Shift the chart to the end of the history data | //+------------------------------------------------------------------+ bool CChartObj::NavigateEnd(void) { this.SetAutoscrollOFF(); return this.Navigate(0,CHART_END); } //+------------------------------------------------------------------+
在此,我们先禁用图表对象的图表右边框自动滚动,并返回 Navigate() 方法的操作结果。 该方法接收图表的零平移值(以柱线为单位)。
将图表平移至历史的末尾 — 当前时间。
该方法创建图表屏幕截图:
//+------------------------------------------------------------------+ //| Create the chart screenshot | //+------------------------------------------------------------------+ bool CChartObj::ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align) { ::ResetLastError(); if(!::ChartScreenShot(m_chart_id,filename,width,height,align)) { CMessage::ToLog(DFUN,::GetLastError(),true); return false; } return true; } //+------------------------------------------------------------------+
该方法接收截图文件名,获取图像的宽度和高度,以及对齐方式(ENUM_ALIGN_MODE)。 当图像高度超过其宽度时,需要后者来创建垂直的屏幕截图。 在这种情况下,对齐方式为图像指定了与图表对齐的边框。
在此,我们简单地调用 ChartScreenShot() 函数,并采用传递给该方法的参数截取屏幕。
如果截图成功,则返回 true。
如果截图失败,在日志中显示相应的消息,并返回 false。
按指定大小创建屏幕截图的三种方法,可作为附加方法:
- 适配图表窗口的屏幕截图,
- 屏幕截图 800х600,
- 屏幕截图 750х562.
选择第二个和第三个尺寸是有原因的 — MQL5.com 论坛、文章和市场的产品描述中发布的图像通常需要遵照这些尺寸。 图表拟合截图允许直接在终端中直接设置窗口的外观和大小,并按所需大小截屏。
该方法创建与图表窗口分辨率相适的屏幕截图(包括价格和时间尺度,如果它们存在):
//+------------------------------------------------------------------+ //| Create the chart screenshot fitting the chart window resolution | //+------------------------------------------------------------------+ bool CChartObj::ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER) { //--- Create the file name or use the one passed to the method string name= (filename==NULL || filename=="" ? SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT : this.FileNameWithExtention(filename) ); //--- Get the chart window having the largest number of all windows CChartWnd *wnd=this.GetWindowByNum(this.m_list_wnd.Total()-1); if(wnd==NULL) { ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ),string(this.m_list_wnd.Total()-1)); return false; } //--- Calculate the screenshot width and height considering the size of the price and time scales int width=this.WidthInPixels()+(IsShowPriceScale() ? 56 : 0); int height=wnd.YDistance()+wnd.HeightInPixels()+(this.IsShowDateScale() ? 15 : 0); //--- Create a screenshot and return the result of the ScreenShot() method bool res=this.ScreenShot(name,width,height,align); if(res) ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")"); return res; } //+------------------------------------------------------------------+
方法清单提供了逻辑的详述注释。 简而言之,如果在创建名称时将 NULL 传递给方法,则默认创建包含函数库截图文件的路径、程序名称和 Defines.mqh 中指定扩展名组成的文件名。 如果传递给该方法的文件名不为空,则调用 FileNameWithExtention() 方法(稍后讲述)检查文件名中是否存在扩展名(屏幕截图可能具有以下三种扩展名之一:.gif、.png 和.bmp),并在文件名中添加扩展名(如果不存在)。
为了计算屏幕截图尺寸,需要考虑到属于图表的所有窗口,我们需要查找具有最大索引的窗口(0 — 主图表窗口,1, 2, 3, N — 在其中从顶部到底部打开的所有窗口)。 换言之,在最底部的窗口编号最高。 已知从主图表窗口的顶部边框到图表上打开的最底部窗口的顶部边框的距离,我们得到了一个参考点,其为我们所需添加的窗口高度。 由此,我们获得了整个图表的完整高度。 我们只需要检查图表上是否存在时间尺度。 如果该尺度存在,则在计算出的高度上增加 15 个像素(改尺寸是基于试验选择)。 如果没有尺度,则不添加任何内容。 这就是我们如何查找未来屏幕截图的高度。
屏幕截图宽度的情况更容易一些 — 获取图表对象的宽度,并加上 56 个像素,以防图表具有价格尺度。 如果没有尺度,则不添加任何内容。 价格尺度大小也是基于试验设定的。
在不同分辨率的屏幕上,价格和时间尺度的大小可能不同。 我还没有机会尝试不同分辨率的显示器。 无论如何,附加在屏幕截图的尺度大小不会在图像外观中引入相当大的错误。
该方法创建 800x600 像素的图表截图:
//+------------------------------------------------------------------+ //| Create the chart screenshot of 800x600 pixels | //+------------------------------------------------------------------+ bool CChartObj::ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER) { string name= (filename==NULL || filename=="" ? SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT : this.FileNameWithExtention(filename) ); int width=800; int height=600; bool res=this.ScreenShot(name,width,height,align); if(res) ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")"); return res; } //+------------------------------------------------------------------+
这里的一切都比上面的方法简单。 文件名的创建方式与前面的方法相同,文件大小在这里被硬编码。 大小被传递给 ScreenShot() 方法,并返回其结果。
该方法创建 750x562 像素的图表截图:
//+------------------------------------------------------------------+ //| Create the chart screenshot of 750x562 pixels | //+------------------------------------------------------------------+ bool CChartObj::ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER) { string name= (filename==NULL || filename=="" ? SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT : this.FileNameWithExtention(filename) ); int width=750; int height=562; bool res=this.ScreenShot(name,width,height,align); if(res) ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")"); return res; } //+------------------------------------------------------------------+
除了图像大小之外,该方法与创建 800х600 像素的屏幕截图的方法雷同。
任何图表及其所有设置、指标和 EA 都可以保存为模板,从而日后可应用在其他图表。 我们将有两种方法 — 用于保存图表模板,并将指定的模板应用在由图表对象定义的图表。
该方法保存当前图表模板设定:
//+------------------------------------------------------------------+ //| Save the chart template with the current settings | //+------------------------------------------------------------------+ bool CChartObj::SaveTemplate(const string filename=NULL) { ::ResetLastError(); string name= (filename==NULL || filename=="" ? TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) : filename ); if(!::ChartSaveTemplate(this.m_chart_id,name)) { CMessage::ToLog(DFUN,::GetLastError(),true); return false; } ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_SAVED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe())); return true; } //+------------------------------------------------------------------+
该方法传递保存模板时所要用的文件名。 如果名称空缺(默认情况下),则它由路径(在 Defines.mqh 中预定)和程序名称组成。
如果模板文件保存成功,则在日志中创建相应的条目,指定图表(品种和时间帧)的其模板已保存,并返回 true。 如果未能保存模板,相应的消息也会发送到日志,且该方法返回 false。
该方法将指定模板应用于图表:
//+------------------------------------------------------------------+ //| Apply the specified template to the chart | //+------------------------------------------------------------------+ bool CChartObj::ApplyTemplate(const string filename=NULL) { ::ResetLastError(); string name= (filename==NULL || filename=="" ? TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) : filename ); if(!::ChartApplyTemplate(this.m_chart_id,name)) { CMessage::ToLog(DFUN,::GetLastError(),true); return false; } ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_APPLIED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe())); return true; } //+------------------------------------------------------------------+
该方法与保存图表模板类似。 所用的模板文件名需传递给方法,或自动创建。 接下来,将模板应用到图表上,并在日志中显示一条操作结果的通知条目。
请记住,如果我们把另一个 EA 的模板应用到当前正在运行 EA 的图表上,且调用该方法,则当前 EA 将从内存中卸载,且不再动作。 它已被模板中的新 EA 替换。 该方法不检查这种冲突的可能性。 因此,我们需要监控我们的模板,并检查将当前 EA 可由应用于图表的模板中启动的 EA 替换的可能性。
该方法将窗口图表 X 和 Y 坐标转换为时间和价格值:
//+------------------------------------------------------------------------------+ //|Convert X and Y coordinates of the chart window into the time and price values| //+------------------------------------------------------------------------------+ int CChartObj::XYToTimePrice(const long x,const double y) { int sub_window=WRONG_VALUE; ::ResetLastError(); if(!::ChartXYToTimePrice(this.m_chart_id,(int)x,(int)y,sub_window,this.m_wnd_time_x,this.m_wnd_price_y)) { //CMessage::ToLog(DFUN,::GetLastError(),true); return WRONG_VALUE; } return sub_window; } //+------------------------------------------------------------------+
ChartXYToTimePrice() 函数将图表上的 X 和 Y 坐标转换为时间和价格值。 同时,把 sub_window 变量的链接传递给它,用于接收子窗口的索引,其中图表 X 和 Y 坐标所在位置对应的时间和价格会被返回。
有基于此,该方法返回图表子窗口索引:0 — 如果坐标位于主图表窗口中;1,2,3, 等等 — 如果坐标落在相应的图表子窗口中;-1 — 如果该坐标计算失败。 如果调用该方法后获得的值不是 -1,则可以调用 TimeFromXY() 和 PriceFromXY() 方法接收时间和价格,它们简单地返回变量,包含自 ChartXYToTimePrice() 函数获得的时间和价格。
如果缺少截图文件,则为其添加扩展名的方法:
//+------------------------------------------------------------------+ //| Add an extension to the screenshot file if it is missing | //+------------------------------------------------------------------+ string CChartObj::FileNameWithExtention(const string filename) { if(::StringFind(filename,FILE_EXT_GIF)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_PNG)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_BMP)>WRONG_VALUE) return filename; return filename+SCREENSHOT_FILE_EXT; } //+------------------------------------------------------------------+
该方法接收验证后的字符串,我们需要在其中查找截图文件扩展名。 由于屏幕截图文件格式已被严格定义,且只包含三种类型 — GIF、PNG 和 BMP,那么,如果传递给该方法的字符串至少含有一个此类扩展名的子字符串(即扩展名已设置),则该方法返回的字符串不变。 否则,取 Defines.mqh 中默认设置的文件扩展名。 这是一个 .png 文件。 返回修改后的字符串作为结果。
在图表中添加新窗口时检测到的一些有关问题:
在详测期间,曾检测到当我们在窗口中向图表添加新指标时,其窗口会出现(尽管我们还没有点击 ОК 或 Cancel)。 该窗口作为已经存在的窗口在终端中立即可见。 函数库看到它,并将其添加到图表对象窗口列表之中,而指标将不再出现在窗口当中。 但如果我们在添加新窗口指标的窗口中点击 Cancel,该窗口将不再出现在客户端图表窗口的列表当中。 在下一次检查期间,函数库会从列表中删除该窗口。
为避免此类不必要的操作和意外访问客户端中不存在的空图表窗口,我们需要把窗口添加到列表之前确保该窗口含有指标。 如果存在指标,则添加窗口。 如果没有,则窗口打开,但不会添加到图表中。 它会被被跳过。
因此,创建图表窗口列表的方法得以改进:
//+------------------------------------------------------------------+ //| Create the list of chart windows | //+------------------------------------------------------------------+ void CChartObj::CreateWindowsList(void) { //--- Clear the chart window list this.m_list_wnd.Clear(); //--- Get the total number of chart windows from the environment int total=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL); //--- In the loop by the total number of windows for(int i=0;i<total;i++) { //--- Create a new chart window object CChartWnd *wnd=new CChartWnd(this.m_chart_id,i); if(wnd==NULL) continue; //--- If the window index exceeds 0 (not the main chart window) and it still has no indicator, //--- remove the newly created chart window object and go to the next loop iteration if(wnd.WindowNum()!=0 && wnd.IndicatorsTotal()==0) { delete wnd; continue; } //--- If the object was not added to the list, remove that object this.m_list_wnd.Sort(); if(!this.m_list_wnd.Add(wnd)) delete wnd; } //--- If the number of objects in the list corresponds to the number of windows on the chart, //--- write that value to the chart object property //--- If the number of objects in the list does not correspond to the number of windows on the chart, //--- write the number of objects in the list to the chart object property. int value=int(this.m_list_wnd.Total()==total ? total : this.m_list_wnd.Total()); this.SetProperty(CHART_PROP_WINDOWS_TOTAL,value); } //+------------------------------------------------------------------+
方法逻辑在其清单中已予以详述。 在高亮显示的代码模块里,检查将尚未创建的窗口添加到列表中的必要性。 换言之,当仅添加窗口指标时,我们也许有一个尚未创建的窗口。 因此,我们不会分析索引为 0 的窗口。 这是主图表窗口,它肯定存在。 只能向其中添加新指标。 如果检测到没有指标的新窗口(如果指标窗口已经添加到图表中,这是不可能的),那么这是我们尚未单击 OK 将其添加到图表的窗口。 跳过此类窗口,从而避免将其添加到窗口列表之中。
整个循环完成后,将窗口数量写入图表对象属性。 在此,我们确保所有窗口都已成功添加到列表当中。 如果它们的实际数量等于列表中的数字,这意味着所有窗口都已被添加 — 将图表上的窗口数量写入对象属性。 如果数字不等,则列表中存在的数量在属性中设置(它不等于实际数量),以便下一次检查时再次显示不相等,我们能够将创建的窗口添加到列表中。 如果我们在添加新窗口时点击 Cancel,则图表上和列表中的窗口数量相等,且无需处理任何变化。
库类的改进至此完结。
自动更新图表对象和窗口的集合类
现在我们来确保若是打开图表的数量变化、图表上的窗口数量和这些窗口中的指标数量发生任何变化,函数库会随即自动更新所有这些数据,如此我们就不会被这些数据分心,而会始终保有相关数据。
在图表对象类的 \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh 中,补充 Refresh() 方法,如此我们不仅可以检查图表上打开窗口的数量变化(在图表对象当中),而且还能管理已打开窗口中的指标数量(一个窗口适配若干个指标)。
该方法更新图表对象及其窗口列表:
//+------------------------------------------------------------------+ //| Update the chart object and its window list | //+------------------------------------------------------------------+ void CChartObj::Refresh(void) { for(int i=0;i<m_list_wnd.Total();i++) { CChartWnd *wnd=m_list_wnd.At(i); if(wnd==NULL) continue; wnd.Refresh(); } int change=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL)-this.WindowsTotal(); if(change==0) return; this.CreateWindowsList(); } //+------------------------------------------------------------------+
检查图表对象中打开窗口数量的变化之前,我们首先应该在所有对象窗口的列表里顺序移动,并在循环中调用每个后续窗口的更新方法。 图表窗口对象的 Refresh() 方法检查放置在其内的指标数量变化,并在识别出变化时重新创建它们的列表。
在图表对象的集合类文件 \MQL5\Include\DoEasy\Collections\ChartObjCollection.mqh 中,修复阻碍我们更新集合列表中的图表对象,及它们的相应更新窗口和指标的逻辑错误。
之前, 更新图表对象的模块位于检查打开图表数量变化的下方,在打开图表数量没有变化的情况下,不会允许我们触及它:
//+------------------------------------------------------------------+ //| Update the collection list of chart objects | //+------------------------------------------------------------------+ void CChartObjCollection::Refresh(void) { //--- Get the number of open charts in the terminal and int charts_total=this.ChartsTotal(); //--- calculate the difference between the number of open charts in the terminal //--- and chart objects in the collection list. These values are displayed in the chart comment int change=charts_total-this.m_list.Total(); Comment(DFUN,", list total=",DataTotal(),", charts total=",charts_total,", change=",change); //--- If there are no changes, leave if(change==0) return; //--- If a chart is added in the terminal if(change>0) { //--- Find the missing chart object, create and add it to the collection list this.FindAndCreateMissingChartObj(); //--- Get the current chart and return to it since //--- adding a new chart switches the focus to it CChartObj *chart=this.GetChart(GetMainChartID()); if(chart!=NULL) chart.SetBringToTopON(true); } //--- If a chart is removed in the terminal else if(change<0) { //--- Find an extra chart object in the collection list and remove it from the list this.FindAndDeleteExcessChartObj(); } //--- In the loop by the number of chart objects in the list, for(int i=0;i<this.m_list.Total();i++) { //--- get the next chart object and CChartObj *chart=this.m_list.At(i); if(chart==NULL) continue; //--- update it chart.Refresh(); } } //+------------------------------------------------------------------+
解决方案非常简单 — 在检查客户端中打开的图表数量之前,简单地将图表对象更新代码模块移到更高的位置 :
//+------------------------------------------------------------------+ //| Update the collection list of chart objects | //+------------------------------------------------------------------+ void CChartObjCollection::Refresh(void) { //--- In the loop by the number of chart objects in the list, for(int i=0;i<this.m_list.Total();i++) { //--- get the next chart object and CChartObj *chart=this.m_list.At(i); if(chart==NULL) continue; //--- update it chart.Refresh(); } //--- Get the number of open charts in the terminal and int charts_total=this.ChartsTotal(); //--- calculate the difference between the number of open charts in the terminal //--- and chart objects in the collection list. These values are displayed in the chart comment int change=charts_total-this.m_list.Total(); //--- If there are no changes, leave if(change==0) return; //--- If a chart is added in the terminal if(change>0) { //--- Find the missing chart object, create and add it to the collection list this.FindAndCreateMissingChartObj(); //--- Get the current chart and return to it since //--- adding a new chart switches the focus to it CChartObj *chart=this.GetChart(GetMainChartID()); if(chart!=NULL) chart.SetBringToTopON(true); } //--- If a chart is removed in the terminal else if(change<0) { //--- Find an extra chart object in the collection list and remove it from the list this.FindAndDeleteExcessChartObj(); } } //+------------------------------------------------------------------+
现在,检查打开图表数量的变化之前(如果数量没有变化,则退出该方法),我们首先循环遍历集合列表中的所有图表对象,并在它们的 Refresh() 方法中检查它们的窗口对象变化,其中调用了自己的 Refresh() 方法,来检查窗口中的指标数量。 因此,我们首先对窗口中指标数量和图表中指标数量的所有可能变化进行全面检查,然后检查打开图表的数量变化。
将打开新图表和关闭现有图表的方法添加到图表对象集合类当中。
在类的公开部分,声明两个新方法:
//--- Update (1) the chart object collection list and (2) the specified chart object void Refresh(void); void Refresh(const long chart_id); //--- (1) Open a new chart with the specified symbol and period, (2) close the specified chart bool Open(const string symbol,const ENUM_TIMEFRAMES timeframe); bool Close(const long chart_id); }; //+------------------------------------------------------------------+
我们在类主体之外编写它们的实现。
该方法打开含有指定品种和周期的新图表:
//+------------------------------------------------------------------+ //| Open a new chart with the specified symbol and period | //+------------------------------------------------------------------+ bool CChartObjCollection::Open(const string symbol,const ENUM_TIMEFRAMES timeframe) { if(this.m_list.Total()==CHARTS_MAX) { ::Print(CMessage::Text(MSG_CHART_COLLECTION_ERR_CHARTS_MAX)," (",(string)CHARTS_MAX,")"); return false; } ::ResetLastError(); long chart_id=::ChartOpen(symbol,timeframe); if(chart_id==0) CMessage::ToLog(::GetLastError(),true); return(chart_id>0); } //+------------------------------------------------------------------+
在此,如果集合中的图表对象数量超过阈值 1 (CHARTS_MAX),则尝试打开新图表将毫无用处。 通知并返回 false。 接下来,如果我们仍然能够打开新图表,采用已打开图表的指定参数来调用 ChartOpen() 函数。 在出错的情况下,向日志发送条目。 返回打开新图表的函数返回非零值的指示标志。.
该方法关闭指定图表:
//+------------------------------------------------------------------+ //| Close a specified chart | //+------------------------------------------------------------------+ bool CChartObjCollection::Close(const long chart_id) { ::ResetLastError(); bool res=::ChartClose(chart_id); if(!res) CMessage::ToLog(DFUN,::GetLastError(),true); return res; } //+------------------------------------------------------------------+
在此,如果尝试关闭由 ID 指定的图表 失败,则在日志里显示相应条目。
该方法返回 ChartClose() 函数的结果。
在 CEngine 库主对象的 \MQL5\Include\DoEasy\Engine.mqh 中,添加管理图表集合的方法。
按品种和时间帧返回图表对象列表的两种方法
//--- Return the list of chart objects by (1) symbol and (2) timeframe CArrayObj *ChartGetChartsList(const string symbol) { return this.m_charts.GetChartsList(symbol); } CArrayObj *ChartGetChartsList(const ENUM_TIMEFRAMES timeframe) { return this.m_charts.GetChartsList(timeframe); }
重命名来匹配其他类的类似方法,并在清单中移动到更高的位置:
//--- Current the chart collection bool ChartCreateCollection(void) { return this.m_charts.CreateCollection(); } //--- Return (1) the chart collection and (2) the list of charts from the chart collection CChartObjCollection *GetChartObjCollection(void) { return &this.m_charts; } CArrayObj *GetListCharts(void) { return this.m_charts.GetList(); } //--- Return the list of chart objects by (1) symbol and (2) timeframe CArrayObj *GetListCharts(const string symbol) { return this.m_charts.GetChartsList(symbol); } CArrayObj *GetListCharts(const ENUM_TIMEFRAMES timeframe) { return this.m_charts.GetChartsList(timeframe); }
添加返回上次所打开图表的图表对象的方法、返回集合列表中图表对象数量的方法,以及打开和关闭指定图表的两种方法:
//--- Current the chart collection bool ChartCreateCollection(void) { return this.m_charts.CreateCollection(); } //--- Return (1) the chart collection and (2) the list of charts from the chart collection CChartObjCollection *GetChartObjCollection(void) { return &this.m_charts; } CArrayObj *GetListCharts(void) { return this.m_charts.GetList(); } //--- Return the list of chart objects by (1) symbol and (2) timeframe CArrayObj *GetListCharts(const string symbol) { return this.m_charts.GetChartsList(symbol); } CArrayObj *GetListCharts(const ENUM_TIMEFRAMES timeframe) { return this.m_charts.GetChartsList(timeframe); } //--- Return (1) the specified chart object, (2) the chart object with the program and (3) the chart object of the last open chart CChartObj *ChartGetChartObj(const long chart_id) { return this.m_charts.GetChart(chart_id); } CChartObj *ChartGetMainChart(void) { return this.m_charts.GetChart(this.m_charts.GetMainChartID());} CChartObj *ChartGetLastOpenedChart(void) { return this.m_charts.GetChart(this.GetListCharts().Total()-1);} //--- Return the number of charts in the collection list int ChartsTotal(void) { return this.m_charts.DataTotal(); } //--- Update (1) the chart specified by ID and (2) all charts void ChartRefresh(const long chart_id) { this.m_charts.Refresh(chart_id); } void ChartsRefreshAll(void) { this.m_charts.Refresh(); } //--- (1) Open and (2) close the specified chart bool ChartOpen(const string symbol,const ENUM_TIMEFRAMES timeframe) { return this.m_charts.Open(symbol,timeframe); } bool ChartClose(const long chart_id) { return this.m_charts.Close(chart_id); } //--- Return (1) the buffer collection and (2) the buffer list from the collection
ChartGetLastOpenedChart() 方法简单地返回指向位于图表对象集合列表中最后一个对象的指针,而 ChartsTotal() 方法返回图表对象集合列表的大小。 ChartOpen() 和 ChartClose() 方法返回相应图表集合类的 Open() 和 Close() 方法的结果。
这些就是我为当前文章预计的所有更新和改进。
测试
为了执行测试我们借用来自前一篇文章的 EA,并把它保存到 \MQL5\Experts\TestDoEasy\Part70\,命名为 TestDoEasyPart70.mq5。
我会继续做什么?将具有以下图标的新按钮添加到 EA 面板:
- "<" 和 ">" — 将图表分别向左和向右平移一根柱线的按钮;
- "<<" 和 ">>" — 将图表分别向左和向右平移十根柱线的按钮;
- "|<" and ">|" — 分别设置图表平移到历史开始和结束处按钮;
- "N" 和 "X" — 分别用于打开一个新图表/关闭最后一个已打开品种图表的按钮;
- "[O]" — 使用 EA 创建当前图表屏幕截图的按钮。
测试新功能的逻辑如下:
- 点击图表平移按钮时,EA 分别把图表向左和向右平移 1 根柱线和 10 根柱线。
- 点击设置图表平移至历史开始和结束处的按钮时,图表会相应设置。
- 点击图表打开按钮时,EA 会按照它们在图表对象集合列表中出现的顺序,依次打开图表对象集合列表中设置的图表,而不是按照它们市场观察窗口中的顺序(因为在市场观察窗口中排序结果可能不同)。
为了保存和应用图表模板,EA 会保存上次打开的图表模板,并在新图表打开时应用它。 换言之,如果我们在点击打开新图表的按钮之前手动打开一个新图表,例如 GBPUSD,并将其外观配置为区别于当前图表,那么所有后续图表将具有相同的外观。 更精确地说,含有预定义图表外观的模板将在它们打开后自动应用。
这意味着为了执行测试,我们需要首先打开一个新的 GBPUSD 图表,并配置其外观。 然后,基于手动配置的 GBPUSD 图表所保存的模板将应用于 EA 随后打开的所有其他图表。 - 点击图表关闭按钮时,EA 关闭最后一个打开的图表。
- 按顺序依次点击屏幕截图创建按钮,EA 将创建图表屏幕截图: 分辨率按以下排列轮转:800х600 --> 750x562 --> 当前图表大小。
在 EA 的 OnInit() 处理程序中,加入包含跟踪当前图表鼠标事件的权限:
//--- Check playing a standard sound by macro substitution and a custom sound by description engine.PlaySoundByDescription(SND_OK); //--- Wait for 600 milliseconds engine.Pause(600); engine.PlaySoundByDescription(TextByLanguage("Звук упавшей монетки 2","Falling coin 2")); //--- Check the calculation of the cursor coordinates in the chart windows. //--- Allow the current chart to track mouse movement events engine.ChartGetMainChart().SetEventMouseMoveON(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
这令程序能够接收有关鼠标按钮移动和按下事件的消息 (CHARTEVENT_MOUSE_MOVE)。
收到鼠标移动事件后,我们需要在 OnChartEvent() 处理程序中以像素为单位获取图表上的光标坐标,调用创建的函数库方法将它们转换为光标所在窗口的时间、价格和索引,最后, 将新获得的时间和价格转换回图表上的光标坐标,并在图表注释中显示所有这些数值。
编写光标移动事件处理程序:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If working in the tester, exit if(MQLInfoInteger(MQL_TESTER)) return; //--- Handling mouse events if(id==CHARTEVENT_OBJECT_CLICK) { //--- Handle pressing the buttons in the panel if(StringFind(sparam,"BUTT_")>0) PressButtonEvents(sparam); } //--- Handling DoEasy library events if(id>CHARTEVENT_CUSTOM-1) { OnDoEasyEvent(id,lparam,dparam,sparam); } //--- Check ChartXYToTimePrice() if(id==CHARTEVENT_MOUSE_MOVE) { //--- Get the chart object of the current (main) program chart CChartObj *chart=engine.ChartGetMainChart(); if(chart==NULL) return; //--- Get the index of a subwindow the cursor is located at int wnd_num=chart.XYToTimePrice(lparam,dparam); if(wnd_num==WRONG_VALUE) return; //--- Get the calculated cursor location time and price datetime time=chart.TimeFromXY(); double price=chart.PriceFromXY(); //--- Get the window object of the chart the cursor is located in by the subwindow index CChartWnd *wnd=chart.GetWindowByNum(wnd_num); if(wnd==NULL) return; //--- If X and Y coordinates are calculated by time and price (make a reverse conversion), if(wnd.TimePriceToXY(time,price)) { //--- in the comment, show the time, price and index of the window that are calculated by X and Y cursor coordinates, //--- as well as the cursor X and Y coordinates converted back from the time and price Comment ( DFUN,"time: ",TimeToString(time),", price: ",DoubleToString(price,Digits()), ", win num: ",(string)wnd_num,": x: ",(string)wnd.XFromTimePrice(), ", y: ",(string)wnd.YFromTimePrice()," (",(string)wnd.YFromTimePriceRelative(),")") ; } } } //+------------------------------------------------------------------+
EA 的 OnChartEvent() 代码中详述了处理鼠标移动事件的逻辑。
在 EA 的 CreateButtons() 函数中,添加创建新面板按钮的代码:
//+------------------------------------------------------------------+ //| Create the buttons panel | //+------------------------------------------------------------------+ bool CreateButtons(const int shift_x=20,const int shift_y=0) { int h=18,w=82,offset=2,wpt=14; int cx=offset+shift_x+wpt*2+2,cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1; int x=cx,y=cy; int shift=0; for(int i=0;i<TOTAL_BUTT;i++) { x=x+(i==7 ? w+2 : 0); if(i==TOTAL_BUTT-6) x=cx; y=(cy-(i-(i>6 ? 7 : 0))*(h+1)); if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-6 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue))) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text); return false; } } h=18; offset=2; cx=offset+shift_x; cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1; x=cx; y=cy; shift=0; for(int i=0;i<18;i++) { y=(cy-(i-(i>6 ? 7 : 0))*(h+1)); if(!ButtonCreate(butt_data[i].name+"_PRICE",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5 : x),y,wpt,h,"P",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue))) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"P\""); return false; } if(!ButtonCreate(butt_data[i].name+"_TIME",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5+wpt+1 : x+wpt+1),y,wpt,h,"T",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue))) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"T\""); return false; } } //--- Left and Right buttons int xbn=x+wpt*2+w*2+5; int ybn=y+h*3+3; if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT1",xbn,ybn,wpt,h,"<",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT1"); return false; } if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT1",xbn+wpt+1,ybn,wpt,h,">",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT1"); return false; } //--- Left 10 and Right 10 buttons ybn=y+h*2+2; if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT10",xbn,ybn,wpt,h,"<<",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT10"); return false; } if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT10",xbn+wpt+1,ybn,wpt,h,">>",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT10"); return false; } //--- Home and End buttons ybn=y+h+1; if(!ButtonCreate(prefix+"BUTT_NAVIGATE_HOME",xbn,ybn,wpt,h,"|<",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_HOME"); return false; } if(!ButtonCreate(prefix+"BUTT_NAVIGATE_END",xbn+wpt+1,ybn,wpt,h,">|",clrGreen)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_END"); return false; } //--- Open and Close buttons ybn=y; if(!ButtonCreate(prefix+"BUTT_CHART_OPEN",xbn,ybn,wpt,h,"N",clrBlue)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_OPEN"); return false; } if(!ButtonCreate(prefix+"BUTT_CHART_CLOSE",xbn+wpt+1,ybn,wpt,h,"X",clrRed)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_CLOSE"); return false; } //--- ScreenShot button ybn=y-h-1; if(!ButtonCreate(prefix+"BUTT_CHART_SCREENSHOT",xbn,ybn,wpt*2+offset,h,"[O]",clrBlue)) { Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_SCREENSHOT"); return false; } ChartRedraw(0); return true; } //+------------------------------------------------------------------+
此处的一切都很简单:为每个新按钮计算坐标,并调用 ButtonCreate() 函数创建按钮,该函数接收欲创建图形对象的名称、坐标、宽度、高度、标题和颜色。 如果创建按钮失败,则激活警报,并返回 false。 在 OnInit() 处理程序中,如果面板创建函数返回 false,则退出,并返回 INIT_FAILED 代码。
在处理按钮点击的 PressButtonEvents() 函数中,添加新按钮点击的处理:
//+------------------------------------------------------------------+ //| Handle pressing the buttons | //+------------------------------------------------------------------+ void PressButtonEvents(const string button_name) { bool comp_magic=true; // Temporary variable selecting the composite magic number with random group IDs string comment=""; //--- Convert button name into its string ID string button=StringSubstr(button_name,StringLen(prefix)); //--- Random group 1 and 2 numbers within the range of 0 - 15 group1=(uchar)Rand(); group2=(uchar)Rand(); uint magic=(comp_magic ? engine.SetCompositeMagicNumber(magic_number,group1,group2) : magic_number); //--- If the button is pressed if(ButtonState(button_name)) { //--- If the button of shifting a chart 1 bar to the left is clicked if(button=="BUTT_NAVIGATE_LEFT1") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateLeft(1); } //--- If the button of shifting a chart 1 bar to the right is clicked if(button=="BUTT_NAVIGATE_RIGHT1") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateRight(1); } //--- If the button of shifting a chart 10 bars to the left is clicked if(button=="BUTT_NAVIGATE_LEFT10") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateLeft(10); } //--- If the button of shifting a chart 10 bars to the right is clicked if(button=="BUTT_NAVIGATE_RIGHT10") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateRight(10); } //--- If the button of shifting a chart to the start of history is clicked if(button=="BUTT_NAVIGATE_HOME") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateBegin(); } //--- If the button of shifting a chart to the end of history is clicked if(button=="BUTT_NAVIGATE_END") { CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.NavigateEnd(); } //--- If the new chart open button is pressed if(button=="BUTT_CHART_OPEN") { int total_charts=engine.ChartsTotal(); static int first_index=total_charts; string name=SymbolName(total_charts-first_index,true); if(engine.ChartOpen(name,PERIOD_CURRENT)) { engine.ChartsRefreshAll(); CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) chart.SetBringToTopON(true); } //--- This code block is needed only for the test and only if there is an open GBPUSD chart //--- GBPUSD chart settings should differ from that of charts opened by default CArrayObj *list_gbpusd=engine.GetListCharts("GBPUSD"); if(list_gbpusd!=NULL && list_gbpusd.Total()>0) { CChartObj *chart=list_gbpusd.At(0); if(chart.SaveTemplate()) { chart=engine.ChartGetLastOpenedChart(); if(chart!=NULL) chart.ApplyTemplate(); } } //--- End of the test code block } //--- If the the last chart close button is pressed if(button=="BUTT_CHART_CLOSE") { CArrayObj *list_charts=engine.GetListCharts(); if(list_charts!=NULL) { list_charts.Sort(SORT_BY_CHART_ID); CChartObj *chart=list_charts.At(list_charts.Total()-1); if(chart!=NULL && !chart.IsMainChart()) engine.ChartClose(chart.ID()); } } //--- If the ScreenShot button is pressed if(button=="BUTT_CHART_SCREENSHOT") { static int num=0; if(++num>3) num=1; CChartObj *chart=engine.ChartGetMainChart(); if(chart!=NULL) { switch(num) { case 1 : chart.ScreenShot800x600(); break; case 2 : chart.ScreenShot750x562(); break; default: chart.ScreenShotWndSize(); break; } } } //--- If the 'BUTT_BUY: Open Buy position' is pressed if(button==EnumToString(BUTT_BUY)) { ... ... ... ... ... ... ...
务须显示整个函数代码,只需显示添加的修改。
在此,我们简单地处理点击每个新按钮。 逻辑很简单,没必要啰嗦。 我将其留待独立分析,特别是考虑到这个处理程序只是作为一个阐述如何在程序中利用函数库方法的例子。 不过,如果您对代码有任何疑问,欢迎在评论区留言。
这些就是我在新的测试 EA 中需要做的所有改进。
编译 EA,在图表上启动它,采用的设置为“仅使用当前交易品种”和“仅使用当前时间帧”:
在启动 EA 之前,确保打开一个新的 GBPUSD 品种图表,并配置其外观,要与 default.tpl 模板默认的有所不同,例如,以下方式(GBPUSD 图表已提前打开):
现在我们可以通过点击面板按钮来测试新的函数库功能:
每次打开新图表时,EA 都会保存先前配置的 GBPUSD 品种图表的模板,之后立即将其应用到每个新打开的图表上,并创建相应的日志条目:
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: USDCHF H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: GBPUSD H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: EURUSD H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: USDRUB H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: EURJPY H1 CChartObj::SaveTemplate: Chart template saved: GBPUSD H4 CChartObj::ApplyTemplate: Template applied to the chart: EURGBP H1 CChartObjCollection::Close: Wrong chart ID (4101)
关闭图表会引发一个错误。 该函数库每半秒更新一次打开图表的状态。
这是在 Defines.mqh 文件中设置的:
//--- Parameters of the chart collection timer #define COLLECTION_CHARTS_PAUSE (500) // Chart collection timer pause in milliseconds #define COLLECTION_CHARTS_COUNTER_STEP (16) // Chart timer counter increment #define COLLECTION_CHARTS_COUNTER_ID (9) // Chart timer counter ID
我以每秒两次以上的速度点击关闭最后一个打开图表的按钮,由此尝试再次关闭已经关闭的最后一个图表(在图表集合列表中仍然有相应的项目)。 可以通过修改指定的宏替换来调整打开图表的当前状态、它们的窗口和指标的更新频率。 减小常量值能更快速地更新环境。 在这种情况下,由于更新频率提高,CPU 的负载亦会增加。 在此找到“黄金均衡”很重要,因为此功能仍可手工干涉控制图表,因此更新频率是可定制的。 尝试访问缺失图表时偶尔出现的错误并不重要。 在下一次环境更新发生时简单地按下第二次按钮,函数库中的图表对象列表就会与客户端中的图表对象状态同步。
现在是时候来测试创建当前图表的屏幕截图了。 每次点击按钮都会创建一个特定大小的图表屏幕截图。 首次点击 — 800x600,第二次 — 750x562,第三次 — 当前图表尺寸:
创建三个不同分辨率的屏幕截图后(附有相应的日志条目),
CChartObj::ScreenShot800x600: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.25.png (800 x 600) CChartObj::ScreenShot750x562: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.28.png (750 x 562) CChartObj::ScreenShotWndSize: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.29.png (726 x 321)
我们还查看了保存这些屏幕截图的文件夹内容。
在创建屏幕截图之前,我将光标悬停在当前图表两个窗口的不同区域上,图表注释会显示时间、价格和子窗口的索引,以及以像素为单位的光标 X/Y 坐标。 光标 Y 坐标有两个值。 第一个值显示品种主窗口初始坐标的 Y 坐标。 第二个所示数值(括号中)显示相对于光标所在窗口上边框的 Y 坐标。
正如我们所见,为当前文章预计的所有功能都可以正常工作。
下一步是什么?
在下一篇文章中,我将实现图表对象属性变化及窗口事件的自动跟踪。
以下是该函数库当前版本的所有文件,以及 MQL5 的测试 EA 文件,供您测试和下载。
请您在评论中留下问题和建议。
*该系列的前几篇文章:
DoEasy 函数库中的其他类(第六十七部分):图表对象类
DoEasy 函数库中的其他类(第六十八部分):图表窗口对象类和图表窗口中的指标对象类
DoEasy 函数库中的其他类(第六十九部分):图表对象集合类