English Русский Español Deutsch 日本語 Português
preview
高效处理指标的便捷方法

高效处理指标的便捷方法

MetaTrader 5示例 | 29 四月 2025, 09:24
460 0
Aleksandr Slavskii
Aleksandr Slavskii

指标早已成为任何交易平台不可或缺的一部分。 几乎所有交易者都使用它们。一个交易系统通常包含一整套指标,而不仅仅是一个指标,因此方便地设置指标是交易中的一个重要方面。

在本文中,我将介绍如何制作一个简单的面板,以便直接从图表中更改指标设置,以及需要对指标进行哪些更改以连接该面板。本文面向 MQL5 的新手用户,因此我会详细解释每一行代码。专业人士可能不会在这里发现什么新内容。


面板实现

这个网站的文章和代码库中已经有许多各种各样的面板,为什么我们不直接使用现成的代码呢?有很多优秀的代码可供选择,包括一些很棒的库,它们允许我们创建任何复杂度的面板。

然而,我并不满意“通用性”是以牺牲“易用性”为代价的。因此,我将开发一个专门用于指标的面板,本质上是一个普通的表格,其中单元格的宽度和高度会根据字体大小自动调整以适应文本的宽度和高度。

面板顶部将有一行用于拖动整个面板。这一行将包含指标的名称,以及用于固定和折叠面板的图标。每个表格单元格将通过一行代码进行描述。这样,如果需要,我们可以在不同行的列中设置不同的宽度。

面板(指标)名称 “固定” “折叠”

设置名称 1 输入
设置名称 2
输入

描述单元格的代码将包含:对象名称、对象类型、行号、单元格文本以及单元格宽度(以面板宽度的百分比表示)。面板宽度和单元格宽度将相互依赖。一行中所有单元格的百分比之和应等于 100%。

假设您需要在一行中指定三个对象,那么这三个对象的行号将相同,宽度例如为 30% + 30% + 40% = 100%。在大多数情况下,将一行分为两部分就足够了:50% 用于设置参数的名称,50% 用于输入字段。

正如我之前所说,面板代码的设计尽可能简单。因此,我计划不使用面向对象编程(OOP)。然而,完全放弃它是不可能的。从一个指标复制大量代码到另一个指标会很不方便。因此,我将面板代码格式化为一个类,并放在一个包含文件中。

我主要使用类而不是普通的独立函数,是因为在析构函数中删除面板对象很方便,否则我将不得不在指标的 OnDeinit() 中删除它们,这在那里面做起来会更困难。

还将有一个 Object.mqh 包含文件,其中包含绘制对象的方法,我也在其中实现了 getter 和 setter,以便更方便地访问函数。我不会描述 getter 和 setter 是什么。如果需要,您可以自行搜索。

面板的想法部分借鉴了以下文章:文章1文章2。 

本文中提到的所有代码文件都附在文章底部。我建议您先下载它们,将它们放入文件夹中,然后再开始研究代码。我在“include”文件夹中为 Object.mqh 文件创建了一个单独的 Object 文件夹。对于 Panel.mqh 文件,我在“include”文件夹中创建了一个单独的 Panel 文件夹。因此,在我的代码中,这些文件的路径是根据嵌套文件夹指定的。

让我们从包含 Object.mqh 文件和声明变量的 “input” 开始。我们需要声明变量,用于指定:面板的颜色、文本、按钮、边框以及当指标隐藏时面板将被涂成的额外颜色、字体大小、字体样式,以及面板与图表边缘的间距。

输入参数:

//+------------------------------------------------------------------+
#include <Object\\Object.mqh>
//+------------------------------------------------------------------+
input group "--- Input Panel ---"
input int    shiftX           = 3;               // Panel offset along the X axis
input int    shiftY           = 80;              // Panel offset along the Y axis
input bool   NoPanel          = false;           // No panel
input int    fontSize         = 9;               // Font size
input string fontType          = "Arial";        /* Font style*/ //"Arial", "Consolas"
input string PanelHiddenShown = "❐";             // Panel hidden/displayed
input string PanelPin         = "∇";             /* Pin the panel*/ // ⮂ ↕  ↔  ➽ 🖈 ∇
input string PanelUnpin       = "_";             // Unpin the panel
input color  clrTitleBar      = C'109,117,171';  // Panel title background color (1)
input color  clrTitleBar2     = clrGray;         // Panel title background color (2)
input color  clrDashboard     = clrDarkGray;     // Panel background color
input color  clrTextDashboard = clrWhite;        // Text color on the panel
input color  clrBorder        = clrDarkGray;     // Border color
input color  clrButton1       = C'143,143,171';  // Button background color (1)
input color  clrButton2       = C'213,155,156';  // Button background color (2)
input color  clrButton3       = clrGray;         // Button background color (3)
input color  clrTextButton1   = clrBlack;        // Button text color (1)
input color  clrTextButton2   = clrWhite;        // Button text color (2)
input color  clrEdit1         = C'240,240,245';  // Input field background color (1)
input color  clrEdit2         = clrGray;         // Input field background color (2)
input color  clrTextEdit1     = C'50,50,50';     // Input field text color (1)
input color  clrTextEdit2     = clrWhite;        // Input field text color (2)
//+------------------------------------------------------------------+

下面是CPanel类本身:

//+------------------------------------------------------------------+
class CPanel
  {
private:

   enum ENUM_FLAG   //flags
     {
      FLAG_PANEL_HIDDEN = 1,  // panel hidden
      FLAG_PANEL_SHOWN  = 2,  // panel displayed
      FLAG_IND_HIDDEN   = 4,  // indicator hidden
      FLAG_IND_SHOWN    = 8,  // indicator displayed
      FLAG_PANEL_FIX    = 16, // panel pinned
      FLAG_PANEL_UNPIN  = 32  // panel unpinned
     };

   int               sizeObject;
   int               widthPanel, heightPanel;
   int               widthLetter, row_height;
   int               _shiftX, _shiftY;
   long              mouseX, mouseY;
   long              chartWidth, chartHeight;
   string            previousMouseState;
   long              mlbDownX, mlbDownY, XDistance, YDistance;
   string            _PanelHiddenShown, _PanelPin, _PanelUnpin;

   struct Object
     {
      string         name;
      string         text;
      ENUM_OBJECT    object;
      int            line;
      int            percent;
      int            column;
      int            border;
      color          txtColr;
      color          backClr;
      color          borderClr;
     };
   Object            mObject[];

   int               prefixInd;
   string            Chart_ID;
   string            addedNames[];
   long              addedXDisDiffrence[], addedYDisDiffrence[];
   int               WidthHidthCalc(int line, string text = "", int percent = 50,  ENUM_OBJECT object = OBJ_RECTANGLE_LABEL);
   void              Add(string name); // save the object name and anchor point
   void              HideShow(bool hide = false);       // hide//show
   void              DestroyPanel();   // delete all objects

public:
                     CPanel(void);
                    ~CPanel(void);

   string            namePanel;    // panel name
   string            indName;      // indicator name should match indicator short name
   string            prefix;       // prefix for panel object names
   bool              hideObject;   // To be used as a flag in indicators where graphical objects need to be hidden
   int               sizeArr;
   double            saveBuffer[]; // array for storing the coordinates of the panel anchor point, panel properties (flag states), and the latest indicator settings

   enum ENUM_BUTON  // flags for allowing button creation
     {
      BUTON_1 = 1,
      BUTON_2 = 2
     };

   void              Init(string name, string indName);
   void              Resize(int size) {sizeArr = ArrayResize(saveBuffer, size + 3); ZeroMemory(saveBuffer);};
   void              Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0);
   bool              OnEvent(int id, long lparam, double dparam, string sparam);
   int               Save() {ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError();}
   bool              Load(string name) {return (FileLoad("pnl\\" + (string)ChartID() + name, saveBuffer) > 0);}

   void              Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1);
   void              ApplySaved();
   void              HideShowInd(bool hide);
  };
//+------------------------------------------------------------------+
CPanel::CPanel(void) {}
//+------------------------------------------------------------------+
CPanel::~CPanel(void) {DestroyPanel(); ChartRedraw();}
//+------------------------------------------------------------------+

我们将使用例子来讨论下面类的方法。

让我们写一个空的指标作为样例: 

#property indicator_chart_window
#property indicator_plots 0
input int _param = 10;
#include <Panel\\Panel.mqh>
CPanel mPanel;
int param = _param;
//+------------------------------------------------------------------+
int OnInit()
  {
   string short_name = "Ind Pnl(" + (string)param + ")";
   mPanel.Init("Ind Pnl", short_name);
   mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);
   mPanel.Record("param", OBJ_EDIT, 1, IntegerToString(param), 40);
   mPanel.Create(0);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   return(rates_total);
  }
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);
  }
//+------------------------------------------------------------------+

当启动该指标时,您会在图表上看到如下所示的面板: 

现在,让我们以这个指标为例,详细检查面板代码。

指标输入参数之后,包含面板类的文件,并声明面板类。

#property indicator_chart_window
#property indicator_plots 0
input int _param = 10;
#include <Panel\\Panel.mqh>
CPanel mPanel;
int param = _param;

通常,类是在代码的开头声明的。但由于面板类在包含输入参数的文件中,如果我们在输入代码的开头就写它,指标参数将会出现在面板输入的下方。这会在启动和配置指标时带来一些不便。

由于输入变量是常量,它们无法被修改。但可以创建输入变量的副本,并通过面板输入字段对其进行编辑。 

接下来,在指标的 OnInit() 函数中添加面板代码。

但首先,我想提醒您,为了面板能够正常工作,指标应在代码中指定一个简短的指标名称,该名称应包含主要输入参数。

string short_name = "Ind Pnl(" + (string)_param + ")";

这是为了能够以不同设置运行指标。

请记住,某些符号不能用于指标名称中。如果您想用冒号分隔参数,最好将其替换为分号。

面板名称可以与指标名称相同,但更方便的是让面板名称不考虑指标参数。

我们将添加到指标中的 CPanel 类的第一个方法是 Init() 方法,我们向其中传递两个名称:面板名称和指标名称。 

mPanel.Init("Ind Pnl", short_name);

Init() 方法首先要做的是确保面板在设置中未被禁用

void CPanel::Init(string name, string short_name)
  {
   if(NoPanel)
      return;

接着,初始化变量:

   namePanel = name;
   indName = short_name;
   MovePanel = true;
   sizeObject = 0;
   Chart_ID = (string)ChartID();
   int lastX = 0, lastY = 0;

让我们设置权限,以便将有关鼠标移动和按钮按下事件(CHARTEVENT_MOUSE_MOVE)的消息发送到图表上的所有 MQL5 程序,并且也允许发送有关创建图形对象事件(CHARTEVENT_OBJECT_CREATE)的消息:

   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);

为了计算面板宽度,我们首先需要设置字体类型和大小,以及获取单个字符的大小,以便后续使用该大小来计算文本与面板边框的间距。

这样我们就能得到与字体大小相关的可伸缩间距。单元格的高度将等于一个半字符的高度。

// set the font type and size
   TextSetFont(fontType, fontSize * -10);
// get the width and height of one character
   TextGetSize("0", widthLetter, row_height);
// calculate the cell height
   row_height += (int)(row_height / 2);

面板设置中包含用于显示/隐藏面板 ❐ 的图标,以及用于固定/取消固定面板的图标 ∇ 和 _。

我在互联网上找到了这些图标,它们可以在设置中进行更换。

让我们为图标添加空格,以便它们能够正确地从面板边缘定位。如果我们不添加空格,图标会显示得过于紧密,鼠标很难准确点击到它们。

   string space = " ";
   _PanelHiddenShown = space + PanelHiddenShown + space;
   _PanelPin         = space + PanelPin + space;
   _PanelUnpin       = space + PanelUnpin + space;

面板由图形对象组成。让我们为它们创建一个前缀,以便这些对象的名称是唯一的:

   MathSrand((int)GetMicrosecondCount());
   prefixInd = MathRand();
   prefix = (string)prefixInd;

请注意,如果图表中包含多个带有面板的指标,那么在创建前缀时,我们不能使用 GetTickCount() 函数,因为切换时间框架所需的时间非常短,如果使用毫秒而不是微秒,一些面板的前缀可能会重合。

在拖动面板时,OnChartEvent() 函数会确定鼠标在图表和对象上的位置。面板可能会与其他面板发生冲突。为了避免这种情况,我们将创建一个全局变量,并且当鼠标按下面板时,第一个将前缀写入该全局变量的面板将移动鼠标。先到先得。

在初始化时,我们将零写入该变量。只要变量中写入零,就认为它是空的。

   GlobalVariableTemp("CPanel");
   GlobalVariableSet("CPanel", 0);

当我们移动、折叠、固定面板或更改指标参数时,我们需要将这些更改保存在某个地方。这将使我们能够在切换时间框架或重新启动终端时,使用最新的设置加载面板和指标。虽然我没有将保存面板和指标最后设置的代码包含到指标中,但即使没有这段代码,指标也会记录对面板设置的更改。为此,我们需要为保存面板设置的数组分配内存。

   sizeArr = ArraySize(saveBuffer);
   if(sizeArr == 0)
      Resize(0);

尽管我们将指标数量设置为 = 0,Resize(0);传递给函数。但函数本身会添加三个单元格来保存面板设置。换句话说,我们使用 saveBuffer 数组的三个单元格来记住面板在图表上的位置、其状态(固定/未固定,折叠/展开)以及指标的状态(显示/隐藏)。

接下来是定义面板锚点初始坐标的代码。问题是面板锚点可以从输入设置中获取,也可以从保存的设置中获取(如果该图表上已经绘制了面板)。另一种选择是使用模板,其中设置了带有面板的指标。 

处理模板变得更加困难。如果我们保存了一个包含带有面板的指标的模板,那么我们无法保存在创建模板时面板的坐标。

但如果我们向图表添加一个指标,保存模板,然后应用它,我们会发现 OBJ_LABEL 文本标签对象被写入了模板。

保存模板:

 

应用模版:

 

正是这些文本标签,我们用它们来确定在创建模板时面板的位置。

   string delPrefix = "";
   int j = 0, total = ObjectsTotal(0, 0, OBJ_LABEL);
   for(int i = 0; i < total; i++)
     {
      string nameObject = ObjectName(0, i, 0, OBJ_LABEL);
      if(StringFind(nameObject, "TitleText " + indName) >= 0) // if the template contains objects with the name of this indicator
        {
         lastX = (int)GetXDistance(nameObject);// define the X coordinates of the panel in the template
         lastY = (int)GetYDistance(nameObject);// define the Y coordinates of the panel in the template
         StringReplace(nameObject, "TitleText " + indName, ""); // remember the object prefix for its subsequent deletion
         delPrefix = nameObject;
        }
     }

lastXlastY 变量设置了对象锚点的坐标——一个带有前缀的文本标签,其名称包含指标名称(面板名称文本坐标)。

请记住,面板名称可能与指标名称不同。在找到必要的文本后,检索前缀并将其保存。

以下代码使用之前保存的前缀来从图表中删除模板中保存的过时文本标签。

   if(delPrefix != "")// delete obsolete objects saved in the template
      ObjectsDeleteAll(0, delPrefix);

接下来是检查和选择面板锚点所需的选项。

   if(lastX != 0 || lastY != 0)// if we use a template
     {
      lastX = lastX - widthLetter / 2;
      lastY = lastY - (int)(row_height / 8);
      saveBuffer[sizeArr - 1] = _shiftX = lastX;
      saveBuffer[sizeArr - 2] = _shiftY = lastY;
     }
   else// if data from the file is used
      if(saveBuffer[sizeArr - 1] != 0 || saveBuffer[sizeArr - 2] != 0)
        {
         _shiftX = (int)saveBuffer[sizeArr - 1];
         _shiftY = (int)saveBuffer[sizeArr - 2];
        }
      else// if this is the first launch of the indicator
        {
         saveBuffer[sizeArr - 1] = _shiftX = shiftX;
         saveBuffer[sizeArr - 2] = _shiftY = shiftY;
        }

在 Init() 方法的最后,我们将不需要编辑的面板对象发送到结构体数组中。这些对象对所有面板都将保持一致。

两个矩形、带有面板名称的文本以及用于显示/隐藏固定/取消固定面板的图标。

   Record("TitleBar");
   Record("MainDashboardBody");
   Record("TitleText " + indName, OBJ_LABEL, 0, namePanel, 100);
   Record("PinUnpin", OBJ_LABEL, 0, _PanelPin, 0);
   Record("CollapseExpand", OBJ_LABEL, 0, _PanelHiddenShown, 0);

现在我们来继续下一个方法 Record()。

在 Record() 方法中填充了未来对象的结构。通常,大部分结构是用默认值填充的。然而,传递给此函数的参数组合允许我们稍微修改默认值,例如,为对象设置不同的颜色。 

//+------------------------------------------------------------------+
void CPanel::Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0)
  {
   if(NoPanel)
      return;
   int column = WidthHidthCalc(line + 1, text, percent, object);

   ArrayResize(mObject, sizeObject + 1);
   mObject[sizeObject].column = column;       // column
   mObject[sizeObject].name = prefix + name;  // object name
   mObject[sizeObject].object = object;       // object type
   mObject[sizeObject].line = line + 1;       // line index
   mObject[sizeObject].text = text;           // text (if any)
   mObject[sizeObject].percent = percent;     // percentage of panel width
   mObject[sizeObject].txtColr = txtColr;     // text color
   mObject[sizeObject].backClr = backClr;     // base color
   mObject[sizeObject].borderClr = borderClr; // border color
   mObject[sizeObject].border = 0;            // offset from the panel edge
   sizeObject++;
  }
//+------------------------------------------------------------------+

在 Record() 方法的开头,我们调用 WidthHidthCalc() 方法,在该方法中计算面板的宽度和高度。

让我们更仔细地看看 WidthHidthCalc() 方法。

此方法计算面板的宽度时会考虑最宽的元素,例如,如果我们为上述描述的 Ind Pnl 指标分配了一个更长的名称。

前一个:

mPanel.Init("Ind Pnl", short_name);

当前:

mPanel.Init("Ind Pnl 0000000000000000000", short_name);

结果:

或者例如,如果我们更改了指标设置的名称,就会得到如下结果。

前一个:

mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);

当前:

mPanel.Record("paramText 0000000000000000000", OBJ_LABEL, 1, "param", 60);

结果:

面板会根据文本大小自动调整。面板的宽度和高度的计算都是在 WidthHidthCalc() 函数中完成的。

首先,我们获取单元格文本的宽度

在这里,我们采用了一种与计算其他单元格文本宽度稍有不同的方法,用于计算带有面板名称和显示/隐藏图标时的文本宽度。

int CPanel::WidthHidthCalc(int line, string text = "", int percent = 50,  ENUM_OBJECT object = OBJ_RECTANGLE_LABEL)
  {
   static int lastLine = -1, column = 0;
   int width, height;
   if(line == 1)
      TextGetSize(text + _PanelPin + _PanelHiddenShown, width, height); // get the width and height of the text for the line with the panel name
   else
      TextGetSize(text, width, height); // get the text width and height

文本应该与单元格边框保持一定的间距,我们将这个间距设置为半个字符的宽度。我们在 Init() 函数中已经找到了一个字符的宽度,并将其赋值给了 widthLetter 变量。

为了在文本两侧提供间距,我们需要在最终的文本宽度上再加上一个字符的宽度,而对于按钮对象的文本(OBJ_BUTTON),我们需要再加上一个字符,以确保文本与按钮边缘之间有足够的间距。

现在我们已经知道了包括间距在内的整个单元格行的大小,我们可以根据单元格下方指定的百分比来计算面板的大小

保存面板的最大宽度值。在未来,所有单元格都将基于这个最大的面板宽度值进行计算。

   double indent = 0;
   if(object == OBJ_BUTTON)
      indent += widthLetter;

   if(text != "" && percent != 0)
     {
      // calculate the width of the panel based on the text size and the percentage allocated for this text 
      int tempWidth = (int)MathCeil((width + widthLetter + indent) * 100 / percent);
      if(widthPanel < tempWidth)
         widthPanel = tempWidth;
     }

在测试指标中,面板宽度的计算将如下所示。

首先,计算标题的宽度,同时考虑图标。对于“Ind Pnl” + “∇” + “❐”,宽度为 71 像素加上一个字符的宽度 7 像素,总共 78 像素——这是面板宽度的 100%。

单元格文本是“param”,宽度为 36 像素,加上额外的间距 7 像素后,总宽度为 43 像素。该单元格分配了面板宽度的 60%,因此面板宽度将等于 43 × 100 / 60 = 72 像素。这小于面板标题所需的宽度,因此面板宽度将等于标题单元格的宽度。

接下来,我们定义索引,并/或在这是新行的情况下增加 面板高度

   if(lastLine != line)// if this is a new row in the panel, then increase the height of the entire panel
     {
      heightPanel = row_height * line;
      lastLine = line;
      column = 0; // reset the number of columns in the new row
     }
   else
      column++; // add a new column

   return column;
  }

因此,我们已经详细研究了 CPanel 类的十个方法中的两个。

程序确定了面板的未来尺寸并将对象参数设置到结构体数组 mObject[] 之后,我们进入下一个方法——Create()。该方法根据之前获得的尺寸构建面板。

像往常一样,方法的开头会检查是否需要面板。接下来是编写两个预定义按钮的代码。一个按钮用于隐藏指标,另一个按钮用于移除指标。根据所选的标志,可以选择以下选项:0——没有按钮;1——一个用于隐藏/显示指标的按钮;2——一个用于移除指标的按钮;3——创建两个按钮。

为什么这些按钮不在指标代码中,而在这里呢?这样可以减少插入到指标代码中的代码量。

接下来是变量的初始化。如果想要不完全按照面板的预期用途,而是例如将其用作更改对象参数的弹出面板,那么这段代码 是必需的。因此,我们需要面板出现在您在图表上点击鼠标的位置。

void CPanel::Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1)
  {
   if(NoPanel)
      return;

   if((Button & BUTON_1) == BUTON_1)// if we need to create buttons
      Record("hideButton", OBJ_BUTTON, mObject[sizeObject - 1].line, "Ind Hide", 50);
   if((Button & BUTON_2) == BUTON_2)// if we need to create buttons
      Record("delButton", OBJ_BUTTON, mObject[sizeObject - 2].line, "Ind Del", 50, clrTextButton1, clrButton2);

   ENUM_ANCHOR_POINT ap = ANCHOR_LEFT_UPPER;
   int X = 0, Y = 0, xSize = 0, ySize = 0;

   if(shiftx != -1 && shifty != -1)
     {
      _shiftX = shiftx;
      _shiftY = shifty;
     }

面板既可以用于信息面板,也可以用于交易面板,还可以用于像这样的用于设置图表对象的面板。 一切都很简单,重点在于纯粹的功能性:

但我说得有点跑题了。让我们继续分析 Create() 方法。接下来是创建两个矩形的代码——一个标题矩形和一个面板主体矩形:

// header rectangle
   RectLabelCreate(0, mObject[0].name, 0, _shiftX, _shiftY, widthPanel, row_height, (mObject[0].backClr == 0 ? clrTitleBar : mObject[0].backClr),
                   BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[0].borderClr == 0 ? clrBorder2 : mObject[0].borderClr), STYLE_SOLID, 1, false, false, true, 1, indName);
   Add(mObject[0].name);// remember the object's anchor point

// panel rectangle
   RectLabelCreate(0, mObject[1].name, 0, _shiftX, row_height - 1 + _shiftY, widthPanel, heightPanel - row_height, (mObject[1].backClr == 0 ? clrDashboard : mObject[1].backClr),
                   BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[1].borderClr == 0 ? clrBorder1 : mObject[1].borderClr), STYLE_SOLID, 1, false, false, true, 0, indName);
   Add(mObject[1].name);

在每个对象创建之后,会调用Add() 函数,该函数将对象的名称以及其相对于图表左上角的坐标写入数组。

//+------------------------------------------------------------------+
void CPanel::Add(string name)// save the object name and anchor point
  {
   int size = ArraySize(addedNames);
   ArrayResize(addedNames, size  + 1);
   ArrayResize(addedXDisDiffrence, size + 1);
   ArrayResize(addedYDisDiffrence, size + 1);

   addedNames[size] =  name;
   addedXDisDiffrence[size] = GetXDistance(addedNames[0]) - GetXDistance(name);
   addedYDisDiffrence[size] = GetYDistance(addedNames[0]) - GetYDistance(name);
  }
//+------------------------------------------------------------------+

这些包含坐标的数组稍后将在移动面板时使用。

让我们回到 Create() 方法的代码。随后,所有对象都按照它们被写入 mObject[] 结构体数组的顺序在循环中创建。坐标尺寸的计算之后是创建对象

由于面板高度专业化,它仅使用三种类型的对象,而这对于其功能来说已经足够。

在用文本填充标题矩形时,我不得不诉诸于特殊情况,并实现了面板固定和折叠图标的锚点,使其与其他所有面板对象的锚点不同。这使得在面板上定位这些图标变得更加容易,因为它们的锚点位于右上角。

 for(int i = 2; i < sizeObject; i++)
     {
      // calculate the coordinates of the object anchor point
      if(mObject[i].column != 0)
        {
         X = mObject[i - 1].border + widthLetter / 2;
         mObject[i].border = mObject[i - 1].border + (int)MathCeil(widthPanel * mObject[i].percent / 100);
        }
      else
        {
         X = _shiftX + widthLetter / 2;
         mObject[i].border = _shiftX + (int)MathCeil(widthPanel * mObject[i].percent / 100);
        }

      Y = row_height * (mObject[i].line - 1) + _shiftY + (int)(row_height / 8);
      //---
      switch(mObject[i].object)
        {
         case  OBJ_LABEL:
            ap = ANCHOR_LEFT_UPPER;
            // unlike all other objects, the "pin" and "collapse" objects' anchor points are implemented in the upper right corner.
            if(i == 3)
              {
               int w, h;
               TextGetSize(_PanelHiddenShown, w, h);
               X = _shiftX + widthPanel - w;
               ap = ANCHOR_RIGHT_UPPER;
              }
            if(i == 4)
              {
               X = _shiftX + widthPanel;
               ap = ANCHOR_RIGHT_UPPER;
              }

            LabelCreate(0, mObject[i].name, 0, X, Y, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize,
                        (mObject[i].txtColr == 0 ? clrTextDashboard : mObject[i].txtColr), 0, ap, false, false, true, 1);
            break;

         case  OBJ_EDIT:
            xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter;
            ySize = row_height - (int)(row_height / 4);

            EditCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, mObject[i].text, fontType, fontSize, ALIGN_LEFT, false, CORNER_LEFT_UPPER,
                       (mObject[i].txtColr == 0 ? clrTextEdit1 : mObject[i].txtColr),
                       (mObject[i].backClr == 0 ? clrEdit1 : mObject[i].backClr),
                       (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, true, 1);
            break;

         case  OBJ_BUTTON:
            xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter;
            ySize = row_height - (int)(row_height / 4);

            ButtonCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize,
                         (mObject[i].txtColr == 0 ? clrTextButton1 : mObject[i].txtColr),
                         (mObject[i].backClr == 0 ? clrButton1 : mObject[i].backClr),
                         (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, false, true, 1);
            break;
        }
      Add(mObject[i].name);
     }

在构建完所有面板对象之后,我们移除 mObject[] 结构体数组,因为它不再需要了。 

   ArrayFree(mObject);

   ApplySaved();

   ChartRedraw();

如果某段代码被多次使用,我通常会为其创建一个单独的函数。然而,如果操作的实质是按意义分组的,我会将其单独作为一个方法提取出来。这就是我对 ApplySaved() 函数所做的。它会检查是否已经保存了面板数据,如果有,则应用这些数据;如果没有,则保存新的数据。

ApplySaved()

如果这是该指标在当前图表上的首次运行,saveBuffer[] 数组将用初始设置填充

如果saveBuffer[]数组已经包含保存的数据,我们将应用这些数据,而不是初始设置。

//+------------------------------------------------------------------+
void CPanel::ApplySaved()
  {
// collapse the panel immediately after the indicator is launched, if this is saved in the file
   if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN)
      CPanel::OnEvent(CHARTEVENT_OBJECT_CLICK, 0, 0, addedNames[4]);
   else
      saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_SHOWN;

// hide the indicator immediately after the indicator is launched, if this is saved in the file
   if(((uint)saveBuffer[sizeArr - 3] & FLAG_IND_HIDDEN) == FLAG_IND_HIDDEN)
     {
      HideShowInd(true);
      SetButtonState(prefix + "hideButton", true);
      hideObject = true;
     }
   else
     {
      saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_IND_SHOWN;
      hideObject = false;
     }

// pin the panel immediately after the indicator is launched, if this is saved in the file
   if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_FIX) == FLAG_PANEL_FIX)
      SetText(addedNames[3], _PanelUnpin);
   else
      saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_UNPIN;

   int Err = Save();
   if(Err != 0)
      Print("!!! Save Error = ", Err, "; Chart_ID + indName =", Chart_ID + indName);
  }
//+------------------------------------------------------------------+

正如您可能注意到的,ApplySaved() 函数也使用了 Save()HideShowInd()OnEvent() 函数。如果您正在阅读这些内容,请在评论中写下“noticed”(注意到了)。我很想知道是否有人会阅读这些描述。

让我们继续描述这些函数。在 Save() 中保存已获取的设置。为保存的面板设置分配一个单独的 pnl 文件夹,以免弄乱“Files”文件夹。

Save() 函数如下所示:

int Save()
  {
   ResetLastError();
   FileSave("pnl\\" + Chart_ID + indName, saveBuffer);
   return GetLastError();
  }

HideShowInd()

此函数的目的是简单地更改面板标题的颜色按钮的颜色和文本从 saveBuffer 数组中移除之前的标志,并设置一个新的标志。

hideObject 变量仅在使用对象(箭头、图标、文本等)进行绘图的指标中需要。在指标中创建新对象时,我们将检查此变量的状态,并根据状态决定是立即将新创建的对象隐藏,还是什么都不做,让对象显示出来。 

//+------------------------------------------------------------------+
void CPanel::HideShowInd(bool hide)
  {
// change the color and text of the buttons depending on the state of the panel, hidden/displayed, as well as the header color
   if(hide)
     {
      SetColorBack(prefix + "TitleBar", clrTitleBar2);
      SetColorBack(prefix + "hideButton", clrButton3);
      SetText(prefix + "hideButton", "Ind Show");
      saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_SHOWN) | FLAG_IND_HIDDEN;
      hideObject = true;
     }
   else
     {
      SetColorBack(prefix + "TitleBar", clrTitleBar);
      SetColorBack(prefix + "hideButton", clrButton1);
      SetText(prefix + "hideButton", "Ind Hide");
      saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN;
      hideObject = false;
     }
   Save();
   ChartRedraw();
  }
//+------------------------------------------------------------------+

该函数仅在点击“显示/隐藏指标”按钮时使用。

隐藏其中一个 RSI 指标的示例。 

代码中还包含了一个 HideShow() 函数,负责隐藏和显示对象。该函数用于折叠面板,并将面板对象置于前景。

该函数接受一个参数,指示面板是否折叠(true/false)。如果面板折叠,那么我们只需要将它的四个对象置于前景:标题栏矩形、标题本身以及两个图标——固定和折叠。

如果标志为 true(面板折叠),我们将依次隐藏并显示五个对象。为什么是五个而不是四个?在必要的对象中,多了一个——面板本身的矩形。我们在名称和图标之前创建了这个矩形,因此需要单独隐藏这个矩形。

如果标志为 false,则所有面板对象依次被隐藏并显示。这样,它们就被置于最前面。

//+------------------------------------------------------------------+
void CPanel::HideShow(bool hide = false) // hide and immediately display objects to bring to the foreground
  {
   int size = hide ? 5 : ArraySize(addedNames);
   for(int i = 0; i < size; i++)
     {
      SetHide(addedNames[i]);
      SetShow(addedNames[i]);
     }
   if(hide)
      SetHide(addedNames[1]);
  }
//+------------------------------------------------------------------+

折叠后的面板如下图所示,与常规面板并排:

接下来我们将查看 OnEvent() 函数。 

在函数的开头,我们检查面板是否在设置中被允许:

bool CPanel::OnEvent(int id, long lparam, double dparam, string sparam)
  {
   if(NoPanel)
      return false;

接下来,让我们看看负责移动面板的代码。我会尽可能详细地解释代码的工作原理。 

“鼠标移动”事件 发生且 允许面板移动的标志 已在内存中设置时,我们 保存鼠标坐标

如果是左键点击,则 sparam 为 1,如果鼠标按钮之前未被点击,则读取全局变量 的值。

这个全局变量是终端中所有运行的面板共有的。在移动面板时,我们会检查是否有另一个面板的前缀值被设置到这个变量中,如果没有设置,则将该面板的前缀设置到这个全局变量中。这意味着其他面板(如果它们位于我们正在移动的面板的下方/上方)将无法再被移动。

如果全局变量包含零或此面板的前缀,则我们读取面板的当前锚点坐标

然后检查这是否是鼠标在 带有面板标题的矩形上的点击。如果是,我们获取图表大小(以像素为单位),禁用图表滚动,同时全局变量设置面板前缀。前缀的设置既是允许面板移动的标志,也是确保只有这个面板会移动的保证。

最后,隐藏/显示面板,将其置于前景,同时将(true/false参数传递给确定面板当前是否隐藏或完全显示的函数。 

   if(id == CHARTEVENT_MOUSE_MOVE && ((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN)
     {
      mouseX = (long)lparam;
      mouseY = (long)dparam;

      if(previousMouseState != "1" && sparam == "1")
        {
         int gvg = (int)GlobalVariableGet("Panel");
         if(gvg == prefixInd || gvg == 0)
           {
            XDistance = GetXDistance(addedNames[0]);
            YDistance = GetYDistance(addedNames[0]);

            mlbDownX = mouseX;
            mlbDownY = mouseY;

            if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height)
              {
               chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
               chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
               GlobalVariableSet("Panel", prefixInd);
               HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN); // hide/display the panel so that it is in the foreground
              }
           }
        }

当用户点击带有面板名称的矩形并开始移动鼠标时,以下代码将运行。

首先,我们检查面板是否允许被移动。如果全局变量包含面板的前缀,则面板可以被移动。然后我们找到面板锚点的坐标。如果根据鼠标坐标移动面板,我们会检查面板的新位置。如果面板可能会超出图表范围,那么我们会稍微更改锚点坐标的值,以防止面板超出图表。

循环中,移动所有面板对象。

将面板锚点的新坐标设置到数组中,以便后续写入文件。之后,我们重新绘制图表。

      if((int)GlobalVariableGet("Panel") == prefixInd)
        {
         // disable the ability to go beyond the chart for the panel
         long posX = XDistance + mouseX - mlbDownX;
         if(posX < 0)
            posX = 0;
         else
            if(posX + widthPanel > chartWidth)
               posX = chartWidth - widthPanel;

         long posY = YDistance + mouseY - mlbDownY;
         if(posY < 0)
            posY = 0;
         else
            if(posY + row_height > chartHeight)
               posY = chartHeight - row_height;

         // move the panel
         int size = ArraySize(addedNames);
         for(int i = 0; i < size; i++)
           {
            SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]);
            SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]);
           }
         saveBuffer[sizeArr - 1] = (double)(posX);
         saveBuffer[sizeArr - 2] = (double)(posY);
         ChartRedraw(0);
        }

在移动面板的最后一步中,当鼠标按钮被释放时,sparam 不再等于 1

我们恢复图表的滚动功能重置全局变量,并将面板锚点的新坐标写入文件

      if(sparam != "1" && (int)GlobalVariableGet("Panel") == prefixInd)
        {
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
         GlobalVariableSet("Panel", 0);
         Save();
        }

      previousMouseState = sparam;
     }

我们已经详细研究了拖动面板的机制,现在我们将研究点击固定/取消固定面板或折叠/展开面板的图标时的动作。

所有这些操作都是在同一个 OnEvent() 函数中完成的。

鼠标按钮点击一个图形对象时,sparam 变量包含被点击对象的名称。如果它与 ❐ 图标的名称匹配,则检查下一个对象,如果它是可见的,我们会隐藏面板对象。如果它是不可见的,则显示所有面板对象更改面板可见性标志并将其写入数组,以便后续保存到文件中。

点击对象时,之前负责移动面板的代码有时会被触发,由于 ❐ 图标位于拖动面板的区域后面,因此图表滚动功能经常被禁用。因此,让我们启用滚动。出于同样的原因,我们还需要重置一个全局变量

将更改保存到文件中,以便在图表上显示更改并重新绘制它

   else
      if(id == CHARTEVENT_OBJECT_CLICK)
        {
         if(sparam == addedNames[4]) // prefix+"CollapseExpand"
           {
            if(GetShow(addedNames[5]) == OBJ_ALL_PERIODS)// if the panel is visible, hide it
              {
               SetHide(addedNames[1]);
               for(int i = 5; i < sizeObject; i++)
                  SetHide(addedNames[i]);

               saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN;
              }
            else// if the panel is hidden, display it
              {
               for(int i = 0; i < sizeObject; i++)
                  SetShow(addedNames[i]);

               saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN;
              }

            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            GlobalVariableSet("Panel", 0);
            Save();
            ChartRedraw(0);
           }

以下代码与上述描述的代码类似,唯一的区别是 ∇ 对象的名称。

         else
            if(sparam == addedNames[3]) // prefix+"PinUnpin"
              {
               if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN)
                 {
                  saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_UNPIN) | FLAG_PANEL_FIX;
                  SetText(addedNames[3], _PanelUnpin);
                 }
               else
                 {
                  saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_FIX) | FLAG_PANEL_UNPIN;
                  SetText(addedNames[3], _PanelPin);
                 }

               ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
               GlobalVariableSet("Panel", 0);
               Save();
               ChartRedraw(0);
              }

代码最后是指标删除按钮:

            else
               if(sparam == prefix + "delButton") // handle the indicator deletion button
                  ChartIndicatorDelete(0, ChartWindowFind(), indName);
        }

我们需要处理的最后一个事件是创建图形对象。 

新创建的图形对象通常会被置于前景,可能会覆盖面板,因此我们首先记住对象的选中状态,然后 隐藏/显示 面板以将其置于前景,最后恢复对象的选中状态。为什么要这么麻烦呢?通过编程隐藏或显示面板对象会导致新创建的对象失去选中状态。

这种现象在 这里 有更详细的描述。

      else
         if(id == CHARTEVENT_OBJECT_CREATE)//https://www.mql5.com/ru/articles/13179   "Making a dashboard to display data in indicators and EAs"
           {
            bool select = GetSelect(sparam);
            HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN);// hide/display the panel so that it is in the foreground
            SetSelect(sparam, select);// restore the state of the extreme object
           }

   return true;
  }

面板代码的讲解到此结束。


需要对指标代码进行哪些修改

不同的指标需要不同的更改。无法创建一个通用代码,将其插入到指标中就能得到一个现成的控制面板。每个指标都需要单独处理。

这里我会给出一些示例,帮助您以类似的方式更改其他指标。

为了保持一致性,我在修改后的指标名称中添加了“Pnl”这个词。


自定义移动平均线指标

要通过面板控制指标,我们应该能够更改输入变量。然而,输入变量是常量,无法修改。

为了解决这个问题,我们可以将输入变量复制到可以修改的普通变量中。为了尽量减少对指标代码的更改,我们用与当前输入变量相同的名称声明新变量,只是在它们后面加上一个下划线。

前一个:

ma 输入参数

之后:

在 input 参数 之后,包含 Panel.mqh 文件并声明 CPanel 类的实例 mPanel;

如果在输入参数之前设置了 #include 指令,那么在包含文件中设置的所有输入参数都将位于指标输入参数的上方,这会在启动指标时带来不便。

如果我们一切操作正确,我们应该会看到如下图所示的画面:

如果我们不需要面板设置,我们可以在 Panel.mqh 包含文件中简单地删除所有“input”字样,并使用默认设置。

在 OnInit() 函数中添加以下代码。

接下来,检查指标中是否启用了面板,如果这不是该指标在当前图表上的首次启动,则 加载 之前保存的面板设置。如果是首次启动,则根据输入参数的数量(共有 三个)调整数组的大小,并将这些输入参数的值写入数组。 

   if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         InpMAPeriod = (int)mPanel.saveBuffer[0];
         InpMAShift  = (int)mPanel.saveBuffer[1];
         InpMAMethod = (int)mPanel.saveBuffer[2];
        }
      else
        {
         mPanel.Resize(3);
         mPanel.saveBuffer[0] = InpMAPeriod;
         mPanel.saveBuffer[1] = InpMAShift;
         mPanel.saveBuffer[2] = InpMAMethod;
        }

面板名称, 指标名称

然后所有内容都以相同的方式填写:对象名称对象类型面板行索引对象本身面板宽度的百分比。 

      mPanel.Init("Moving Average", short_name);
      mPanel.Record("MAPeriodText", OBJ_LABEL, 1, "MAPeriod:", 50);
      mPanel.Record("MAPeriod", OBJ_EDIT, 1, IntegerToString(InpMAPeriod), 50);
      mPanel.Record("MAShiftText", OBJ_LABEL, 2, "MAShift:", 50);
      mPanel.Record("MAShift", OBJ_EDIT, 2, IntegerToString(InpMAShift), 50);
      mPanel.Record("MAMethodText", OBJ_LABEL, 3, "MAMethod:", 50);
      mPanel.Record("MAMethod", OBJ_EDIT, 3, IntegerToString(InpMAMethod), 50);
      mPanel.Create();
     }

如果我们现在运行指标,我们将得到一个如下图所示的面板:

所以,我们已经有一个面板了,剩下的就是编写与用户交互的代码。

在指标中再添加一个函数——OnChartEvent()

负责移动面板的方法已经在上面描述过了。当发生“完成编辑图形对象中的文本”事件时,检查完成编辑的 Edit 对象的前缀。如果该前缀与我们面板的前缀匹配,那么我们检查哪个指标参数被更改了,将更改后的值设置到变量数组中,以便后续保存。

接下来,我们将所有更改保存到文件中,并重新启动指标

void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= 0)
        {
         if(sparam == mPanel.prefix + "MAPeriod")
           {
            mPanel.saveBuffer[0] = InpMAPeriod = (int)StringToInteger(GetText(sparam));
           }
         else
            if(sparam == mPanel.prefix + "MAShift")
              {
               mPanel.saveBuffer[1] = InpMAShift = (int)StringToInteger(GetText(sparam));
               PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift);
              }
            else
               if(sparam == mPanel.prefix + "MAMethod")
                 {
                  mPanel.saveBuffer[2] = InpMAMethod = (int)StringToInteger(GetText(sparam));
                 }

         mPanel.Save();
         ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
        }

在移动平均线(MA)指标中处理隐藏/显示按钮的逻辑极其简单。

为了隐藏移动线,我们只需要将图形构造的样式设置为 DRAW_NONE,而要显示它时,则设置为 DRAW_LINE。 

   if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
      if(GetButtonState(sparam))
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
         mPanel.HideShowInd(true);
        }
      else
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE);
         mPanel.HideShowInd(false);
        }
  }

这杨就完成了对自定义移动平均线指标的修改。修改后的指标称为 Custom Moving Average Pnl。


ParabolicSAR 指标

ParabolicSAR 指标的修改与自定义移动平均线指标完全相同,只是有一些非常细微的差别。

ParabolicSAR 指标不需要创建与输入变量同名的新变量,因为它们已经存在。

因此,我们直接添加包含文件:

在 OnInit() 中添加代码:

  if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         ExtSarStep = mPanel.saveBuffer[0];
         ExtSarMaximum = mPanel.saveBuffer[1];
        }
      else
        {
         mPanel.Resize(2);
         mPanel.saveBuffer[0] = ExtSarStep;
         mPanel.saveBuffer[1] = ExtSarMaximum;
        }
      mPanel.Init("ParabolicSAR", short_name);
      mPanel.Record("SARStepText", OBJ_LABEL, 1, "SARStep:", 50);
      mPanel.Record("SARStep", OBJ_EDIT, 1, DoubleToString(ExtSarStep, 3), 50);
      mPanel.Record("SARMaximumText", OBJ_LABEL, 2, "SARMax:", 50);
      mPanel.Record("SARMaximum", OBJ_EDIT, 2, DoubleToString(ExtSarMaximum, 2), 50);
      mPanel.Create();
     }

 向指标中添加OnChartEvent()函数。

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= 0)
        {
         if(sparam == mPanel.prefix + "SARStep")
            mPanel.saveBuffer[0] = ExtSarStep = StringToDouble(GetText(sparam));
         else
            if(sparam == mPanel.prefix + "SARMaximum")
               mPanel.saveBuffer[1] = ExtSarMaximum = StringToDouble(GetText(sparam));

         mPanel.Save();
         ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
        }

   if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
      if(GetButtonState(sparam))
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
         mPanel.HideShowInd(true);
        }
      else
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW);
         PlotIndexSetInteger(0, PLOT_ARROW, 159);
         mPanel.HideShowInd(false);
        }
  }
//+------------------------------------------------------------------+

这是对ParabolicSAR指标的所有的修改。


RSI 指标

在 RSI 指标中,所有操作都与前两个指标完全相同。

在输入设置之后插入:

#include <Panel\\Panel.mqh>
CPanel mPanel;

接下来,在 OnInit() 中:

   if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         ExtPeriodRSI = (int)mPanel.saveBuffer[0];
        }
      else
        {
         mPanel.Resize(1);
         mPanel.saveBuffer[0] = ExtPeriodRSI;
        }
      mPanel.Init("RSI", short_name);
      mPanel.Record("PeriodRSIText", OBJ_LABEL, 1, "PeriodRSI:", 60);
      mPanel.Record("PeriodRSI", OBJ_EDIT, 1, IntegerToString(ExtPeriodRSI), 40);
      mPanel.Create();
     }

OnChartEvent() 将与前两个指标略有不同。

输入字段对象的处理方式相同。但隐藏/显示指标的处理方式不同。到目前为止,我们考虑的都是主图表上的指标,而 RSI 是一个子窗口指标。

点击“隐藏指标”时,我们将指标窗口的高度设置为零更改面板颜色,以及按钮的颜色和文本

再次按下按钮(此时按钮名称已变为“显示指标”),我们将 CHART_HEIGHT_IN_PIXELS 的值设置为 -1更改面板颜色,以及按钮的颜色和文本

引用说明书中:

“通过编程设置 CHART_HEIGHT_IN_PIXELS 属性可以防止用户编辑窗口/子窗口的大小。要取消固定大小,将属性值设置为 -1。”

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   if(mPanel.OnEvent(id, lparam, dparam, sparam))
     {
      if(id == CHARTEVENT_OBJECT_ENDEDIT)
         if(StringFind(sparam, mPanel.prefix) >= 0)
            if(sparam == mPanel.prefix + "PeriodRSI")
              {
               mPanel.saveBuffer[0] = ExtPeriodRSI = (int)StringToInteger(GetText(sparam));
               mPanel.Save();
               ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
              }

      if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") // hide the subwindow indicator
        {
         if(GetButtonState(sparam))
           {
            ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), 0);
            mPanel.HideShowInd(true);
           }
         else
           {
            ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -1);
            mPanel.HideShowInd(false);
           }
        }
     }
  }
//+------------------------------------------------------------------+



再来看另一个指标

有些指标根本不使用图形样式,而是绘制图形对象,通常是箭头。这是处理“隐藏/显示指标”按钮的另一种方式。我们来更详细地查看一下。

我没有去寻找带有箭头的指标,而是直接编写了一个分形指标,其中上部图标使用 PLOT_ARROW 图形构造显示,而下部图标则通过绘制 OBJ_ARROW 对象来显示。

我将在这里完整地提供指标代码。

设置中包括分形肩部的大小,以及我们将绘制 OBJ_ARROW 的天数。我们不得不限制天数,因为大量的对象可能会显著减慢图表的运行速度。

与前面的指标一样,在输入变量之后,我们包含 Panel.mqh 文件并声明 CPanel 类的实例

复制 input 变量为普通变量

#property indicator_chart_window
#property indicator_plots 1
#property indicator_buffers 1
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrRed
#property indicator_label1 "Fractals"

input int  _day      = 10; // day
input int  _barLeft  = 1;  // barLeft
input int  _barRight = 1;  // barRight
#include <Panel\\Panel.mqh>
CPanel mPanel;

double buff[];
int day = _day, barLeft = _barLeft, barRight = _barRight;
datetime limitTime = 0;

在OnInit()中,一切都和之前的指标一样。

//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, buff, INDICATOR_DATA);
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
   PlotIndexSetInteger(0, PLOT_ARROW, 217);
   PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, -5);
   string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight);
   IndicatorSetString(INDICATOR_SHORTNAME, short_name);

   if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         day = (int)mPanel.saveBuffer[0];
         barLeft = (int)mPanel.saveBuffer[1];
         barRight = (int)mPanel.saveBuffer[2];
        }
      else
        {
         mPanel.Resize(3);
         mPanel.saveBuffer[0] = day;
         mPanel.saveBuffer[1] = barLeft;
         mPanel.saveBuffer[2] = barRight;
        }
      mPanel.Init("Fractals", short_name);
      mPanel.Record("dayText", OBJ_LABEL, 1, "Days:", 50);
      mPanel.Record("day", OBJ_EDIT, 1, IntegerToString(day), 50);
      mPanel.Record("barLeftText", OBJ_LABEL, 2, "barLeft:", 50);
      mPanel.Record("barLeft", OBJ_EDIT, 2, IntegerToString(barLeft), 50);
      mPanel.Record("barRightText", OBJ_LABEL, 3, "barRight:", 50);
      mPanel.Record("barRight", OBJ_EDIT, 3, IntegerToString(barRight), 50);
      mPanel.Create();
     }

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

与前面指标中的 OnChartEvent() 的主要区别在于,每次指标参数发生变化时,我们需要从图表中 删除指标绘制的对象

如果我们按下隐藏指标的按钮,我们会在循环中隐藏指标绘制的所有对象。此外,我们还将图形构造的类型设置为 DRAW_NONE。

在相反的情况下,我们不仅需要设置 DRAW_ARROW 图形构造的类型,还需要设置来自Wingdings的箭头索引。接下来,在循环中将所有隐藏的对象变为可见。

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= 0)
        {
         if(sparam == mPanel.prefix + "day")
            mPanel.saveBuffer[0] = day = (int)StringToInteger(GetText(sparam));
         else
            if(sparam == mPanel.prefix + "barLeft")
               mPanel.saveBuffer[1] = barLeft = (int)StringToInteger(GetText(sparam));
            else
               if(sparam == mPanel.prefix + "barRight")
                  mPanel.saveBuffer[2] = barRight = (int)StringToInteger(GetText(sparam));

         mPanel.Save();
         ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
         ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
        }

   if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
     {
      if(GetButtonState(sparam))
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
         for(int i = ObjectsTotal(0) - 1; i >= 0; i--)
           {
            string name = ObjectName(0, i);
            if(StringFind(name, "DN_") >= 0)
               SetHide(name);
           }
         mPanel.HideShowInd(true);
        }
      else
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW);
         PlotIndexSetInteger(0, PLOT_ARROW, 217);
         for(int i = ObjectsTotal(0) - 1; i >= 0; i--)
           {
            string name = ObjectName(0, i);
            if(StringFind(name, "DN_") >= 0)
               SetShow(name);
           }
         mPanel.HideShowInd(false);
        }
     }
  }
//+------------------------------------------------------------------+

我们还需要在每次绘制新对象后,添加对标志的检查,该标志指示是否需要隐藏指标。如果该标志为true,则新绘制的对象应该被隐藏

//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   int limit = prev_calculated - 1;

   if(prev_calculated <= 0)
     {
      ArrayInitialize(buff, 0);
      datetime itime = iTime(_Symbol, PERIOD_D1, day);
      limitTime = itime <= 0 ? limitTime : itime;

      if(limitTime <= 0)
         return 0;
      int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime);
      limit = MathMax(rates_total - shift, barRight + barLeft);
     }

   for(int i = limit; i < rates_total && !IsStopped(); i++)
     {
      bool condition = true;
      for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
         if(high[j - 1] >= high[j])
           {
            condition = false;
            break;
           }

      if(condition)
         for(int j = i - barRight + 1; j <= i; j++)
            if(high[j - 1] <= high[j])
              {
               condition = false;
               break;
              }

      if(condition)
         buff[i - barRight] = high[i - barRight];

      condition = true;
      for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
         if(low[j - 1] <= low[j])
           {
            condition = false;
            break;
           }

      if(condition)
         for(int j = i - barRight + 1; j <= i; j++)
            if(low[j - 1] >= low[j])
              {
               condition = false;
               break;
              }

      if(condition)
        {
         string name = mPanel.prefix + "DN_" + (string)time[i - barRight];
         ObjectCreate(0, name, OBJ_ARROW, 0, time[i - barRight], low[i - barRight]);
         ObjectSetInteger(0, name, OBJPROP_ARROWCODE, 218);
         ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlue);
         if(mPanel.hideObject)
            SetHide(name);
        }
     }
   return(rates_total);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
  }
//+------------------------------------------------------------------+

在撰写本文的过程中,我举了一个使用面板作为快速访问对象设置的例子。我认为有必要解释一下该指标的代码。


设置对象面板指标

在该指标中,我们不需要在 OnInit() 中创建面板类的对象,因为面板应该用于不同的对象,这意味着我们将使用 new 运算符动态创建它。

声明一个类对象的句柄。如果在图表上点击了图形对象并且按下了Shift 键,则初始化之前创建的类对象句柄。 

我们创建面板的方式与在指标中创建的方式相同,唯一的区别是——Create() 方法接收当前鼠标在图表上的坐标作为参数。

输入字段的更改的处理方式与指标中的处理方式相同,唯一的区别是我们不需要将所做的更改保存到文件中。

完成编辑后,可以通过按下“删除面板”(Del Pnl)按钮来移除面板,同时移除句柄

不同的对象可能具有不同的属性,因此我们在绘制面板时应该牢记这一点。如果我们编辑趋势线,则不需要面板中负责填充对象的字段。 

这意味着我们不为趋势线创建这样的字段,而是只为设计用于填充的对象创建它。在这里,我们无法提前知道面板的确切行数,因此我们引入了line变量,将当前行索引写入其中,并根据需要增加它

#property indicator_chart_window
#property indicator_plots 0
#define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) delete (P)
#include <Panel\\Panel.mqh>
CPanel * mPl;
//+------------------------------------------------------------------+
int OnCalculate(const int, const int, const int, const double &price[]) {return(0);}
//+------------------------------------------------------------------+
void OnChartEvent(const int id,  const long &lparam, const double &dparam, const string &sparam)
  {
   static bool panel = false;

   if(panel)
      mPl.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_CLICK)
      if(!panel)
        {
         if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < 0)
           {
            int line = 1;
            mPl = new CPanel();
            ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam);
            mPl.Init(EnumToString(ObjectType), sparam);
            mPl.Record("Color_Text", OBJ_LABEL, line, "Color", 50);
            mPl.Record("Color", OBJ_EDIT, line, ColorToString((color)GetColor(sparam)), 50);
            line++;
            mPl.Record("StyleText", OBJ_LABEL, line, "Style", 50);
            mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), 50);
            line++;
            mPl.Record("WidthText", OBJ_LABEL, line, "Width", 50);
            mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), 50);
            line++;
            if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE)
              {
               mPl.Record("FillText", OBJ_LABEL, line, "Fill", 50);
               mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), 50);
               line++;
              }
            mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", 100);
            mPl.Create(0, (int)lparam, (int)dparam);
            panel = true;
           }
        }
      else
         if(sparam == mPl.prefix + "delButton")
           {
            FREE(mPl);
            panel = false;
           }

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPl.prefix) >= 0)
        {
         if(sparam == mPl.prefix + "Color")
            SetColor(mPl.indName, StringToColor(GetText(sparam)));
         else
            if(sparam == mPl.prefix + "Style")
               SetStyle(mPl.indName, (int)StringToInteger(GetText(sparam)));
            else
               if(sparam == mPl.prefix + "Width")
                  SetWidth(mPl.indName, (int)StringToInteger(GetText(sparam)));
               else
                  if(sparam == mPl.prefix + "Fill")
                     SetFill(mPl.indName, (int)StringToInteger(GetText(sparam)));
         ChartRedraw();
        }
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {FREE(mPl);}
//+------------------------------------------------------------------+

通过按下Shift键并鼠标左键点击对象来调出对象设置面板。


结论

优点:

  • 一个对用户友好的解决方案。

缺点:

  • 我想在 CPanel 类中设置“隐藏指标”功能,但没有成功。
  • 如果我们不在指标的短名称中添加输入变量,那么由于名称匹配,将无法在图表上同时调用多个带有面板的指标。
  • 如果我们在一个图表上启动一个指标,使用面板更改其设置,然后保存模板,那么在加载模板时,加载的将是指标启动时在设置中指定的参数,而不是最新的参数。
  • 并不是所有的指标都可以配备面板。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14672

附加的文件 |
MQL5.zip (28.8 KB)
Object.mqh (37.25 KB)
Panel.mqh (49.63 KB)
Ind_Pnl.mq5 (3.22 KB)
Fractals_Pnl.mq5 (12.99 KB)
RSI_Pnl.mq5 (12.37 KB)
ZigzagColor_Pnl.mq5 (24.24 KB)

该作者的其他文章

射箭算法(Archery Algorithm, AA) 射箭算法(Archery Algorithm, AA)
本文详细探讨了受射箭启发的优化算法——射箭算法(Archery Algorithm, AA),重点介绍了如何使用轮盘赌法(roulette method)作为选择“箭矢”目标区域的机制。该方法允许评估解决方案的质量,并选择最有希望的位置进行进一步的探究。
开发回放系统(第 61 部分):玩转服务(二) 开发回放系统(第 61 部分):玩转服务(二)
在本文中,我们将研究使回放/模拟系统更高效、更安全地运行的修改。我也不会对那些想要充分利用这些类的人置之不理。此外,我们将探讨 MQL5 中的一个特定问题,即在使用类时降低代码性能,并解释如何解决它。
量化风险管理方法:应用 VaR 模型优化多货币投资组合(使用 Python 和 MetaTrader 5) 量化风险管理方法:应用 VaR 模型优化多货币投资组合(使用 Python 和 MetaTrader 5)
本文探讨了价值风险(VaR)模型在多货币投资组合优化中的潜力。借助 Python 的强大功能和 MetaTrader 5 的功能,我们展示了如何实施 VaR 分析,以实现高效的资金分配和头寸管理。从理论基础到实际实施,文章涵盖了将 VaR——这一最稳健的风险计算系统之一——应用于算法交易的方方面面。
Connexus入门(第一部分):如何使用WebRequest函数? Connexus入门(第一部分):如何使用WebRequest函数?
本文是‘Connexus’库开发系列的开篇之作,旨在为MQL5环境下的HTTP请求提供便利支持。该项目的目的是为终端用户提供这个机会,并展示如何使用这个辅助库。我打算尽可能地简化,以便于学习,从而为进一步开发提供可能性。
OSZAR »