
利用 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
注意: 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.



你好、
您喜欢我的文章,我感到非常高兴!感谢您的反馈。
我认为您不需要修改我的代码就能实现您的目标,因为您的目标似乎与我的代码最初的设计目标截然不同。不过,您当然可以利用我的代码使您动态创建的仪表板可移动。
要检测图表上的鼠标点击,您可以使用 ChartEvent,特别是 CHARTEVENT_CLICK。该事件将提供鼠标点击的坐标,让您可以在该位置创建一个新的仪表盘。然后,您就可以应用我的代码来使新创建的仪表盘可移动。
如果您想了解有关 ChartEvent 的更多信息,我建议您参考本系列的第一部分,特别是题为"图表事件解码 " 的章节:可移动 GUI 的基石"一节。
此外,我强烈建议使用类,因为如果使用得当,它们会简化您的任务。如果您不熟悉类的概念,可以参考我的文章"理解类的概念",了解基本知识。
希望对您有所帮助!
哦,我明白您的困难所在了。我来帮您。
我写了一个名为 MultiDash 的智能交易系统 (EA),与您想要的一模一样,只需稍微修改一下代码。
我把它附在下面供您参考。如果代码中有任何不明白的地方,请随时咨询。我很乐意提供帮助。
哦,我知道你有什么困难了。我来帮你。
我已经编写了一个名为 MultiDash 的智能交易系统 (EA),只要稍微修改一下代码,就能完全满足您的要求。
我把它附在下面供您参考。如果代码中有任何不明白的地方,请随时咨询。我很乐意提供帮助。
帮助!
您的预览引起了我的兴趣,让我觉得我不需要制作一个类 Text。 相反,我计划使用您的 GUI 作为基类,由我的每个独特面板的子类继承。 GUI 类应包含函数 Move(....) 的定义,但不包含任何工作代码。此外,子类还将包含一个 Move 函数,该函数将从 GUI onEvent 函数中获取 x 坐标和 y 坐标,并包含将这些坐标分配给面板上每个特定对象的 x y 坐标的代码。
虽然我是一个优秀的程序员,但我并不是一个优秀的对象程序员,事实上,我还是一个新手。 我收到了 "clsGUI::CreatePanel - cannot access private member function"(clsGUI::CreatePanel - 无法访问私有成员函数)的提示,我想这意味着我需要一些其他限定符,以便直接在子类中使用它们来解决这个错误。 到目前为止,我的参考资料还没有找到解决方案。
附件中的包含文件和程序与您的代码相同,但包含了我为解决问题而做的许多修改。
警告使用此代码的其他人,其中包含许多错误,我对此不承担任何责任。
非常感谢您的帮助
科达角
帮助!
您的预览引起了我的兴趣,让我觉得我不需要制作一个类 Text。 相反,我计划使用您的 GUI 作为基类,由我的每个独特面板的子类继承。 GUI 类应包含函数 Move(....) 的定义,但不包含任何工作代码。此外,子类还将包含一个 Move 函数,该函数将从 GUI onEvent 函数中获取 x&y 坐标,并包含将这些坐标分配给面板上每个特定对象的 x y 坐标的代码。
虽然我是一个优秀的程序员,但我并不是一个优秀的对象程序员,事实上,我是一个新手。 我得到的结果是 "clsGUI::CreatePanel - 无法访问私有成员函数",我想这意味着我需要一些其他限定符,以便在子类中直接使用它们来解决这个错误。 到目前为止,我的参考资料还没有找到解决方案。
附件中的包含文件和程序与您的代码相同,但包含了我为解决问题所做的许多更改。
警告使用此代码的其他人,此代码包含大量错误,我对此不承担任何责任。
非常感谢您的帮助
科达角
在 .mqh 文件的第 103 行:
class clsSample : clsGUI
到
问题解决了。
概念:继承类型 ->
以下是每种继承类型的含义:
公共继承(类 Child :公共 父类):父类的公共成员和受保护成员分别成为子类的公共成员和受保护成员。从本质上讲,公共继承意味着 "is-a"。例如,"子类 "是 "父类 "的一种类型。
保护继承(类 Child : 受保护的父类):父类的公共成员和受保护成员都成为子类的受保护成员。这意味着可以从 Child 类及其子类访问它们,但不能从这些类之外访问。
私有继承(类 Child : 私有父类):父类的公共成员和受保护成员都成为子类的私有成员。这意味着这些成员只能在子类内部访问,而不能从子类或子类外部访问。
希望对您有所帮助!
注:使用图表重绘,否则会等待价格跳动。