我们任务的主要目标在于:开发一款用于 MetaTrader 5 客户端的图表任意文本信息输出的用户友好且可扩展的工具。比如说,“EA 交易”的当前设置,或是指定时间间隔内其运行结果(作为一个表呈现),交易者使用的价格值或指标值表,或是“EA 交易”的交易日志。在 EA 或使用此库的指标运行期间,所有此类信息都可以动态显示于图表上方。
作为开发此库的基础,有一系列的 MetaTrader 5 客户端标准库类被采用。我们首先要研究这些类,并对其实施相应的扩展,以完成我们的任务。
我们感兴趣的这组类,位于 Include 和 Include\ChartObjects 文件夹中。
文件如下:
我们更仔细地讲讲这些类。
类描述很短,所以贴出来看看:
class CObject { protected: CObject *m_prev; CObject *m_next; public: CObject(); CObject *Prev() { return(m_prev); } void Prev(CObject *node) { m_prev=node; } CObject *Next() { return(m_next); } void Next(CObject *node) { m_next=node; } virtual bool Save(int file_handle) { return(true); } virtual bool Load(int file_handle) { return(true); } virtual int Type() const { return(0); } protected: virtual int Compare(const CObject *node,int mode=0) const { return(0); } }; //+------------------------------------------------------------------+ void CObject::CObject() { m_prev=NULL; m_next=NULL; }
您也看到了,此类中只包含一般用途的数据和方法,它们并不与图表的输出直接相关。
但是,它仍具备一个非常重要的属性 - 可用于创建单链和双链表。这些功能由 CObject::m_prev 与 CObject* 类型的 CObject::m_next 数据字段及其读/写方法提供。CObject::m_prev 字段引用的是前一个列表元素,而 CObject::m_next 则是引用下一个。有关列表构造的更多详情,我们稍候再谈。
此外,还有一种对比 CObject* 类型两种对象的方法 - CObject::Compare 方法,可在对列表元素排序时使用。想要实现文件中数据字段的保存/计数,还有两种更有用的方法 - 那就是 CObject::Save 和 CObject::Load 方法。想要获取想要的功能,就要在后代类中重载这些方法。
CObject::Type 是对象类型识别方法。此方法在处理包含不同类型对象的列表时很有用。
CObject 类(及其实例)拥有下述功能:
如上所述的大多数方法都是虚拟的,并未于基类中实施。基类并没有任何带有实体意义的实际属性。作为 OOP 中的惯例,功能要在后代类中实施。
CChartObject 是 CObject 类的一个后代。
通过其名称,您就可以看出这是一个描述某些抽象图形对象的类。不过,这个抽象对象已经包含了一些实体属性和使用这些属性的方法。此类属性对 MetaTrader 5 中的所有图形对象通用,所以将其置入此类也符合逻辑。
我们更仔细地讲讲它们。我们会利用下述数据,将图形对象附至图表窗口:
protected: long m_chart_id; // 包含指定图形对象的 // 图表编号 (0 - 当前图表) int m_window; // 图表子窗口编号 (0 - 主窗口) string m_name; // 图表上对象的唯一名称 int m_num_points; // 绑定对象的点数
在我们指定或读取真实图形对象的属性之前,必须将其与对象相连(类实例)。这一动作通过 CChartObject::Attach 方法来完成。在后代类中,它会在图表上创建图形对象后立即被调用。
bool CChartObject::Attach(long chart_id,string name,int window,int points) { if(ObjectFind(chart_id,name)<0) { return(false); } if(chart_id==0) chart_id=ChartID(); m_chart_id =chart_id; m_window =window; m_name =name; m_num_points=points; return(true); }
首先,我们验证真实图形对象是否存在。如果存在,则将其属性存储至 CChartObject 类对象的内部字段。之后,我们可以读取或修改图形对象的属性(颜色、位置等)。
图形对象属性的保存/读取方法,已于 CChartObject::Save 和 CChartObject::Load 方法中实施。在后代类中,要先调用保存/读取的父方法,再调用自己的方法。
与基类相比,CChartObject 类(及其实例)拥有下述新属性:
现在,我们把注意力转向 ChartObjectsTxtControls.mqh 文件。我们会在这里找到专为各种图形对象输出而开发的类的描述,其中包含图表上方的文本。我们来研究研究它们的基本功能。
它们的基类是 CChartObjectText 类。它封装了与图表上文本输出关联的属性和方法。
该类的描述如下:
class CChartObjectText : public CChartObject { public: double Angle() const; bool Angle(double angle); string Font() const; bool Font(string font); int FontSize() const; bool FontSize(int size); ENUM_ANCHOR_POINT Anchor() const; bool Anchor(ENUM_ANCHOR_POINT anchor); bool Create(long chart_id,string name,int window,datetime time,double price); virtual int Type() const { return(OBJ_TEXT); } virtual bool Save(int file_handle); virtual bool Load(int file_handle); };
与 CChartObject 相比,它包含文本图形对象属性的读取与修改方法 - 图表上文本方向的角度、文本的字体名称、字号以及图形对象的坐标。有一种新方法出现 CChartObjectText::Create 允许我们在图表上创建一个 OBJ_TEXT 类型的真实图表对象。
其实施:
bool CChartObjectText::Create(long chart_id,string name,int window,datetime time,double price) { bool result = ObjectCreate( chart_id, name, OBJ_TEXT, window, time, price ); if(result) { result &= Attach(chart_id, name, window, 1 ); } return(result); }
如果图形对象已成功创建(ObjectCreate 方法返回 true),则 CChartObject::Attach 方法即被调用(我们之前讲过的方法)。
由此,对比父类,CChartObjectText 的新功能包括:
标准类层级中的下一个类,就是 CChartObjectLabel 类。它允许您在图表上创建 OBJ_LABEL 类型(文本标签)的图形对象。
此为该类描述:
class CChartObjectLabel : public CChartObjectText { public: int X_Distance() const; bool X_Distance(int X); int Y_Distance() const; bool Y_Distance(int Y); int X_Size() const; int Y_Size() const; ENUM_BASE_CORNER Corner() const; bool Corner(ENUM_BASE_CORNER corner); bool Time(datetime time) { return(false); } bool Price(double price) { return(false); } bool Create(long chart_id,string name,int window,int X,int Y); virtual int Type() const { return(OBJ_LABEL); } virtual bool Save(int file_handle); virtual bool Load(int file_handle); };
这里,我们必须注意 OBJ_TEXT 类型图形对象与 OBJ_LABEL 类型对象两者间的区别。
前一种绑定价格图表(价格时间坐标)或子窗口的图表。后一种则绑定窗口的图表坐标,或是图表的子窗口(像素)。
因此,OBJ_TEXT 类型的对象会在滚动时随图表一同移动,而 OBJ_LABEL 类型的对象则会在滚动时保持不动。因此,我们必须根据任务,选择特定的图形文本对象类型。
对比父类,CChartObjectLabel 类包含的特色功能如下:
层级中的下一个类是 CChartObjectEdit 类。这是一个用于创建 OBJ_EDIT 类型(输入字段)图形对象的类。
该类型的对象也和 OBJ_LABEL 类型的对象一样,利用图表坐标(像素)绑定,所以,像标准类一样由 CChartObjectLabel 类衍生出它来也合乎逻辑:
class CChartObjectEdit : public CChartObjectLabel { public: bool X_Size(int X); bool Y_Size(int Y); color BackColor() const; bool BackColor(color new_color); bool ReadOnly() const; bool ReadOnly(bool flag); bool Angle(double angle) { return(false); } bool Create(long chart_id,string name,int window,int X,int Y,int sizeX,int sizeY); virtual int Type() const { return(OBJ_EDIT); } virtual bool Save(int file_handle); virtual bool Load(int file_handle); };
OBJ_EDIT 类型输入字段与文本标签两者的区别如下:
CChartObjectButton 是文本图形对象层级中的又一个类。该对象被称为按钮,开发目的就是在图表上创建一个按下式按钮形式的控件元素。该类是 CChartObjectEdit 类的一个后代,且继承其功能性:
class CChartObjectButton : public CChartObjectEdit { public: bool State() const; bool State(bool state); virtual int Type() const { return(OBJ_BUTTON); } virtual bool Save(int file_handle); virtual bool Load(int file_handle); };
源自输入字段的 OBJ_BUTTON 类型的按钮差别如下:
标准库类的结构(层级)可总结如下:
图 1. 标准类的总体结构
CObject 类是其它标准类的基类,比如操作列表以及其它的 CList 类。
我们简要地探讨过旨在于图表上生成文本图形对象的标准类的层级。现在,我们利用新的类来扩展此层级。首先,我们必须决定实施所需的功能性。介绍一下要求。因为我们要处理文本信息的输出,所以按下述结构化形式提供此信息是合乎逻辑的:
标题 | 信息文本 |
文本信息的这种呈现结构,适用于大多数的简单情况。
比如说,交易品种参数的指标可能如下所示:
图 2. 文本信息结构化显示示例
它采用了上面提到结构的六个信息字段。可能会缺少结构中的某些元素。比如说图 2 中顶部字段的平铺就没有显示。其它字段包含标题与文本。该指标可在本文随附的 PricelInfo.mq5 文件中找到。
我们需要研究的第二点,就是在图表上定位图形文本对象的方式。采用的按屏幕像素指定坐标的方法,允许对图表中任意位置对象的定位。
如果您要在图表的不同位置上放置多个文本对象,在实际应用中会很不方便,因为所有的屏幕坐标都需要计算。此外,如果您更改了图表尺寸,则所有的像素坐标都需要重新计算,以确保对象在屏幕上的相对位置不被改变。
如果我们将图表窗口描画成同样大小的矩形(字段),并为每个矩形分配一个水平与垂直坐标,我们就会得到一个独立于屏幕分辨率之外的通用定位系统。
创建图形文本对象时,用户可以设置垂直与水平方向字段的最大数量,以及此类字段中作为参数的对象坐标。而内嵌于相应类中的功能,则会在您更改屏幕分辨率时自动调整图形对象的坐标。如此一来,坐标仅需指定一次,无需任何进一步的调整。
下一个必须处理的问题,就是图形文本对象名称的自动生成。对于生成方法的主要要求,就是在给定的窗口内获取一个独特的名称。如此则允许定位屏幕上的大量对象,且无需费心绞尽脑汁地自创不重复的名称。
我们建议采用下述方法生成名称(由字段构成的字符串):
日期时间 | 毫秒数 |
要获取字符串中包含日期与时间的部分,请使用下述调用:
TimeToString(TimeGMT(), TIME_DATE|TIME_MINUTES|TIME_SECONDS);
要获取毫秒数,请使用下述调用:
DoubleToString(GetTickCount(), 0);
但是,即便我们测量时间到了毫秒,在连续调用 GetTickCount () 函数两次或更多次时,也极有可能会取得相同的值。这是由于操作系统与处理器内计时器不连续的限制。因此,有必要采取额外措施来检测此类情况。
建议方法利用下述函数实施:
string GetUniqName() { static uint prev_count = 0; uint count = GetTickCount(); while(1) { if(prev_count == UINT_MAX) { prev_count = 0; } if(count <= prev_count) { prev_count++; count = prev_count; } else { prev_count = count; } // 确认没有同样名称的对象: string name = TimeToString(TimeGMT(), TIME_DATE|TIME_MINUTES|TIME_SECONDS)+" "+DoubleToString(count, 0); if(ObjectFind(0, name) < 0) { return(name); } } return(NULL); }
此方法的局限:每秒钟生成的独特名称超不过 4294967295 (UINT_MAX) 个。很明显,这在实际应用中足够了。为防万一,还有一个是否存在同名图形对象的附加验证。
在终端屏幕上显示标题的类,如下所示:
class TTitleDisplay : public CChartObjectLabel { protected: long chart_id; int sub_window; long chart_width; // 图表的像素点数宽度 long chart_height; // 图表的像素点数高度 long chart_width_step; // 水平方向上网格坐标距离 long chart_height_step; // 垂直方向上网格坐标距离 int columns_number; // 列数 int lines_number; // 行数 int curr_column; int curr_row; protected: void SetParams(long chart_id, int window, int cols, int lines);// 指定对象参数 protected: string GetUniqName(); // 设置一个唯一名称 bool Create(long chart_id, int window, int cols, int lines, int col, int row); void RecalcAndRedraw();// 重新计算坐标并重绘 public: void TTitleDisplay(); // 构造函数 void ~TTitleDisplay(); // 析构函数 };
创建图形文本对象的基本方法:
bool Create(long chart_id, int window, int _cols, int _lines, int _col, int _row);
输入参数的赋值如下:
TTitleDisplay::SetParams 方法会计算对象与屏幕上位置关联的参数。
实施如下:
void TTitleDisplay::SetParams(long _chart_id, int _window, int _cols, int _lines) { this.chart_id = _chart_id; this.sub_window = _window; this.columns_number = _cols; this.lines_number = _lines; // 指定窗口的像素尺寸: this.chart_width = GetSystemMetrics(SM_CXFULLSCREEN); this.chart_height = GetSystemMetrics(SM_CYFULLSCREEN); // 计算坐标网格的距离 this.chart_width_step = this.chart_width/_cols; this.chart_height_step = this.chart_height/_lines; }
这里,我们采用 GetSystemMetrics WinAPI 函数调用来获取当前显示设置。此函数由 user32.dll Windows 系统库导入。
创建信息文本的 TFieldDisplay 类也用类似方法构建。详情请见本文随附的 TextDisplay.mqh 库。
现在来研究另一个标准类,我们会在实现计划时用到它。此类允许您将对象组至列表。它位于 Include\Arrays\List.mqh 文件中。此文件会提供某标准 CList 类的描述与实施 - 该类为 CObject 基类的后代,前文我们讲过。它包含一系列处理列表中对象的方法(添加到列表、由列表中移除、访问列表任意元素以及清空列表)。
我们来看基本方法:
int Add(CObject *new_node); int Insert(CObject *new_node,int index);
向列表中添加新项目有两种方法。第一种 CList::Add 方法允许您将新元素 new_node 添加到列表末尾。第二种 CList::Insert 方法则能让您将新元素 new_node 插入到列表中任意位置(由 索引指定)。
bool Delete(int index);
CList::Delete 方法允许您将带有指定索引的某个元素由列表中移除。同时,除了由列表中移除该项以外,由 CObject 类型元素占用的内存也会被释放。换句话说,对象会被“物理”删除。
int IndexOf(CObject* node); CObject* GetNodeAtIndex(int index); CObject* GetFirstNode(); CObject* GetPrevNode(); CObject* GetNextNode(); CObject* GetLastNode();
CList::IndexOf 方法会返回列表中某特定元素的索引。索引的编号从零开始:第一个元素的索引为零。此方法会执行反运算,并按元素指针返回索引。如果元素未在列表中,则返回 -1。
巡览 CList::GetFirstNode 列表的另四种方法会返回前一元素,而 CList::GetNextNode 则会返回下一元素,CList::GetLastNode 会返回列表中最后一个元素。
void Clear();
CList::Clear 方法允许您移除列表中所有元素,同时释放被对象占用的内存。
这些都是 CList 类的基本方法,其余方法的描述请见《MQL5 参考》。
那么,我们已经得到了实现计划所需的一切。这里是一个简单的类,允许您在一个任意大小的表内组织文本图形对象:
class TableDisplay : public CList { protected: long chart_id; int sub_window; public: void SetParams(long _chart_id, int _window, ENUM_BASE_CORNER _corner = CORNER_LEFT_UPPER); int AddTitleObject(int _cols, int _lines, int _col, int _row, string _title, color _color, string _fontname = "Arial", int _fontsize = 8); int AddFieldObject(int _cols, int _lines, int _col, int _row, color _color, string _fontname = "Arial", int _fontsize = 8); bool SetColor(int _index, color _color); bool SetFont(int _index, string _fontname, int _fontsize); bool SetText(int _index, string _text); public: void TableDisplay(); void ~TableDisplay(); };
向此表添加图形对象之前,您必须先设置参数:图表标识符、子窗口索引以及固定点。可以通过调用 TableDisplay::SetParams 方法来实现。此后,您可以向此表添加任何数量的标题和文本字段。
我们开发的完整库文本,均见本文随附的 TextDisplay.mqh 文件。
考虑以指标的形式创建多交易品种值显示表的示例。
大体如下所示:
图 3. 屏幕表格示例
#include <TextDisplay.mqh>
#define NUMBER 8 //--------------------------------------------------------------------- string names[NUMBER] = {"EURUSD", "GBPUSD", "AUDUSD", "NZDUSD", "USDCHF", "USDCAD", "USDJPY", "EURJPY"}; int coord_y[NUMBER] = {2, 3, 4, 5, 6, 7, 8, 9};坐标从零开始排序。
TableDisplay Table1;
int OnInit() { // 创建一个表格 Table1.SetParams(0, 0); for(int i=0; i<NUMBER; i++) { Table1.AddFieldObject(40, 40, 3, coord_y[i], Yellow); } for(int i=0; i<NUMBER; i++) { Table1.AddTitleObject(40, 40, 1, coord_y[i], names[i]+":", White); } ChartRedraw(0); EventSetTimer(1); return(0); }
最好是在 OnInit 事件处理程序中完成。首先,利用 TableDisplay::AddFieldObject 方法添加信息字段。然后,再利用 TableDisplay::AddTitleObject 方法向其添加标题。
出于两种原因,上述添加要在单独的周期内实施:首先,标题的数量一般都不会与信息字段的数量相符;第二,如果更新的信息字段于一行中索引,则更易于组织访问(本例是从零到值 NUMBER - 1)。
本例中,通过计时器事件处理程序组织动态信息的更新。该事件的 OnTimer 处理程序应接收给定工具的价格值,并于图表上显示。
文本如下所示:
//--------------------------------------------------------------------- double rates[NUMBER]; datetime times[NUMBER]; MqlTick tick; //--------------------------------------------------------------------- // OnTimer 事件处理函数 //--------------------------------------------------------------------- void OnTimer() { for(int i=0; i<NUMBER; i++) { // 获得价格数值: ResetLastError(); if(SymbolInfoTick(names[i], tick) != true) { Table1.SetText(i,"Err "+DoubleToString(GetLastError(),0)); Table1.SetColor(i,Yellow); continue; } if(tick.time>times[i]) { Table1.SetText(i, DoubleToString(tick.bid, (int)(SymbolInfoInteger(names[i], SYMBOL_DIGITS)))); if(tick.bid>rates[i]) { Table1.SetColor(i, Lime); } else if(tick.bid<rates[i]) { Table1.SetColor(i, Red); } else { Table1.SetColor(i, Yellow); } rates[i] = tick.bid; times[i] = tick.time; } } ChartRedraw(0); }
周期开始时,读取指定工具的价格跳动数据,然后,再验证数据的相关性 - 如果价格跳动时间与之前不同,我们则认定数据不相关。此后,我们再分析之前一跳动相关的报价值。
如果当前价格高于前一价格,则我们将该信息字段指定为绿色。如果当前价格小于前一价格,则指定为红色;如果相等,则指定为黄色。周期结束时,存储价格的当前值和价格跳动时间,以供 OnTimer 事件处理程序被调用时进行分析。
一般来讲,更新动态信息的代码要取决于任务。基本上,用户只需要实施这部分代码。假设我们决定向图 2 中价格的右侧添加一个点差,我们来看看如何实现。
需要做的,只是将附加数据字段添加到此表:
for(int i=0; i<NUMBER; i++) { Table1.AddFieldObject(40, 40, 5, coord_y[i], Yellow); }
再把会更新点差值的代码,添加到 OnTimer 事件处理程序中:
Table1.SetText(i+NUMBER, DoubleToString((tick.ask-tick.bid)/SymbolInfoDouble(names[i], SYMBOL_POINT), 0));
如此一来,我们就能看到下图:
图 4. 带点差的价格
注意:点差索引字段从 NUMBER 值开始(如果等于零)。
void OnDeinit(const int _reason) { EventKillTimer(); // 删除显示的元素: Table1.Clear(); }
这里是要把计时器和表清空。同时,这些对象占用的内存也都会被释放。
该指标的完整文本,请见本文随附的 PriceList.mq5 文件。附件中包含该指标的一个“改进”版本 - 点差显示。它拥有大量用于图表内标题颜色指定及表格定位的外部参数。
随附的 MarketWatch.mq5 (及内含的 MarketWatch.mqh)中,包含一个一览表形式的、显示交易工具基本参数的指标。每个交易品种的信息,皆如图 2 所示。
此外,它还会显示指定时间间隔内的价格变动百分比。一组的交易品种(不超过 16 种)和时间间隔,被指定为带元素的字符串,且由分号隔开。此指标的运行结果如图 5 所示:
图 5. 市场回顾指标
我们讲到了在 MetaTrader 5 客户端图表上显示文本信息的一种方式。
利用客户端提供的标准库类,我们就能相当轻松快速地开发出新的、以二维表形式呈现文本信息的功能。MQL5 语言的面向对象方法非常强大。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...