
利用 MQL5 的交互式 GUI 改进您的交易图表(第 II 部分):可移动 GUI(II)
概述
欢迎阅读本系列的第二部分。在第一部分中,我们讨论了如何创建一个简单的可移动仪表板。第二部分旨在实现相同的目标,但会以更高效的方式,适用于成熟的 EA/指标应用程序。
例如,如果我们想在屏幕上有两个可移动的仪表板,我们需要复制现有代码,并创建六个额外的全局变量,每个变量都有不同的名称。现在,如果我们决定需要三个可移动的仪表板,代码的复杂度将急剧增加,并且更难管理。显然,我们需要一种更加精简的方式。
幸运的是,我们可以求助于 .mqh 文件来简化这个过程。
以下是我们将在本文中涵盖的内容:
理解类的概念
在我们深入研究之前,首先必须理解类的概念。随着我们深入研究,类可能会变得非常高级和复杂,但就目前,我们只涵盖了基础知识。理解并有效利用这些基本概念是进阶更复杂细节之前的重要步骤。
那么,究竟什么是类?
简单说,类是一种复杂的数据类型,与 int、string、等相似,但触及更复杂的层面。
定义类的方式有很多种,但从根本上说,它们可以被看作是代码的集群。您也许会问,什么样的代码?它们的典型情况是函数(通常称为方法)和变量的集合。有些人也学会认为这个定义有些模糊或不太准确。不过,重点是要记住,我们无需像在学校里那样为应付考试而死记硬背。我们的主要目标是利用类的力量,令编码更易于管理和高效,为此,严格的定义并非关键。
从本质上讲,类是函数和变量的集合,我们可以利用它们来发挥自己的优势。
现在,这种理解自然而然地导致了四个基本问题:
- 我们在哪里创建它们?
- 我们如何声明它们?
- 如何编写它们?
- 如何使用它们?
顺便说一句,如果您挠头苦思,好奇为什么我们第一处就需要类,答案很直白。类简化了编码过程,令代码管理更轻而易举。
- 我们在哪里创建它们?
用于创建类的文件类型选择 — 可以是 .mq5 或 .mqh — 很灵活。不过,典型情况下,我们选择单独的 .mqh 文件。
在 .mq5 和 .mqh 中创建类,它们之间的区别值得注意。如果您是在 .mqh 文件中开发类,则需要将其导入到 .mq5 之中。这是因为要创建可运行的 EA/指标,唯有通过 .mq5 文件编译执行代码。不过,如果您直接在 .mq5 文件中建立类,则不需要任何导入过程。
我们通常倾向于单独的 .mqh 文件,因为它们增强了代码的可管理性。导入过程非常直白 — 仅需单行代码而已。为了便于讨论,我们将使用一个单独的 .mqh 文件。
- 我们如何声明它们?
类的声明很直白。下面示例是如何声明一个简单的空类:
class YourClassName { };
在上面的代码片段中,“YourClassName” 是一个占位符。将 “YourClassName” 替换为您想分配给类的实际名称。
- 怎样编写它们?
为了理解这一点,我们先讨论变量,然后再讨论函数。
假设您希望声明两个变量:其一是 “int” 类型,另一个是 “bool” 类型。您可以通过以下手段做到:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class YourClassName { int var1; bool var2; }; //+------------------------------------------------------------------+
重要的是,请注意,不能直接在类声明中为这些变量赋值。例如,以下代码将导致错误:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class YourClassName { int var1 = "int var"; bool var2 = true; }; //+------------------------------------------------------------------+
您将遇到的错误会说:
'=' — 非法赋值
'=' — 非法赋值
在我们的逻辑依赖于初始变量值的境况下,我们可以利用称之为构造函数的东西。此外,我们还有一种称为析构函数的东西,它本质上是构造函数的对应过程。
构造函数和析构函数是始终与类关联的特殊函数,无论是否显式声明它们。如果您未声明它们,则它们会隐式作为空函数。在声明类的实例时执行构造函数,而析构函数则在类实例出离界域时执行。重点要注意的是,在 MQL5 中,无法显式删除类的实例。
例如,在我们代码中的 OnInit() 函数充当构造函数,而 OnDeinit() 函数充当析构函数。此处的类隐藏在后台以维护简单性。这种范式在许多语言中都很常见,包括 Java,它总是包含一个默认类。
我们将在下一步中讨论“实例”的含义(如何使用它们?)。现在,明白我们可用构造函数为变量赋初值。析构函数与本讨论无关,但我们肯定会在本系列的后面部分涵盖它。请注意,强烈建议使用构造函数。
虽然一些程序员也许会忽略编译器警告,并假设如果他们没有显式定义变量,则它们将被隐式设为默认值,但这种方式并不完全正确,也不完全错误。
这个假设源于这样一个事实,即在许多编程语言中(甚至在 MQL4 中),没有显式定义的变量默认为某个值,而不会导致代码不一致。不过,在 MQL5 中,如果您没有显式定义变量,您可能会在代码中遇到不一致的情况。
以下是最常用数据类型的假定默认值:
类型 声明代码 假定默认值 int int test; 0 double double test; 0.0 bool bool test; false string string test; NULL datetime datetime test; 1970.01.01 00:00:00 注意: “假定默认值”是打印未初始化变量时看到的值。如果使用 if 语句进行相同的检查,则某些语句的行为将按预期进行,但另一些则不会,从而可能导致问题。
我们的测试脚本如下所示:
void OnStart() { type test; if(test == presumedDefaultValue) { Alert("Yes, This is the default value of the test variable."); } else { Alert("No, This is NOT the default value of the test variable."); } }
将 “type” 替换为某个变量类型,将 “presumedDefaultValue” 替换为预期的默认值。
于此,您会看到对于 bool 和 string,一切正常,并且将触发警报“是的,这是测试变量的默认值”。然而,对于 int、double 和 datetime,就不是那么直白了。您将收到一条警报,说是 “不,这不是测试变量的默认值。”此意外结果可能会导致逻辑故障。
现在我们明白了构造函数的重要性,我们来看看如何创建一个构造函数:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class name { private: public: int var1; bool var2; name(); ~name(); }; //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ name::name() { var1 = 0; var2 = true; } //+------------------------------------------------------------------+
在此,我们在类中包含了 public 和 private 字段,尽管 private 当前为空。使用公开访问修饰符声明构造函数,从而能在其它文件中调用它非常重要。为了解释这一点,我们讨论访问修饰符(已知还称为访问规范符)。
访问修饰符定义编译器如何访问变量、结构成员、或类。
访问修饰符 说明 public 允许不受限制地访问变量或类方法
private 允许从该类内部的方法,以及公开继承类的方法访问。不能从它处访问;
protected 仅允许从相同类的方法访问变量和类方法。
virtual 仅适用于类方法(但不适用于结构方法),并告诉编译器此方法应放在类的虚拟函数表之中。 在本文中,我们仅需了解 public 和 private,未用到其它,但我们肯定会在本系列的后面部分讲解它们。
在本文中,我们将重点讲解 public 和 private 修饰符,而其它修饰符留待本系列的后部。公开本质上意味着定义为 public 的变量和函数可以在任何地方调用/修改(变量),包括不同的 .mq5 或 .mqh 文件。另一方面,private 只允许访问当前类中定义的函数。
如果您在懵圈,为什么第一处就需要它们?这有很多原因,比如数据隐藏、抽象、可维护性、可重用性。
我们定义了构造函数代码,如下所示:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ name::name() { var1 = 0; var2 = true; } //+------------------------------------------------------------------+
构造函数/析构函数没有返回类型。
请注意,构造函数/析构函数没有返回类型。我们在此简单地给未初始化变量赋值,因为有时您也许需要将 bool 变量(即本例中的 var2)设置为 true 作为初始值。
还有替代方式可以做到这一点:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ name::name() : var1(0), var2(true) { } //+------------------------------------------------------------------+
这个构造函数没有主体,简单地把未初始化变量的值初始化为我们想要的值。虽然您可以采用任意一种方法,但通常最好使用第二种。举例,如果您用 const 声明变量令其不可更改,则第一种给未初始化变量赋值的方法将不起作用,但第二种可以。这是因为在第一种方法中,我们是在分配值,而第二种,我们是在初始化值。
或者您也可以在成员列表中编写://+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class name { private: public: int var1; bool var2; name() : var1(0), var2(true) {} ~name(); }; //+------------------------------------------------------------------+
现在我们已经涵盖如何声明变量、构造函数和析构函数,清楚如何直白地创建函数。如果要创建一个名为 functionName() 的函数,取一个输入作为字符串变量,并简单地打印该变量,则它看来像这样:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class className { public: void functionName(string printThis) }; //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void className::functionName(string printThis) { Print(printThis); } //+------------------------------------------------------------------+
我们在名为 “className” 的类成员列表中声明函数,并用 public 访问修饰符,如此我们就可以在任何地方调用该函数。然后,我们编写函数的主体。注意在类中该函数的声明,即
functionName(string printThis)
在编写函数主体时必须完全匹配。
我们编写 MQL5 类的基本概述至此完毕。
- 如何使用它们?
为了更好地理解这一点,我们来看看文件夹结构:
- 测试项目
- mainFile.mq5
- includeFile.mqh
首先,我们看看在 includeFile.mqh 中编写的完整类代码:
在此示例中,我们声明了一个名为 “className” 的类,其包含一个构造函数、一个析构函数、三个变量(一个是私密变量,两个是公开变量)、和一个公开函数。
- 构造函数:我们为变量 var0、var1 和 var2 分别初始化为 10、0 和 true。
- 析构函数:目前,它是空的,因此不执行任何操作。
- var0:这是一个私密整数型变量,初始化为 10,并在函数(functionName)中使用。
- var1:这是一个公开整数型变量,初始化为 0,也在函数(functionName)当中使用。
- functionName:这个名为 “functionName” 的 void 函数接收一个整数型 “printThisNumber”,并打印 printThisNumber、var0 及 var1 的合计。
接下来,我们来查验 mainFile.mq5:
#include "includeFile.mqh" className classInstance; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { classInstance.functionName(5)//This line will print (5+10+0) = 15 classInstance.var1 = 50;//This will change the value of var1 to 50 classInstance.functionName(5)//Now, this line will print (5+10+50) = 65 return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
我们首先将 includeFile.mqh 文件包含在 mainFile.mq5 之中。然后,我们如下创建类的实例
className classInstance;
这个实例令我们能够修改和使用类中的变量或函数。这个过程称为实例化,但我们不会在这里深入研究深层定义。
您可以创建任意数量的实例,它们都是相互独立的。我们使用 “” 替代 <> 来搜索 .mqh 文件,因为 <> 是在 'include' 文件夹中查找 .mqh 文件,而 “” 则是在当前目录中搜索 .mqh 文件。
本演练提供了有关类如何使用的扎实理解。 - 测试项目
使用 .mqh 文件创建相同的仪表板
我们开启旅程,从头开始创建类似的仪表板,但这次用的是 .mqh 文件。如有必要,我们将借用之前的代码片段。为了有效地组织我们的代码文件,我们将创建一个新文件夹,恰如其分地命名为 “Movable Dashboard MQL5”。
接下来,我们将生成两个新文件:“Movable_Dashboard_MQL5.mq5” 文件,它将用作我们的主要 .mq5 文件,以及 “GUI_Movable.mqh” 文件,它持有令仪表板可移动的代码。正确命名这些文件对于轻松管理多个文件至关重要。
首先,我们在主 .mq5 文件(Movable_Dashboard_MQL5.mq5)的 OnInit() 中利用对象创建方法制作一个 200x200 的白色仪表板:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //Set the name of the rectangle as "TestRectangle" string name = "TestRectangle"; //Create a Rectangle Label Object at (time1, price1)=(0,0) ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0); //Set XDistance to 100px i.e. Distance of Rectangle Label 100px from Left of the Chart Window ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 100); //Set YDistance to 100px i.e. Distance of Rectangle Label 100px from Top of the Chart Window ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 100); //Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(0, name, OBJPROP_XSIZE, 200); //Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(0, name, OBJPROP_YSIZE, 200); ChartRedraw(); //--- return(INIT_SUCCEEDED); }
结果:
图例 1. 基本仪表板图像
您也许会问,为什么我们要用主 .mq5 文件(Movable_Dashboard_MQL5.mq5)替代 .mqh 文件(GUI_Movable.mqh)创建仪表板?这个决定主要是为了简单起见,且取决于您的具体目标,我们将在下一章节中采取这种方式。
我们把注意力转向 .mqh 文件(GUI_Movable.mqh),它目前看上去像这样:
//+------------------------------------------------------------------+ //| Class GUI_Movable | //+------------------------------------------------------------------+ class GUI_Movable { }; //+------------------------------------------------------------------+
在此,我们只是声明了一个类,并未显式定义构造函数和析构函数。
那么,我们的标的物是什么?我们旨在缝合该代码,如此它就可在我们的主要文件中实现,从而令我们的仪表板可移动。
好吧,我们如何达成这一目标?在我们之前的 .mq5 文件(Movable_Dashboard_MQL5.mq5)中,唯一需要能够令仪表板可移动的代码是:
//Declare some global variable that will be used in the OnChartEvent() function int previousMouseState = 0; int mlbDownX = 0; int mlbDownY = 0; int mlbDownXDistance = 0; int mlbDownYDistance = 0; bool movingState = false; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { //Verify the event that triggered the OnChartEvent was CHARTEVENT_MOUSE_MOVE because we only want to execute out code when that is the case if(id == CHARTEVENT_MOUSE_MOVE) { //define X, Y, XDistance, YDistance, XSize, YSize int X = (int)lparam; int Y = (int)dparam; int MouseState = (int)sparam; string name = "TestRectangle"; int XDistance = (int)ObjectGetInteger(0, name, OBJPROP_XDISTANCE); //Should be 100 initially as we set it in OnInit() int YDistance = (int)ObjectGetInteger(0, name, OBJPROP_YDISTANCE); //Should be 100 initially as we set it in OnInit() int XSize = (int)ObjectGetInteger(0, name, OBJPROP_XSIZE); //Should be 200 initially as we set it in OnInit() int YSize = (int)ObjectGetInteger(0, name, OBJPROP_YSIZE); //Should be 200 initially as we set it in OnInit() if(previousMouseState == 0 && MouseState == 1) //Check if this was the MLB first click { mlbDownX = X; //Set mlbDownX (Variable that stores the initial MLB X location) equal to the current X mlbDownY = Y; //Set mlbDownY (Variable that stores the initial MLB Y location) equal to the current Y mlbDownXDistance = XDistance; //Set mlbDownXDistance (Variable that stores the initial XDistance i.e. Width of the dashboard) equal to the current XDistance mlbDownYDistance = YDistance; //Set mlbDownYDistance (Variable that stores the initial YDistance i.e. Height of the dashboard) equal to the current YDistance if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) //Check if the click was on the dashboard { movingState = true; //If yes the set movingState to True } } if(movingState)//if movingState is true, Update the Dashboard position { ChartSetInteger(0, CHART_MOUSE_SCROLL, false);//Restrict Chart to be moved by Mouse ObjectSetInteger(0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX);//Update XDistance to: mlbDownXDistance + (X - mlbDownX) ObjectSetInteger(0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY);//Update YDistance to: mlbDownYDistance + (Y - mlbDownY) ChartRedraw(0); //Redraw Chart } if(MouseState == 0)//Check if MLB is not pressed { movingState = false;//set movingState again to false ChartSetInteger(0, CHART_MOUSE_SCROLL, true);//allow the cahrt to be moved again } previousMouseState = MouseState;//update the previousMouseState at the end so that we can use it next time and copare it with new value } } //+------------------------------------------------------------------+
现在我们将按以下手段处置:
- 编写 GUI_Movable 类代码
- 在主要 .mq5 文件中创建类的实例
- 为该实例命名
- 调用 GUI_Movable 类方法令仪表板可移动
这些步骤初看可能令人生畏,但随着实践,这个过程会变得更直观。
- 编写 GUI_Movable 类代码:
我们需要计划好类的组成部分。此处是分解:
- 我们需要声明六个 private 修饰符的变量(previousMouseState、mlbDownX、mlbDownY、mlbDownXDistance、mlbDownYDistance、movingState)。这些变量包括五个整数型和一个布尔型。
- 我们必须声明的第七个 public 变量,其中存储仪表板的名称。我们需要这个变量公开,因为我们需要从主要 .mq5 文件中修改它。
- 我们需要找到一种途径来利用 .mqh 文件中的 OnChartEvent 函数,因为我们所有声明的变量都位于该文件之中,并且我们需要在 OnChartEvent 函数中使用这些变量。
-
我们首先在类中声明我们的六个私密变量,其中五个是整数型,一个是布尔型。我们将利用构造函数来初始化这些值:
//+------------------------------------------------------------------+ //| Class GUI_Movable | //+------------------------------------------------------------------+ class GUI_Movable { private: int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; bool movingState; public: GUI_Movable() : previousMouseState(0), mlbDownX(0), mlbDownY(0), mlbDownXDistance(0), mlbDownYDistance(0), movingState(false) {}; }; //+------------------------------------------------------------------+
现在我们需要 0 作为所有 int 的初始值,而所有 bool 则是 false,故我们利用构造函数来初始化它们。
-
接下来,我们将声明一个公开变量来存储仪表板的名称。这个变量需要从我们的主要 .mq5 文件中访问。
public: string Name;
它的初始值当然是 NULL,但为了形式化,我们会将其初始化为 NULL,并修改我们的构造函数来做这件事(形式化因为字符串不会导致不一致)
GUI_Movable() : previousMouseState(0), mlbDownX(0), mlbDownY(0), mlbDownXDistance(0), mlbDownYDistance(0), movingState(false), Name(NULL) {};
-
这一步也许看起来有点复杂,但一旦您理解了它,它就很直白了。
我们将创建一个名为 OnEvent 的公开函数,该函数将接收以下输入:id、lparam、dparam 和 sparam。由于 OnChartEvent() 不返回任何内容(void),我们也会把 OnEvent() 定义为 void。
OnEvent 函数将执行 OnChartEvent() 设计要执行的所有操作,但它将在 .mqh 文件中执行此操作。我们将在主文件的实际 OnChartEvent() 函数中调用 OnEvent()。
为了避免在 .mqh 和主文件中声明 OnChartEvent() 而导致错误,我们创建了这个名为 OnEvent() 的单独函数。此处就是我们如何声明它的:
public: string Name; void OnEvent(int id, long lparam, double dparam, string sparam);
现在,我们编写函数代码。它将执行原始 OnChartEvent() 设计的所有操作:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void GUI_Movable::OnEvent(int id, long lparam, double dparam, string sparam) { } //+------------------------------------------------------------------+
我们将该函数置于全局界域。现在,我们能够在此放置相同的代码,它将可以访问类中声明的变量。
完整的函数将如下所示:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void GUI_Movable::OnEvent(int id, long lparam, double dparam, string sparam) { //Verify the event that triggered the OnChartEvent was CHARTEVENT_MOUSE_MOVE because we only want to execute out code when that is the case if(id == CHARTEVENT_MOUSE_MOVE) { //define X, Y, XDistance, YDistance, XSize, YSize int X = (int)lparam; int Y = (int)dparam; int MouseState = (int)sparam; string name = Name; int XDistance = (int)ObjectGetInteger(0, name, OBJPROP_XDISTANCE); //Should be 100 initially as we set it in OnInit() int YDistance = (int)ObjectGetInteger(0, name, OBJPROP_YDISTANCE); //Should be 100 initially as we set it in OnInit() int XSize = (int)ObjectGetInteger(0, name, OBJPROP_XSIZE); //Should be 200 initially as we set it in OnInit() int YSize = (int)ObjectGetInteger(0, name, OBJPROP_YSIZE); //Should be 200 initially as we set it in OnInit() if(previousMouseState == 0 && MouseState == 1) //Check if this was the MLB first click { mlbDownX = X; //Set mlbDownX (Variable that stores the initial MLB X location) equal to the current X mlbDownY = Y; //Set mlbDownY (Variable that stores the initial MLB Y location) equal to the current Y mlbDownXDistance = XDistance; //Set mlbDownXDistance (Variable that stores the initial XDistance i.e. Width of the dashboard) equal to the current XDistance mlbDownYDistance = YDistance; //Set mlbDownYDistance (Variable that stores the initial YDistance i.e. Height of the dashboard) equal to the current YDistance if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) //Check if the click was on the dashboard { movingState = true; //If yes the set movingState to True } } if(movingState)//if movingState is true, Update the Dashboard position { ChartSetInteger(0, CHART_MOUSE_SCROLL, false);//Restrict Chart to be moved by Mouse ObjectSetInteger(0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX);//Update XDistance to: mlbDownXDistance + (X - mlbDownX) ObjectSetInteger(0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY);//Update YDistance to: mlbDownYDistance + (Y - mlbDownY) ChartRedraw(0); //Redraw Chart } if(MouseState == 0)//Check if MLB is not pressed { movingState = false;//set movingState again to false ChartSetInteger(0, CHART_MOUSE_SCROLL, true);//allow the cahrt to be moved again } previousMouseState = MouseState;//update the previousMouseState at the end so that we can use it next time and copare it with new value } } //+------------------------------------------------------------------+
我们唯一的修改是
string name = "TestRectangle";
至
string name = Name;
因为当然,我们需要使用我们在主要 .mq5 文件中设置的 Name 变量。
-
在主要 .mq5 文件中创建类的实例:
这可以通过以下方式非常简单地完成:
#include "GUI_Movable.mqh" GUI_Movable Dashboard;
在此,我们包含了 .mqh 文件,选择 “” 替代 <> 来指定文件的位置。<> 是在 include 文件夹中搜索 .mqh 文件,而 “” 则在当前目录中查找 .mqh 文件,在本例中为名为 “Movable Dashboard MQL5” 的文件夹。然后,我们声明 GUI_Movable 类的实例,并为其分配一个便捷的名称 “Dashboard”。这个名称允许我们利用我们在 .mqh 文件中编写的代码。
-
为该实例命名:
这可以在 OnInit() 函数中毫不费力地执行。我们的 OnInit() 函数应该是这样显示的:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //Set the name of the rectangle as "TestRectangle" string name = "TestRectangle"; //Create a Rectangle Label Object at (time1, price1)=(0,0) ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0); //Set XDistance to 100px i.e. Distance of Rectangle Label 100px from Left of the Chart Window ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 100); //Set YDistance to 100px i.e. Distance of Rectangle Label 100px from Top of the Chart Window ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 100); //Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(0, name, OBJPROP_XSIZE, 200); //Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(0, name, OBJPROP_YSIZE, 200); //Set CHART_EVENT_MOUSE_MOVE to true to detect mouse move event ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //Give dashboard's name to the class instance Dashboard.Name = name; //--- return(INIT_SUCCEEDED); }
最后,我们使用//Give dashboard's name to the class instance Dashboard.Name = name;
给 GUI_Movable 类的 Dashboard 实例 “Name” 变量赋值。稍后将在实例的 OnEvent() 函数中会用到。请务必记住将 CHART_EVENT_MOUSE_MOVE 属性设置为 true。这样启用检测鼠标事件。稍后我们将在构造函数中重复此步骤,但现在,我们坚持不把事情搞复杂
-
调用 GUI_Movable 类方法令仪表板可移动:
尽管有些复杂的命名,但这一步很直白。
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { Dashboard.OnEvent(id, lparam, dparam, sparam); }
在此阶段,我们将 OnEvent() 函数放在 OnChartEvent() 当中,以便利用 .mqh 文件中的 OnChartEvent() 函数。
最后,此处是我们的完整代码:
文件夹结构:
- Movable Dashboard MQL5
- Movable_Dashboard_MQL5.mq5
- GUI_Movable.mqh
- Movable_Dashboard_MQL5.mq5
#include "GUI_Movable.mqh" GUI_Movable Dashboard; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //Set the name of the rectangle as "TestRectangle" string name = "TestRectangle"; //Create a Rectangle Label Object at (time1, price1)=(0,0) ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0); //Set XDistance to 100px i.e. Distance of Rectangle Label 100px from Left of the Chart Window ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 100); //Set YDistance to 100px i.e. Distance of Rectangle Label 100px from Top of the Chart Window ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 100); //Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(0, name, OBJPROP_XSIZE, 200); //Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(0, name, OBJPROP_YSIZE, 200); //Set CHART_EVENT_MOUSE_MOVE to true to detect mouse move event ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //Give dashboard's name to the class instance Dashboard.Name = name; //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { Dashboard.OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+
- GUI_Movable.mqh
//+------------------------------------------------------------------+ //| Class GUI_Movable | //+------------------------------------------------------------------+ class GUI_Movable { private: int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; bool movingState; public: GUI_Movable() : previousMouseState(0), mlbDownX(0), mlbDownY(0), mlbDownXDistance(0), mlbDownYDistance(0), movingState(false), Name(NULL) {}; string Name; void OnEvent(int id, long lparam, double dparam, string sparam); }; //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void GUI_Movable::OnEvent(int id, long lparam, double dparam, string sparam) { //Verify the event that triggered the OnChartEvent was CHARTEVENT_MOUSE_MOVE because we only want to execute out code when that is the case if(id == CHARTEVENT_MOUSE_MOVE) { //define X, Y, XDistance, YDistance, XSize, YSize int X = (int)lparam; int Y = (int)dparam; int MouseState = (int)sparam; string name = Name; int XDistance = (int)ObjectGetInteger(0, name, OBJPROP_XDISTANCE); //Should be 100 initially as we set it in OnInit() int YDistance = (int)ObjectGetInteger(0, name, OBJPROP_YDISTANCE); //Should be 100 initially as we set it in OnInit() int XSize = (int)ObjectGetInteger(0, name, OBJPROP_XSIZE); //Should be 200 initially as we set it in OnInit() int YSize = (int)ObjectGetInteger(0, name, OBJPROP_YSIZE); //Should be 200 initially as we set it in OnInit() if(previousMouseState == 0 && MouseState == 1) //Check if this was the MLB first click { mlbDownX = X; //Set mlbDownX (Variable that stores the initial MLB X location) equal to the current X mlbDownY = Y; //Set mlbDownY (Variable that stores the initial MLB Y location) equal to the current Y mlbDownXDistance = XDistance; //Set mlbDownXDistance (Variable that stores the initial XDistance i.e. Width of the dashboard) equal to the current XDistance mlbDownYDistance = YDistance; //Set mlbDownYDistance (Variable that stores the initial YDistance i.e. Height of the dashboard) equal to the current YDistance if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) //Check if the click was on the dashboard { movingState = true; //If yes the set movingState to True } } if(movingState)//if movingState is true, Update the Dashboard position { ChartSetInteger(0, CHART_MOUSE_SCROLL, false);//Restrict Chart to be moved by Mouse ObjectSetInteger(0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX);//Update XDistance to: mlbDownXDistance + (X - mlbDownX) ObjectSetInteger(0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY);//Update YDistance to: mlbDownYDistance + (Y - mlbDownY) ChartRedraw(0); //Redraw Chart } if(MouseState == 0)//Check if MLB is not pressed { movingState = false;//set movingState again to false ChartSetInteger(0, CHART_MOUSE_SCROLL, true);//allow the cahrt to be moved again } previousMouseState = MouseState;//update the previousMouseState at the end so that we can use it next time and copare it with new value } } //+------------------------------------------------------------------+
首先,编译 .mqh 文件,然后是 .mq5 文件。这将创建一个可以加载到图表的 .ex5 文件。
通过这些步骤,我们用更高效的代码复现了第 1 部分中完成的工作。注意,第 1 部分和第 2 部分之间主要 .mq5 文件中使用的代码体量存在明显差异。最好的部分是,从这里它只会变得更好。
结果:
图例 2. 简单可移动仪表板
使用 .mqh 文件在同一图表上设置两个仪表板
现在,我们将在 .mqh 文件中执行此操作,替代主要 .mq5 文件中调用 ObjectCreate 创建仪表板。您会看到以后事情会变得多么简单。
我们深入研究一下我们将对 .mqh 文件所做的修改:
-
我们需要将字符串变量 “Name” 的修饰符从 public 更改为 private。“Name” 在我们的主要文件中不是必需的 — 我们只在 .mqh 文件中需要它。故此,我们将其设为私密。这可以按如下方式完成:
从:
//+------------------------------------------------------------------+ //| Class GUI_Movable | //+------------------------------------------------------------------+ class GUI_Movable { private: int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; bool movingState; public: GUI_Movable() : previousMouseState(0), mlbDownX(0), mlbDownY(0), mlbDownXDistance(0), mlbDownYDistance(0), movingState(false), Name(NULL) {}; string Name; void OnEvent(int id, long lparam, double dparam, string sparam); };
至:
//+------------------------------------------------------------------+ //| Class GUI_Movable | //+------------------------------------------------------------------+ class GUI_Movable { private: int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; bool movingState; string Name; public: GUI_Movable() : previousMouseState(0), mlbDownX(0), mlbDownY(0), mlbDownXDistance(0), mlbDownYDistance(0), movingState(false), Name(NULL) {}; void OnEvent(int id, long lparam, double dparam, string sparam); };
我们简单地更改了位置
string Name;
将变量修饰符从 public 更改为 private
-
接下来,我们添加一个名为 CreateDashboard() 的公开方法。此方法将接收以下输入:name(string)、xDis(int)、yDis(int)、xSize(int)、ySize(int)。
我们首先将其添加到类成员列表之中:
public: void OnEvent(int id, long lparam, double dparam, string sparam); void CreateDashboard(string name, int xDis, int yDis, int xSize, int ySize);
现在,我们在全局空间中定义这个函数,从我们的主文件中复制代码,如下所示:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void GUI_Movable::CreateDashboard(string name, int xDis, int yDis, int xSize, int ySize) { //Create a Rectangle Label Object at (time1, price1)=(0,0) ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0); //Set XDistance to 100px i.e. Distance of Rectangle Label 100px from Left of the Chart Window ObjectSetInteger(0, name, OBJPROP_XDISTANCE, xDis); //Set YDistance to 100px i.e. Distance of Rectangle Label 100px from Top of the Chart Window ObjectSetInteger(0, name, OBJPROP_YDISTANCE, yDis); //Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(0, name, OBJPROP_XSIZE, xSize); //Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(0, name, OBJPROP_YSIZE, ySize); //Set CHART_EVENT_MOUSE_MOVE to true to detect mouse move event ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //Give dashboard's name to the class instance Name = name; //Redraw Chart ChartRedraw(); } //+------------------------------------------------------------------+
据此,我们需要修改我们的 .mq5 文件:
#include "GUI_Movable.mqh" GUI_Movable Dashboard1; GUI_Movable Dashboard2; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { Dashboard1.CreateDashboard("Dashboard1", 100, 100, 200, 200); Dashboard2.CreateDashboard("Dashboard2", 100, 350, 200, 200); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { Dashboard1.OnEvent(id, lparam, dparam, sparam); Dashboard2.OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+
为您分解:
我们首先包含 “GUI_Movable.mqh” 文件,其中包含 GUI_Movable 类定义。该类拥有创建和处理与可移动仪表板相关的事件方法。
接下来,我们声明 GUI_Movable 类的两个实例,即 Dashboard1 和 Dashboard2。这些实例代表我们将在程序中创建和控制的两个仪表板。
在 OnInit() 函数中(EA 交易启动时自动调用),我们通过调用两个实例的 CreateDashboard() 方法来创建两个仪表板。我们将仪表板的名称及其位置和大小(以像素为单位)作为参数传递给该方法。然后,该函数返回 INIT_SUCCEEDED,指示初始化成功。
最后,我们有 OnChartEvent() 函数,每当图表上发生事件(如鼠标单击或移动)时,就会触发该函数。在该函数中,我们调用两个仪表板实例的 OnEvent() 方法,传递所有接收到的参数。这允许每个仪表板根据 GUI_Movable 类的 OnEvent() 方法中定义的逻辑独立处理事件。
如您所见,这种方式简单明了,同时维持了相同的功能。这令代码在成熟的 EA/指标中具有高度的可用性。
.mqh 文件的完整代码:
//+------------------------------------------------------------------+ //| Class GUI_Movable | //+------------------------------------------------------------------+ class GUI_Movable { private: int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; bool movingState; string Name; public: GUI_Movable() : previousMouseState(0), mlbDownX(0), mlbDownY(0), mlbDownXDistance(0), mlbDownYDistance(0), movingState(false), Name(NULL) {}; void OnEvent(int id, long lparam, double dparam, string sparam); void CreateDashboard(string name, int xDis, int yDis, int xSize, int ySize); }; //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void GUI_Movable::OnEvent(int id, long lparam, double dparam, string sparam) { //Verify the event that triggered the OnChartEvent was CHARTEVENT_MOUSE_MOVE because we only want to execute out code when that is the case if(id == CHARTEVENT_MOUSE_MOVE) { //define X, Y, XDistance, YDistance, XSize, YSize int X = (int)lparam; int Y = (int)dparam; int MouseState = (int)sparam; string name = Name; int XDistance = (int)ObjectGetInteger(0, name, OBJPROP_XDISTANCE); //Should be 100 initially as we set it in OnInit() int YDistance = (int)ObjectGetInteger(0, name, OBJPROP_YDISTANCE); //Should be 100 initially as we set it in OnInit() int XSize = (int)ObjectGetInteger(0, name, OBJPROP_XSIZE); //Should be 200 initially as we set it in OnInit() int YSize = (int)ObjectGetInteger(0, name, OBJPROP_YSIZE); //Should be 200 initially as we set it in OnInit() if(previousMouseState == 0 && MouseState == 1) //Check if this was the MLB first click { mlbDownX = X; //Set mlbDownX (Variable that stores the initial MLB X location) equal to the current X mlbDownY = Y; //Set mlbDownY (Variable that stores the initial MLB Y location) equal to the current Y mlbDownXDistance = XDistance; //Set mlbDownXDistance (Variable that stores the initial XDistance i.e. Width of the dashboard) equal to the current XDistance mlbDownYDistance = YDistance; //Set mlbDownYDistance (Variable that stores the initial YDistance i.e. Height of the dashboard) equal to the current YDistance if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) //Check if the click was on the dashboard { movingState = true; //If yes the set movingState to True } } if(movingState)//if movingState is true, Update the Dashboard position { ChartSetInteger(0, CHART_MOUSE_SCROLL, false);//Restrict Chart to be moved by Mouse ObjectSetInteger(0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX);//Update XDistance to: mlbDownXDistance + (X - mlbDownX) ObjectSetInteger(0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY);//Update YDistance to: mlbDownYDistance + (Y - mlbDownY) ChartRedraw(0); //Redraw Chart } if(MouseState == 0)//Check if MLB is not pressed { movingState = false;//set movingState again to false ChartSetInteger(0, CHART_MOUSE_SCROLL, true);//allow the cahrt to be moved again } previousMouseState = MouseState;//update the previousMouseState at the end so that we can use it next time and copare it with new value } } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void GUI_Movable::CreateDashboard(string name, int xDis, int yDis, int xSize, int ySize) { //Create a Rectangle Label Object at (time1, price1)=(0,0) ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0); //Set XDistance to 100px i.e. Distance of Rectangle Label 100px from Left of the Chart Window ObjectSetInteger(0, name, OBJPROP_XDISTANCE, xDis); //Set YDistance to 100px i.e. Distance of Rectangle Label 100px from Top of the Chart Window ObjectSetInteger(0, name, OBJPROP_YDISTANCE, yDis); //Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(0, name, OBJPROP_XSIZE, xSize); //Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(0, name, OBJPROP_YSIZE, ySize); //Set CHART_EVENT_MOUSE_MOVE to true to detect mouse move event ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //Give dashboard's name to the class instance Name = name; //Redraw Chart ChartRedraw(); } //+------------------------------------------------------------------+
结果:
图例 3. 同一图表上的两个可移动仪表板
结束语
对于那些想看到他们先期存在的仪表板可移动的人,这个过程相当直白。参考标题为 “使用 .mqh 文件创建相同的仪表板” 的章节,您会发现只需在现有的 EA/指标中只需几行代码即可将任何仪表板转换为可移动的仪表板。所有需要的只是包含 GUI_Movable.mqh 文件,并创建类实例,并将仪表板的名称分配给该实例。通过这些简单的步骤,您的仪表板就会变得具有交互性,并且可以用鼠标轻松移动。
随着第二部分的完成,我们已经成功地学会了如何令仪表板可移动来增强仪表板的交互性。这可以应用于任何先期存在的 EA/指标,或者从头开始构建的新 EA/指标。
虽然这是一篇很长的文章,而且类的概念在解释和理解方面都具有挑战性,但我相信这些知识将极大地有益于您未来的编码之旅。
我真诚地希望这篇文章对您有所帮助,即便是最小的方式。期待在系列文章的下一部分再次见到您。
祝您编码愉快,交易愉快!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/12880


