
自定义图形控件。第一部分:创建简单控件
简介
MQL5 语言向开发人员提供各种各样以程序方式控制的图形对象:一个按钮、一个文本标签、一个编辑字段、一个位图标签(图 1)、不同的图形分析工具(图 2)。
图 1. 图形对象:一个按钮、一个文本标签、一个编辑字段、一个位图标签
图 2. 某些图形分析对象:椭圆、斐波纳契扇形线、斐波纳契扩展
在 MetaTrader 5 客户端中共有四十多种图形对象。所有这些对象都可以单独使用,但是更常见的是在一个相互连接的对象链中使用它们。例如,当使用一个编辑字段 (OBJ_EDIT) 时,通常还会使用一个位图标签 (OBJ_LABEL), 以指明该编辑字段的功能。
使用编辑字段时,通常您不得不检查用户输入的数据是否正确,还要提供使用句点和逗号作为小数分隔符的可能性。
使用程序化数据输出时,您应对数据进行格式化。例如,您应删除不必要的零。因此,拥有一个包含编辑字段、位图标签和某些其他功能特征的单一对象会更加简单一些。
目前,在编程界有某些几乎在每一个应用程序中都有使用的图形控件:窗体(应用程序界面的基础,所有控件元件都位于其中)、框架(允许将具有一个功能目的的若干元件群组在一起)、按钮、编辑字段、标签、复选框、单选按钮、垂直和水平滚动条、列表、下拉列表、菜单行、菜单选项卡(图 3)。
图 3. 含有大多数常用标准控件的窗体
上述元件在 MQL5 中的表示与其他编程语言类似(按钮和编辑字段)。在您的兵工厂中准备其他普遍使用的控件会更加方便。
几种开发环境向程序员提供特殊的工具来创建自定义控件。MQL5 不提供此类功能。然而,因为 MQL5 是一种面向对象的语言,没有必要具备此类功能。一切都可以通过单独编写的对象的形式来进行。
本文将进一步讨论创建自定义控件的原则和方法。只需要基础知识,擅长编程的所有人都能够创建可以在应用程序中重复使用的一组必需控件。
1. 什么是图形控件
1.1. 一般要求和原则
要能使用,图形控件应使应用程序的开发更加容易。因此,它们应满足以下要求:
- 应该能够在开发期间快速创建一个控件。使用面向对象的编程方法解决了此问题。一个图形控件表示为一个编写好的对象。
- 控件应是灵活的。换言之,它应该能改变其属性:大小、位置、颜色等。
- 控件应易于使用 - 它应该只具有需要的属性和方法,并且能够从元件的目的和方法的名称理解它们的目的。现在,让我们对控件的属性分类:
- 不受控制的属性。此类属性包括颜色方案。在应用程序中使用的所有控件都应有一致的样式。这是为什么单独为每个控件设置颜色会令人疲乏不堪的原因。
此外,为某些控件搜索一种颜色也是非常困难的任务,我不想为其浪费时间。例如一个滚动条。某些 Web 开发人员可能会面临这一有趣的任务。
- 在控件的创建阶段设置的属性或很少更改的属性。例如一个控件的大小。在一个应用程序中将所有控件放在平衡和方便的位置是一项特殊而艰巨的任务,在界面制作阶段解决。
与之相关联的,在程序操作期间通常不会更改控件的大小。然而,有时您可能需要更改此类属性。这是为什么您应提供在程序的操作期间更改这些属性的可能的原因。
- 主要的可操作属性。经常从程序更改的属性。构成控件目的的属性。这些属性可分为两类:
- 自动更新控件显示的属性。例如一个编辑字段。一旦值被程序设定,则更改应显示在屏幕上。在编程中,应使用一行简单的代码执行它。
- 需要强制刷新显示的属性。例如一个列表。列表意味着处理数据数组,这是为什么不应该在处理列表的单个元件之后刷新列表的原因。在这里,最好是在处理完毕列表的所有元件之后执行强制更新。这种方法显著提升应用程序的性能。
- 快速且简单地隐藏和显示控件的可能性。使控件可见并不需要反复设置显示属性;对象属性的存取应独立于此控件的图形对象的可见性。换言之,编写好的对象应包含其所含控件的所有属性,并且不应使用图形对象的属性。
- 为了具备对应于控件的事件输出,应有对包含在控件内的单个图形对象的事件的处理。
1.2. 使用控件的方式和需要的方法
考虑到上述要求,我们制定出创建图形界面的以下方案,以及编写好的对象需要的属性和方法:
- 控件的初始化和很少更改的属性的并行设置。此方法将被称为 Init();它有几个参数。第一个强制参数 - 控件的名称。此参数将用作包含在该控件内的所有图形对象的名称的前缀。此外,可以包含设置控件的大小的参数以及其他参数(视控件的目的而定)。
- 控件在图表中可以有一个固定位置,也可能需要有移动的可能性。因此,我们将使用单独的方法来确定坐标:SetPosLeft() - 设置 X 坐标,SetPosTop() - 设置 Y 坐标。每种方法应有一个参数。通常情况下,必须更改两个坐标,因此最好具有带两个参数的 SetPos() 方法,该方法同时更改 X 坐标和 Y 坐标。
为了计算控件的位置,您可能需要获取另一控件的大小和位置的相关信息。为此,我们使用以下方法:Width() - 宽度、Height() - 高度、Left() - X 坐标、Top() - Y 坐标。在操作的这一阶段,计算控件的坐标,调用设置控件的方法。
- 在创建之后或在应用程序的操作的另一阶段,我们需要让控件可见。为此,我们使用 Show() 方法。要隐藏控件,使用 Hide() 方法。
- 如前所述,在程序的操作期间,我们可能需要更改控件的大小。这是为什么编写好的对象应具有单独的方法来设置大小 - SetWidth() 和/或 SetHeght() 的原因。因为这些是很少改变的属性,要使更改生效,我们需要调用更新显示的 Refresh() 方法。
- 单个图形对象的事件将在 Event() 方法中处理,该方法返回与控件的具体事件相对应的值。应从 OnChartEvent() 函数调用 Event() 方法;该方法具有与 OnChartEvent() 函数一样的参数。
因此,我们获得了编写好的对象需要的一组方法:
- void Init(string aName...)- 控件的初始化;
- void SetPosLeft(int aLeft) - 设置 X 坐标;
- void SetPosTop(int aTop) - 设置 Y 坐标;
- void SetPos(int aLeft, int aTop) - 同时设置 X 和 Y 坐标;
- void SetWidth(int aWidth) - 设置宽度;
- void SetHeght(int aHeight) - 设置高度;
- int Width() - 获取宽度;
- int Height() - 获取高度;
- int Left() - 获取 X 坐标;
- int Top() - 获取 Y 坐标;
- void Refresh() - 完全刷新显示;
- void Show() - 显示;
- void Hide() - 隐藏;
- int Event(const int id, const long & lparam, const double & dparam, const string & sparam) - 处理图表事件;
其他方法的存在(或以上某些方法的缺失)取决于控件本身及其目的。
在本文的后面,我们将尝试实施上述原则 - 我们将为用户创建一个用于输入文本或数字的控件。
在这之前,我们需要向我们自己提供用于快速方便地处理图形对象的工具。
2. 如何快速方便地处理图形对象
为了处理图形对象,MQL5提供了以下基本函数:ObjectCreate()、ObjectDelete()、ObjectSetDouble()、ObjectSetInteger()、ObjectSetString()、ObjectGetDouble()、ObjectGetInteger()、ObjectGetString()。我们可以直接使用这些函数,但是编程过程将会非常耗时耗力。这些函数具有较长的名称;很多具有长名称的标识符必须传递给函数。
为了让处理图形对象更加方便,我们可以使用一个包含在 MetaTrader 5 客户端包中的准备就绪的类(来自 MQL5/Include/ChartObjects/ChartObject.mqh 文件的 CChartObject 类),或者我们可以编写我们自己的类,并向其提供所有必需的方法。
开个玩笑,这种编程方法由按一个键,之后再按一个点组成。在指定对象的名称之后,剩下的就是输入一个点,然后对象的属性和方法列表就会打开;从列表中选择必需的项目(图 4)。
图 4. 对象的属性和方法的列表
使用辅助类管理图形对象的情形有两种:
- 为每个图形对象创建一个单独的类实例。这种方法相当便捷,但是从占用内存空间的观点来看并不经济。对于这种情形,最好为每种图形对象编写特殊的类。但是这种方法并不是最佳的,因为它的工作量非常大。与编写 EA 交易程序截然不同,创建用户界面时没有对最大性能的严格要求。
- 使用一个类的实例。如果必须要管理一个图形对象,则将对象添加到类。我们将使用第二种情形。
让我们创建一个最适合管理图形对象的第二种类型的通用类。
3. 管理图形对象的通用类
在编程期间,对每个图形对象的处理包含三个阶段:创建、读取/设置属性、在应用程序的操作结束时删除对象。
因此,管理图形对象的类首先应包含创建它们的方法。用于创建图形对象的一个强制参数为对象的名称;因此,创建对象的方法将有一个用于指定所创建对象的名称的强制参数。
通常情况下,图形对象是在程序(EA 交易程序、指标或脚本)运行所在的图表上创建的。子窗口是一种较为少见的情形,而客户端的另一个图表窗口则是更为少见的情形。因此,第二个可选参数将是指定子窗口编号的参数,第三个参数是一个图表的标识符。
在默认情况下,这两个可选参数都等于 0(价格走势图在其“自己的”图表上)。查看说明文档中的图形对象的类型列表。为每种类型添加创建方法。
在这之前,我们需要创建一个文件。为此,打开 MetaEditor,创建一个新的包含文件,并在 IncGUI.mqh 中调用该文件。在打开的文件中,创建含有受保护部分和公共部分的 CGraphicObjectShell 类。在受保护的部分中,为对象的名称和图表的标识符声明变量。
在对象的创建方法中,这些变量将被赋予方法作为参数传递的值,以便有可能在创建对象后无需指定名称和图表标识符也可以管理它们。因此,我们还可以使用针对管理图形对象的第一种情形的类。
要有可能使用针对第二种情形的类(管理任何图形对象),向其提供附加一个图形对象的方法(Attach() 方法)。这种方法有一个强制参数 - 图形对象的名称,还有一个可选参数 - 图表标识符。您可能需要知道附加的图形对象的名称和标识符。为此,向对象添加了以下方法:Name() 和 ChartID()。
结果,我们获得类的以下“工件”:
class CGraphicObjectShell { protected: string m_name; long m_id; public: void Attach(string aName,long aChartID=0) { m_name=aName; m_id=aChartID; } string Name() { return(m_name); } long ChartID() { return(m_id); } };添加上述创建图形对象的方法。这些方法的名称将以 "Create" 开头。
读者可以跳过这一点,本文附带的文件 IncGUI.mqh 包含了准备就绪的类 CGraphicObjectShell。
作为一个例子,以下是创建垂直线图形对象 (OBJ_VLINE) 的一种方法:
void CreateVLine(string aName,int aSubWindow=0,long aChartID=0) { ObjectCreate(m_id,m_name,OBJ_VLINE,aSubWindow,0,0); Attach(aName,aChartID); }现在,打开本用户指南中的图形对象的属性列表;编写使用 ObjectSetDouble()、ObjectSetInteger() 和 ObjectSetString() 函数为每个属性设置值的方法。这些方法的名称将以 "Set" 开头。接下来,编写使用 ObjectGetDouble()、ObjectGetInteger() 和 ObjectGetString() 函数读取属性的方法。
作为一个例子,以下是设置和获取颜色的方法:
void SetColor(color aColor) { ObjectSetInteger(m_id,m_name,OBJPROP_COLOR,aColor); } color Color() { return(ObjectGetInteger(m_id,m_name,OBJPROP_COLOR)); }现在,我们似乎已经具有最低要求的图形对象处理工具,但并不是全部工具。
在处理图形对象的某些时候,您可能对一个对象仅需要执行一个操作。在这种情形下,为对象执行 Attach() 方法,然后返回到主对象,再次为对象执行 Attach() 方法并不方便。
让我们向类添加另外两种设置/获取属性的方法。
第一种 - 在其“自己的”图表上按名称:
void SetColor(string aName,color aColor) { ObjectSetInteger(0,aName,OBJPROP_COLOR,aColor); } color Color(string aName) { return(ObjectGetInteger(0,aName,OBJPROP_COLOR)); }第二种 - 按名称和图表的标识符:
void SetColor(long aChartID,string aName,color aColor) { ObjectSetInteger(aChartID,aName,OBJPROP_COLOR,aColor); } color Color(long aChartID,string aName) { return(ObjectGetInteger(aChartID,aName,OBJPROP_COLOR)); }
除了ObjectGet...和ObjectSet...函数以外,还有用于处理图形对象的其他函数:ObjectDelete()、ObjectMove()、ObjectFind()、ObjectGetTimeByValue()、ObjectGetValueByTime()、ObjectsTotal()。它们也可以被添加到类,每一个具有三种调用情形。
最后,在此文件中用一个简短的名称 "g" 来声明CGraphicObjectShell类。
CGraphicObjectShell g;
现在,要开始处理图形对象,已经足以连接 IncGUI.mqh 文件;这样,我们将拥有要处理的 "g" 类。通过该类,可以轻松地管理所有可用的图形对象。
4. 控件的工件
尽管我们拥有了用于快速处理图形对象的类,我们还是可以通过一种更容易的方式创建控件。所有控件都可以通过四个图形对象来创建:
- 矩形标签 (OBJ_RECTANGLE_LABEL)
- 文本标签 (OBJ_LABEL)
- 编辑字段 (OBJ_EDIT)
- 按钮 (OBJ_BUTTON)
在创建图形对象之后,应该为它们设置很多属性:坐标、大小、颜色、字号等。为了加速过程,让我们创建另一个类并将其命名为 CWorkPiece(工件),然后向其提供通过在参数中传递的属性创建图形对象的方法。
要使控件工作,您需要处理图表事件。不能处理其他图表的事件,因此我们将仅处理自己的图表 - 在 CWorkPiece 类的方法的参数中没有图表标识符。将在任何地方使用 0(自己的图表)。
将使用指定子窗口编号的参数来提供在价格趋势图及其子窗口中创建控件的可能性。图形对象将仅被绑定到左上角;如果必须相对于任何其他角落重新定位控件,则考虑到图表大小,重新计算整个控件的坐标会容易得多。要控制图表的大小改变,您可以处理 CHARTEVENT_CHART_CHANGE 事件。
作为很多控件的基础,我们将使用“矩形标签”对象;向 CWorkPiece 类添加创建此对象的方法;方法被称为 Canvas():
void Canvas(string aName="Canvas", int aSubWindow=0, int aLeft=100, int aTop=100, int aWidth=300, int aHeight=150, color aColorBg=clrIvory, int aColorBorder=clrDimGray) { g.CreateRectangleLabel(aName,aSubWindow); // 创建长方形标签 g.SetXDistance(aLeft); // 设置 X 轴坐标 g.SetYDistanse(aTop); // 设置 Y轴坐标 g.SetXSize(aWidth); // 设置宽度 g.SetYSize(aHeight); // 设置高度 g.SetBgColor(aColorBg); // 设置背景颜色 g.SetColor(aColorBorder); // 设置边界颜色 g.SetCorner(CORNER_LEFT_UPPER); // 设置定位点 g.SetBorderType(BORDER_FLAT); // 设置边线类型 g.SetTimeFrames(OBJ_ALL_PERIODS); // 设置在所有时间框架上可见 g.SetSelected(false); // 禁用选择 g.SetSelectable(false); // 禁用可选 g.SetWidth(1); // 设置边线宽度 g.SetStyle(STYLE_SOLID); // 设置边线样式 }
注意:方法包含 14 行代码;每次创建此对象时都要编写所有这些代码会让人感到非常受挫。现在,仅编写一行代码就足够了,方法的所有参数都是可选的,并且按它们的使用频率排列:位置、大小、颜色等。
类似于 Canvas() 方法,编写创建文本标签、按钮和编辑字段的方法:Label()、Button() 和 Edit()。本文附带的 IncGUI.mqh 文件包含了准备就绪的 CWorkPiece 类。除了上述方法以外,该类还包含其他几个方法:Frame() 和 DeleteFrame() - 创建和删除框架的方法(图 5)。框架是在其左上角有一个标题的矩形标签。
框架在群组窗体中的控件时使用。
图 5. 带有标题的框架。
本文附带了 CWorkPiece 类的所有方法的列表。
类似于 CGraphicObjectShell 类,用简短名称 "w' 声明 CWorkPiece 类,以便能够在连接 IncGUI.mqh 文件后就能立即使用它。
CWorkPiece w;
所有辅助工具都已准备就绪,因此我们可以继续本文的主题 - 创建自定义控件。
5. 创建“编辑”控件
首先,为了不混淆术语,让我们称图形对象 OBJ_EDIT 为文本字段,称 OBJ_LABEL 对象为标签,称创建的控件为编辑字段。创建的控件由两个图形对象构成:一个编辑字段 (OBJ_EDIT) 和一个文本标签 (OBJ_LABEL)。
控件将支持两种操作模式:文本数据输入和数字数据输入。在数字数据输入模式中,有输入值的范围限制,并且接受逗号和句点作为小数分隔符。在程序输出编辑字段中的值时,依据指定的小数位数对其进行格式化。
因此,在对控件进行初始化时,我们应指定其操作模式:文本或数字;使用 aDigits 参数指定该模式。大于零的值将模式设置为数字模式,指定小数位数为该参数值,负值将模式设置为文本模式。
默认情况下,值的可接受范围为 -DBL_MAX 至 DBL_MAX(双精度变量的整个值范围)。如有必要,您可以通过调用 SetMin() 和 SetMax() 方法设置另一个范围。在大小参数中,将仅设置控件的宽度。为了使编辑字段看起来平衡,应为其设置对应的高度和宽度。
字号的更改需要图形对象高度的相应更改。并且这需要更改其他控件的位置;没人会这样做。我们假定所有控件和对应的编辑字段都使用恒定不变的字号。但是,控件的类有一个返回其高度的方法,以便于计算其他元件的坐标。
有四个颜色参数:背景色、文本颜色、标题颜色和警告颜色(例如,在输入不正确的值时,可以更改文本字段的背景色以引起用户的注意)。
如前所述,支持子窗口中的控件。除了控件工作所需的主要参数以外,我们将使用另一个参数 Tag(标记);它是一个存储在类的实例中的简单文本值。标记是一个非常方便的辅助工具。
该类将被称为 CInputBox。因此,我们有了以下一组类的变量(在私有部分中):
string m_NameEdit; // Edit对象的名称 string m_NameLabel; // Lable对象的名称 int m_Left; // X轴坐标 int m_Top; // Y轴坐标 int m_Width; // 宽度 int m_Height; // 高度 bool m_Visible; // 控件的可视化标识 int m_Digits; // 双精度数字的小数位数;采用文本模式时等于-1 string m_Caption; // 标题 string m_Value; // 值 double m_ValueMin; // 最小值 double m_ValueMax; // 最大值 color m_BgColor; // 背景颜色 color m_TxtColor; // 文本颜色 color m_LblColor; // 标题颜色 color m_WarningColor; // 告警字体颜色 bool m_Warning; // 告警标识 int m_SubWindow; // 子窗口 string m_Tag; // 标签当我们使用一个控件时,第一个方法称为 Init()。
在这个方法中,我们准备以前确定的所有参数的值:
// 初始化方法 void Init(string aName="CInputBox", int aWidth=50, int aDigits=-1, string aCaption="CInputBox") { m_NameEdit=aName+"_E"; // 准备文本区域的名称 m_NameLabel=aName+"_L"; // 准备标题名称 m_Left=0; // X轴坐标 m_Top=0; // Y轴坐标 m_Width=aWidth; // 宽度 m_Height=15; // 高度 m_Visible=false; // 可视性 m_Digits=aDigits; // 操作模式和小数位数 m_Caption=aCaption; // 标题文本 m_Value=""; // 文本模式下的值 if(aDigits>=0)m_Value=DoubleToString(0,m_Digits); // 数字模式下的值 m_ValueMin=-DBL_MAX; // 最小值 m_ValueMax=DBL_MAX; // 最大值 m_BgColor=ClrScheme.Color(0); // 文本区域的背景颜色 field m_TxtColor=ClrScheme.Color(1); // 文本的颜色和文本区的框架 m_LblColor=ClrScheme.Color(2); // 标题颜色 m_WarningColor=ClrScheme.Color(3); // 告警颜色 m_Warning=false; // 模式:告警,正常 m_SubWindow=0; // 子窗口编号 m_Tag=""; // 标签 }
如果控件在文本模式下工作,则 m_Value 变量被赋予值 "",如果它在数字模式下工作,则被赋予 0 及指定的小数数位。颜色参数被设置为它们的默认值;我们将在最后阶段处理颜色方案。
确定控件坐标的变量被设置为零,因为控件仍然不可见。在调用 Init() 方法后(如果计划控件在图表上有一个固定位置),我们可以用 SetPos() 方法设定坐标:
// 设置X和Y轴坐标 void SetPos(int aLeft,int aTop) { m_Left=aLeft; m_Top=aTop; }之后,我们可以让控件可见(Show() 方法):
// 在先前指定位置显示 void Show() { m_Visible=true; // 设置为可见 Create(); // 创建图形对象 ChartRedraw(); // 刷新图表 }
Create() 函数是从 Show() 方法调用的;它创建图形对象(在私有部分中),然后刷新图表 (ChartRedraw())。下面给出 Create() 函数的代码:
// 创建图形对象的函数 void Create(){ color m_ctmp=m_BgColor; // 正常的背景颜色 if(m_Warning){ // 设置报警模式 m_ctmp=m_WarningColor; // 文本区域使用报警颜色 } // 创建文本区域 w.Edit(m_NameEdit,m_SubWindow,m_Left,m_Top,m_Width,m_Height,m_Value,m_ctmp,m_TxtColor,7,"Arial"); if(m_Caption!=""){ // 如果有标题 // 创建标题 w.Label(m_NameLabel,m_SubWindow,m_Left+m_Width+1,m_Top+2,m_Caption,m_LblColor,7,"Arial"); } }在 Create() 函数中创建图形对象时,视 m_Warning 的值而定,文本字段被赋予相应的背景色。如果 m_Caption 变量含有一个值,则创建标题(您可以创建没有标题的控件)。
如果您计划创建一个可移动的控件,请使用 Show() 方法的第二种情形 - 指定坐标。在这种方法中,设定坐标并调用 Show() 方法的第一种情形:
// 设置X和Y轴坐标 void SetPos(int aLeft,int aTop) { m_Left=aLeft; m_Top=aTop; }在显示控件之后,有时您需要隐藏它。
Hide() 方法用于此目的:
// 隐藏(删除图形对象) void Hide() { m_Visible=false; // 设置为不可见状态 Delete(); // 删除图形对象 ChartRedraw(); // 刷新图表 }Hide() 方法调用删除图形对象的 Delete() 函数,然后它调用 ChartRedraw() 函数来刷新图表。Delete() 函数位于私有部分中:
// 删除图形对象的函数 void Delete() { ObjectDelete(0,m_NameEdit); // 删除文本区域 ObjectDelete(0,m_NameLabel); // 删除标题 }因为我们已经使用仅设置属性值而不更改控件显示的方法(SetPos() 方法),创建用于强制刷新控件的方法 - Refresh() 方法是符合逻辑的。
// 刷新显示(删除并创建) void Refresh() { if(m_Visible) { // 可见 Delete(); // 删除图形对象 Create(); // 创建图形对象 ChartRedraw(); // 重绘图表 } }
控件相当简单,这是为什么我们使用简单的刷新方法 - 删除和创建的原因。如果是一个更加复杂的控件,例如包含很多编辑字段的列表,则我们会选择更明智的方法。
这样,我们完成了控件的放置。现在,让我们继续设置值 - SetValue() 方法。因为控件可以在两种模式下工作,SetValue() 方法也有两种情形:字符串类型和双精度类型。在文本模式中,值的使用如下所示:
// 设置文本值 void SetValue(string aValue) { m_Value=aValue; //为变量分配存储值 if(m_Visible) { // 开启控件的可见性 // 为管理图形对象的对象分配文本区域 g.Attach(m_NameEdit); g.SetText(m_Value); // 为文本区设置值 ChartRedraw(); // 重绘图表 } }获得的自变量被赋予 m_Value 变量,并且如果控件可见,它将显示在文本字段中。
在数字模式中,获得的自变量依据 m_Digits 值进行标准化,然后依据最大值和最小值(m_MaxValue、m_MinValue)进行纠正,转变为一个字符串,再调用第一个方法 SetValue()。
// 设置数值 void SetValue(double aValue) { if(m_Digits>=0) { // 在数字模式下 // 根据指定的精度标准化该数字 aValue=NormalizeDouble(aValue,m_Digits); // 根据最小允许值“调整”数值 aValue=MathMax(aValue,m_ValueMin); // 根据最大允许值“调整”数值 aValue=MathMin(aValue,m_ValueMax); // 将获取到的值设置为一个字符串 SetValue(DoubleToString(aValue,m_Digits)); } else { // 在文本模式下 SetValue((string)aValue); // 为变量分配存储值 } }让我们编写两个用于获取值的方法:一个用于获取字符串值,另一个用于获取双精度值:
// 获取文本值 string ValueStrind() { return(m_Value); } // 获取一个数值 double ValueDouble() { return(StringToDouble(m_Value)); }
为控件设定的值依据可接受的最大值和最小值进行纠正;让我们添加获取和设置它们的方法:
// 设置最大允许值 void SetMaxValue(double aValue) { m_ValueMax=aValue; // 记录新的最大可接受值 if(m_Digits>=0) { // 控件在数字模式下运行 if(StringToDouble(m_Value)>m_ValueMax) { /* 控件的当前值大于新的最大可接受值*/ SetValue(m_ValueMax); // 设置新值为最大可接受值 } } } // 设置最小可接受值 void SetMinValue(double aValue) { m_ValueMin=aValue; // 记录新的最小可接受值 if(m_Digits>=0) { // 控件在数字模式下运行 if(StringToDouble(m_Value)<m_ValueMin) { /* 控件的当前值小于新的最小可接受值*/ SetValue(m_ValueMin); // 将新值设置为最小可接受值 } } } // 获取最大可接受值 double MaxValue() { return(m_ValueMax); } // 获取最小可接受值 double MinValue() { return(m_ValueMin); }如果控件在数字模式下工作,则在设定新的最大和最小可接受值时对当前值进行检查和纠正(如有必要)。
现在让我们处理用户的值输入,Event() 方法。使用 CHARTEVENT_OBJECT_ENDEDIT 事件检查用户的数据输入。在文本模式下工作时,如果用户指定的值不等于 m_Value,则新的值被赋予 m_Value,并且将 Event() 方法返回的 m_event 变量的值设为 1。
在数字模式下工作时,在 m_OldValue 中记住先前的 m_Value 变量值,用句点代替逗号,将字符串转换为数字,然后将其传递到 SetValue() 函数。接着,如果 m_Value 和 m_OldValue 不相等,则“生成”事件(将 m_event 变量的值设为 1)。
// 事件句柄 int Event(const int id, const long & lparam, const double & dparam, const string & sparam) { bool m_event=0; // 对应此控件事件的变量 if(id==CHARTEVENT_OBJECT_ENDEDIT) { // 结束编辑文本区域的事件 if(sparam==m_NameEdit) { //名称为m_NameEdit的文本区域被更改 if(m_Digits<0) { // 在文本模式下 g.Attach(m_NameEdit); // 分配文本区域来控制它 if(g.Text()!=m_Value) { // 文本区的新值 m_Value=g.Text(); // 为变量分配存储值 m_event=1; // 有事件发生 } } else { // 在数字模式下 string m_OldValue=m_Value; // 控件的前值变量 g.Attach(m_NameEdit); // 分配文本区域来控制它 string m_stmp=g.Text(); // 从文本区获取用户指定的文本 StringReplace(m_stmp,",","."); // 用点代替逗号 double m_dtmp=StringToDouble(m_stmp); // 转换成数字 SetValue(m_dtmp); // 设置新的数值 // 比较前值和现值 if(StringToDouble(m_Value)!=StringToDouble(m_OldValue)) { m_event=1; // 有事件发生 } } } } return(m_event); // 返回事件。0 - 无事件发生,1 - 有事件发生 }支持控件在子窗口中工作。为了提供这一功能,添加 SetSubWindow() 方法,该方法在出现 CHARTEVENT_CHART_CHANGE 事件时从 OnChartEvent() 函数调用。如果您计划仅在价格走势图上使用控件,则无需调用此方法。
已经声明了 m_SubWindow 变量,默认情况下它等于 0,并且在创建控件的图像对象时被传递到 "w" 类的 Edit() 和 Label() 方法。子窗口的编号将被传递到 SetSubWindowName() 方法;如果编号改变,则更改 m_SubWindow 变量的值并执行 Refresh() 方法。
//通过编号设置一个子窗口。 void SetSubWindow(int aNumber) { int m_itmp=(int)MathMax(aNumber,0); /*如果子窗口编号为负,用0用来表示价格图表*/ if(m_itmp!=m_SubWindow) { /* 控件所在窗口的编号和指定窗口编号不符*/ m_SubWindow=m_itmp; // 记录新的子窗口编号 Refresh(); // 重新创建图形对象 } }或许,将子窗口的名称而不是其编号传递给函数更加方便。添加 SetSubWindow() 方法的另一种情形:
// 根据子窗口名称设置子窗口 void SetSubWindow(string aName) { SetSubWindow(ChartWindowFind(0,aName)); // 通过名称检测子窗口编号并根据编号设置子窗口 }
依据本文开头指出的概念,向控件的类供应其他缺失的方法。
一旦我们具有允许同时设置控件的两个坐标的 SetPos() 方法,添加单独设置坐标的方法:
// 设置X轴坐标 void SetPosLeft(int aLeft) { m_Left=aLeft; } // 设置Y轴坐标 void SetPosTop(int aTop) { m_Top=aTop; }设置宽度的方法:
// 设置宽度 void SetWidth(int aWidth) { m_Width=aWidth; }
获取坐标和大小的方法:
// 获取X轴坐标 int Left() { return(m_Left); } // 获取Y轴坐标 int Top() { return(m_Top); } // 获取宽度 int Width() { return(m_Width); } // 获取高度 int Height() { return(m_Height); }处理标记的方法:
// 设置标签 void SetTag(string aValue) { m_Tag=aValue; } // 获取标签 string Tag() { return(m_Tag); }
用于警告的方法:
// 设置告警模式 void SetWarning(bool aValue) { if(m_Visible) { // 启用可见性 if(aValue) { // 我们需开启告警模式 if(!m_Warning) { // 告警模式未启用 g.Attach(m_NameEdit); // 分配用于控制的文本区域 g.SetBgColor(m_WarningColor); // 设置文本区域的告警颜色背景 } } else { // 我们需要禁用告警模式 if(m_Warning) { // 告警模式启用 g.Attach(m_NameEdit); // 分配用于控制的文本区域 g.SetBgColor(m_BgColor); // 设置正常的字体颜色 } } } m_Warning=aValue; // 注册当前模式 } // 获取告警模式 bool Warning() { return(m_Warning); }如果在设置警告模式时控件可见,则检查传递到 SetWarning 方法的参数值;如果其值与控件的当前状态不相对应,则显示文本字段的背景色。
不管怎么样,设置的模式被注册,而不是在控件不可见时将对应的颜色设置到文本字段。
还剩下一个属性 - m_Digits。让我们添加用于获取和设置其值的方法:
// 设置小数位数 void SetDigits(int aValue) { m_Digits=aValue; // 设置新值 if(m_Digits>=0) { // 数字模式 SetValue(ValueDouble()); // 重设当前值 } } // 获取m_Digits的值 int Digits() { return(m_Digits); }我们已经完成最有趣的部分。现在,是时候进行最精彩的部分了。
6. 颜色方案
颜色方案将存储在 CСolorSchemes 类的变量中。
该类将在 IncGUI.mqh 文件中预先声明,名称为 ClrScheme。为了设置一个颜色方案,我们将调用 SetScheme() 方法,并且将颜色方案的编号指定为一个参数。如果未调用 SetScheme() 方法,则将使用编号为 0 的颜色方案。
为了获取一个颜色,我们将使用 Color() 方法,其中含有来自颜色方案的指定颜色编号。让我们编写含有公共部分和私有部分的 CСolor 方案类。在私有部分中,为存储颜色方案的索引声明 m_ShemeIndex 变量。在公共部分中,编写 SetScheme() 方法:
// 设置颜色方案编号 void SetScheme(int aShemeIndex) { m_ShemeIndex=aShemeIndex; }Color() 方法。在方法中声明了一个两维数组:第一维是颜色方案编号;第二维是方案中的颜色编号。视指定的颜色方案的编号而定,它按在方法参数中指定的编号返回颜色。
color Color(int aColorIndex) { color m_Color[3][4]; // 第一维 - 色彩方案编号,第二维 - 色彩方案中的颜色数量 // 默认 m_Color[0][0]=clrSnow; m_Color[0][1]=clrDimGray; m_Color[0][2]=clrDimGray; m_Color[0][3]=clrPink; // 黄 黑 m_Color[1][0]=clrLightYellow; m_Color[1][1]=clrBrown; m_Color[1][2]=clrBrown; m_Color[1][3]=clrPink; // 蓝色 m_Color[2][0]=clrAliceBlue; m_Color[2][1]=clrNavy; m_Color[2][2]=clrNavy; m_Color[2][3]=clrPink; return(m_Color[m_ShemeIndex][aColorIndex]); // 根据色彩方案和方案中的颜色数量返回一个值 }
目前为止,每个颜色方案包含四种颜色;其中两个有相同的值。此外,在创建其他控件时,我们可能需要更多的颜色。
为了在方案中轻松找到一个适当的颜色,或决定添加新的颜色,类包含了一个允许查看颜色的方法 - Show() 方法(图 6)。还有一个相反的方法 Hide(),用于从图表上删除颜色示例。
图 6. 使用 Show() 方法查看颜色方案
本文附带了 ColorSchemesView.mq5。它是一个用于查看颜色方案的 EA (ColorSchemesView.mq5)。
让我们稍微修改一下 CInputBox 类中的 Init() 方法。用来自 ClrScheme 类的颜色代替其颜色:
m_BgColor=ClrScheme.Color(0); // 文本区域的背景颜色 field m_TxtColor=ClrScheme.Color(1); // 字体的颜色和文本区的框架颜色 m_LblColor=ClrScheme.Color(2); // 标题颜色 m_WarningColor=ClrScheme.Color(3); // 告警颜色这样就结束了一个控件的创建;现在,我们拥有了开发任何其他控件的基础。
7. 使用控件
让我们创建一个 EA 并将其命名为 GUITest;连接到 IncGUI.mqh 文件:
#include <IncGUI.mqh>
用名字 ib 声明 CInputBox 类:CInputBox ib;
在 EA 的 OnInit() 中,调用 ib 对象的 Init() 方法:
ib.Init("InpytBox",50,4,"input");
使控件可见,并设置控件的位置:
ib.Show(10,20);
在 EA 的 OnDeinit() 函数中删除控件:
ib.Hide();
编译并将 EA 附加到一个图表。您将看到我们的控件(图 7)。
图 7. InputBox 控件
添加更改 EA 颜色方案的可能性。
目前我们有三个颜色方案。让我们进行一次枚举并使用一个外部变量来选择颜色方案:
enum eColorScheme { DefaultScheme=0, YellowBrownScheme=1, BlueScheme=2 }; input eColorScheme ColorScheme=DefaultScheme;在 EA 的 OnInit() 函数的刚开始处添加一个颜色方案的设置:
ClrScheme.SetScheme(ColorScheme);
现在,在 EA 的属性窗口中,我们可以选择三个颜色方案之一(图 8)。
图 8. 不同的颜色方案
为了处理指定新值的事件,向 EA 的 OnChartEvent() 函数添加以下代码:
if(ib.Event(id,lparam,dparam,sparam)==1) { Alert("Entered value "+ib.ValueStrind()); }现在,在编辑字段中指定一个新值时,一个消息窗口打开,通知指定的值。
向 EA 提供在子窗口中创建控件的可能性。
首先,创建一个测试指标 TestSubWindow(附加在 TestSubWindow.mq5 文件中)。在 MQL5 Wizard 中创建指标时,指定它应在单独的子窗口中工作。向 EA 的 OnChartEvent() 函数添加以下代码:
if(CHARTEVENT_CHART_CHANGE) { ip.SetSubWindow("TestSubWindow"); }现在,如果指标不在价格走势图上,则在价格走势图上创建控件。如果您将指标附加到价格走势图上,则控件将跳到子窗口中(图 9)。如果您删除指标,则控件返回到价格走势图。
图 9. 子窗口中的控件
总结
作为已完成工作的结果,我们具有包含以下类的 IncGUI.mqh 包含文件:CGraphicObjectShell(创建和管理图形对象)、CWorkPiece(通过使用参数来设置它们的属性,快速创建几个图形对象)、CColorSchemes(设置一个颜色方案及获取当前颜色方案的颜色)以及控件的一个类 - CInputBox。
CGraphicObjectShell、CWorkPiece 和 CColorSchemes 类已经在文件中分别用 "g"、"w" 和 "ClrScheme" 进行声明,即在连接 IncGUI.mqh 文件之后马上就要使用它们。
让我们重复一下如何使用 CInputBox 类:
- 连接 IncGUI.mqh 文件。
- 声明 CInputBox 类型的类。
- 调用 Init() 方法。
- 使用 SetPos() 方法设置坐标;如有必要,使用 Show() 启用可见性。第二种情形:使用指定了坐标的 Show() 启用可见性。
- 如有必要,或在 EA 的工作结束时,使用 Hide() 方法隐藏控件。
- 向 OnChartEvent() 函数添加 Event() 方法的调用。
- 如果需要在子窗口中创建控件,向 OnChartEvent() 函数供应在 CHARTEVENT_CHART_CHANGE 事件发生时进行的 SetSubWindow() 方法调用。
- 要使用颜色方案,在调用 Init() 方法之前调用 ClrScheme 类的 SetScheme() 方法。
附件
- IncGUI.mqh - 主要的包含文件。文件应放在客户端数据文件夹的 MQL5/Include 文件夹中。
- GUITest.mq5 - 含有 CInputBox 控件示例的 EA。文件应放在客户端数据文件夹的 MQL5/Experts 文件夹中。
- TestSubWindow.mq5 - 用于测试在子窗口中显示一个控件的函数的指标。文件应放在客户端数据文件夹的 MQL5/Indicators 文件夹中。
- ColorSchemesView.mq5 - 用于查看颜色方案的 EA。一个用于创建控件的辅助工具。文件应放在客户端数据文件夹的 MQL5/Experts 文件夹中。
- IncGUImqh.chm - IncGUI.mqh 文件的说明文档。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/310
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




我不明白第四项原则。你能举个例子吗?
你好,Leo,当你提到第四项原则时,你具体指的是什么?我知道这篇文章的主题 4 涉及 "控制工件"。您能进一步说明一下您的问题吗?
我想创建一个自定义图形对象,它由矩形、方框和一些信息组成。我会经常使用这个对象,因此我希望它能方便访问。是否可以在 MT5 工具栏中添加自定义图形对象?如果不能,能否提供一些其他建议?
奇怪...
不是应该这样吗?
或者像这样
一个错字