English Русский 中文 Español 日本語 Português
preview
Einfache Lösungen für die komfortable Handhabung von Indikatoren

Einfache Lösungen für die komfortable Handhabung von Indikatoren

MetaTrader 5Beispiele | 8 Mai 2025, 08:10
66 0
Aleksandr Slavskii
Aleksandr Slavskii

Indikatoren sind längst zu einem festen Bestandteil jeder Handelsplattform geworden. Fast alle Händler verwenden sie. Ein Handelssystem umfasst in der Regel ein ganzes System von Indikatoren und nicht nur einen einzigen, sodass die Bequemlichkeit der Einrichtung eines Indikators ein wichtiger Aspekt beim Handel ist.

In diesem Artikel beschreibe ich, wie man ein einfaches Panel erstellt, um die Einstellungen des Indikators direkt im Chart zu ändern, und welche Änderungen am Indikator vorgenommen werden müssen, um das Panel zu verbinden. Dieser Artikel ist für MQL5-Anfänger gedacht, daher werde ich jede Codezeile erklären. Fachleute werden hier nichts Neues finden.


Durchführung des Panels

Diese Website bietet viele verschiedene Panels in den Artikeln und der CodeBase, warum also nicht einen vorgefertigten Code nehmen? Es gibt eine Menge guter Codes, einschließlich einiger großartiger Bibliotheken, die es uns ermöglichen, ein Panel von beliebiger Komplexität zu erstellen.

Ich bin jedoch nicht glücklich über die Tatsache, dass die Universalität auf Kosten der Nutzerfreundlichkeit geht. Daher werde ich ein Panel speziell für Indikatoren entwickeln, im Wesentlichen eine normale Tabelle, in der sich die Breite und Höhe der Zellen automatisch an die Breite und Höhe des Textes in Abhängigkeit von der Schriftgröße anpassen.

Am oberen Rand des Fensters befindet sich eine Zeile, in der Sie das gesamte Fenster verschieben können. Diese Zeile enthält den Namen des Indikators sowie die Symbole zum Anheften und Ausklappen des Bereichs. Jede Tabellenzelle wird durch eine einzige Codezeile beschrieben. Auf diese Weise können wir bei Bedarf Spalten in verschiedenen Zeilen mit unterschiedlicher Breite erstellen.

Panel (Indikator) Name                               „Anheften“    „Einklappen“

Einstellung Name 1 Eingabefeld
Einstellung Name 2
Eingabefeld

Der Code, der die Zelle beschreibt, enthält den Objektnamen, den Objekttyp, die Zeilennummer, den Zellentext und die Breite der Zelle in % der Feldbreite. Die Panel-Breite und die Zellenbreite sind voneinander abhängig. Die Summe der Prozentsätze aller Zellen in einer Zeile sollte 100 % betragen.

Angenommen, Sie müssen drei Objekte in einer Reihe angeben, dann ist die Zeilennummer für alle drei Objekte gleich und die Breite z.B. 30% + 30% + 40% = 100%. In den meisten Fällen reicht es aus, die Reihe in zwei Teile zu teilen: 50% für den Namen des Einstellungsparameters und 50 % für das Eingabefeld.

Wie ich bereits sagte, wurde der Panel-Code so einfach wie möglich gestaltet. Also plante ich, auf objektorientierte Programmierung (OOP) zu verzichten. Es war jedoch nicht möglich, sie vollständig aufzugeben. Das Kopieren einer Menge Code von einem Indikator zum anderen wäre unpraktisch. Also habe ich den Panel-Code als Klasse in einer Include-Datei formatiert.

Ich habe eine Klasse anstelle regulärer, individueller Funktionen verwendet, weil es praktisch ist, Panel-Objekte im Destruktor zu löschen, da ich sie sonst im OnDeinit() des Indikators löschen müsste, was dort schwieriger zu bewerkstelligen wäre.

Es wird auch die Include-Datei Object.mqh mit Methoden zum Zeichnen von Objekten geben, und ich habe darin auch Getter und Setter implementiert, um den Zugriff auf Funktionen zu erleichtern. Ich werde nicht beschreiben, was Getter und Setter sind. Bei Bedarf können Sie sie googeln.

Die Idee des Panels wurde teilweise aus diesen Artikeln entlehnt: Artikel 1 und Artikel 2

Alle hier beschriebenen Codedateien sind am Ende des Artikels angehängt. Ich empfehle, sie herunterzuladen, in Ordner abzulegen und erst dann mit dem Studium des Codes zu beginnen. Ich habe einen separaten Ordner „Object“ im Ordner „Include“ für die Datei „Object.mqh“ erstellt. Für die Datei Panel.mqh wurde ein separater Panel-Ordner im Ordner „include“ erstellt. Dementsprechend wird der Pfad zu diesen Dateien in meinem Code unter Berücksichtigung von verschachtelten Ordnern angegeben.

Beginnen wir mit dem Einbinden der Datei Object.mqh und der Deklaration der Eingabevariablen. Wir müssen Variablen deklarieren, in denen wir Folgendes angeben: die Farben des Panels, des Textes, der Schaltflächen, der Rahmen sowie zusätzliche Farben, in denen das Panel gezeichnet wird, wenn der Indikator ausgeblendet ist, Schriftgröße, Schriftart sowie die Einzüge des Panels von den Rändern des Charts.

Eingabeeinstellungen:

//+------------------------------------------------------------------+
#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)
//+------------------------------------------------------------------+

Als Nächstes kommt die Klasse CPanel selbst:

//+------------------------------------------------------------------+
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();}
//+------------------------------------------------------------------+

Im Folgenden werden wir die Klassenmethoden anhand von Beispielen diskutieren.

Lassen Sie uns als Beispiel einen leeren Indikator schreiben: 

#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);
  }
//+------------------------------------------------------------------+

Wenn Sie den Indikator starten, sehen Sie das Panel wie folgt auf dem Chart: 

Lassen Sie uns nun den Code des Panels am Beispiel dieses Indikators im Detail untersuchen.

Direkt nach den Eingabeparametern des Indikators binden wir die Datei mit der Panel-Klasse ein und deklarieren die Klasse des Panels.

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

Normalerweise wird eine Klasse ganz am Anfang des Codes deklariert. Da sich die Panel-Klasse jedoch in einer einzubindenden Datei befindet, die auch Eingaben enthält, befinden sich die Indikatorparameter unterhalb der Eingaben des Panels, wenn wir sie ganz am Anfang des Eingabecodes schreiben. Dies führt zu einigen Unannehmlichkeiten beim Starten und Konfigurieren des Indikators.

Da die Eingabevariablen konstant sind, können sie nicht verändert werden. Es ist jedoch möglich, eine Kopie der Eingabevariablen zu erstellen und sie über das Eingabefeld des Panels zu bearbeiten. 

Als Nächstes fügen wir den Panel-Code in OnInit() des Indikators ein.

Zunächst möchte ich Sie jedoch darauf aufmerksam machen, dass der Indikator einen Kurznamen im Code haben sollte, der die wichtigsten Eingabeparameter enthält, damit das Panel korrekt funktioniert.

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

Dies ist notwendig, um den Indikator mit verschiedenen Einstellungen laufen zu lassen.

Ich möchte Sie daran erinnern, dass einige Symbole nicht in Indikatornamen verwendet werden können. Wenn Sie Parameter mit einem Doppelpunkt trennen wollen, ist es besser, ihn durch ein Semikolon zu ersetzen.

Der Name des Panels kann derselbe sein wie der Name des Indikators, aber es ist bequemer, den Namen des Panels ohne Berücksichtigung der Indikatorparameter festzulegen.

Die erste Methode der Klasse CPanel, die wir dem Indikator hinzufügen werden, ist die Methode Init(), der wir zwei Namen übergeben: den Namen des Panels und den Namen des Indikators. 

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

Init() stellt zunächst sicher, dass das Panel in den Einstellungen nicht deaktiviert ist.

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

Als Nächstes werden die Variablen initialisiert:

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

Lassen Sie uns die Erlaubnis erteilen, Nachrichten über Mausbewegungen und Tastendruckereignisse (CHARTEVENT_MOUSE_MOVE) an alle MQL5-Programme auf dem Chart zu senden, und erlauben Sie auch das Senden von Nachrichten über das Ereignis der Erstellung eines grafischen Objekts (CHARTEVENT_OBJECT_CREATE):

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

Um die Breite des Feldes zu berechnen, müssen wir zunächst die Schriftart und -größe festlegen und die Größe eines einzelnen Symbols ermitteln, damit diese Größe in Zukunft zur Berechnung der Texteinzüge an den Feldrändern verwendet werden kann.

Auf diese Weise erhalten wir skalierbare Einzüge, die an die Schriftgröße gebunden sind. Die Zellenhöhe entspricht der Höhe von eineinhalb Zeichen.

// 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);

Die Panel-Einstellungen enthalten die Symbole zum Ein-/Ausblenden des Panels ❐ sowie zum Anheften/Entheften ∇ und _.

Ich habe die Icons im Internet gefunden und sie können in den Einstellungen geändert werden.

Fügen wir den Symbolen Leerzeichen hinzu, um sie vom Rand des Panels aus richtig zu positionieren. Wenn wir keine Leerzeichen hinzufügen, werden die Symbole zu nahe beieinander angezeigt und es wird schwierig, sie mit der Maus zu treffen.

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

Das Panel besteht aus grafischen Objekten. Wir erstellen ein Präfix für sie, damit die Namen dieser Objekte eindeutig sind:

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

Wenn das Chart mehrere Indikatoren mit einem Panel enthält, kann die Funktion GetTickCount() bei der Erstellung eines Präfixes nicht verwendet werden, da der Wechsel der Zeitrahmen so wenig Zeit in Anspruch nimmt, dass sich die Präfixe einiger Panels überschneiden können, wenn Sie Millisekunden anstelle von Mikrosekunden verwenden.

Beim Ziehen des Panels bestimmt die Funktion OnChartEvent() die Position der Maus auf dem Chart und auf dem Objekt. Das Panel kann mit einem anderen Panel zusammenstoßen, was zu einem Konflikt führen kann. Um dies zu vermeiden, erstellen wir eine globale Variable, und wenn die Maus auf dem Panel gedrückt wird, wird das erste Panel, das sein Präfix in diese globale Variable schreibt, die Maus bewegen. Wer zuerst kommt, mahlt zuerst.

Bei der Initialisierung schreiben wir Null in diese Variable. Solange Null in die Variable geschrieben wird, gilt sie als frei.

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

Wenn wir einen Bereich verschieben, einklappen, anheften oder die Parameter des Indikators ändern, müssen wir diese Änderungen irgendwo speichern. Dies ermöglicht es uns, das Panel und den Indikator mit den neuesten Einstellungen zu laden, wenn wir den Zeitrahmen wechseln oder das Terminal neu starten. Ich habe den Code zum Speichern der letzten Einstellungen des Panels und des Indikators nicht in den Indikator aufgenommen, aber auch ohne diesen Code wird der Indikator Änderungen an den Panel-Einstellungen aufzeichnen. Zu diesem Zweck müssen wir Speicher für ein Array mit den Panel-Einstellungen zuweisen.

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

Obwohl wir die Anzahl der Indikatoreinstellungen = 0, Resize(0); an die Funktion übergeben. In der Funktion selbst werden drei Zellen hinzugefügt, um die Panel-Einstellungen zu speichern. Mit anderen Worten, wir verwenden drei Zellen des saveBuffer-Arrays, um die Position des Panels auf dem Chart, seinen Zustand (angeheftet/entheftet, ein-/ausgeklappt) und auch den Zustand des Indikators (angezeigt/ausgeblendet) zu speichern.

Als Nächstes folgt der Code, der die Anfangskoordinaten des Panel-Ankerpunkts definiert. Der Punkt ist, dass der Ankerpunkt des Panels aus den Eingabeeinstellungen oder aus den gespeicherten Einstellungen übernommen werden kann, wenn das Panel bereits auf diesem Chart gezeichnet wurde. Eine weitere Möglichkeit ist die Verwendung einer Vorlage, in der der Indikator mit dem Panel eingestellt wurde. 

Die Handhabung der Vorlage erwies sich als schwieriger. Wenn wir eine Vorlage speichern, die einen Indikator mit dem Panel enthält, können wir die Koordinaten des Panels zum Zeitpunkt der Erstellung der Vorlage nicht speichern.

Wenn wir jedoch einen Indikator zum Chart hinzufügen, die Vorlage speichern und dann anwenden, sehen wir, dass das Objekt OBJ_LABEL text label in die Vorlage geschrieben wird.

Speichern der Vorlage:

 

Anwenden der Vorlage:

 

Anhand dieser Textbeschriftungen wird die Position des Panels zum Zeitpunkt der Erstellung der Vorlage festgelegt.

   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;
        }
     }

Die Variablen lastX und lastY legen die Koordinaten des Objektankerpunkts fest — ein Textlabel mit einem Namen, der den Namen des Indikators (Textkoordinaten des Panel-Namens) nach dem Präfix enthält.

Ich möchte Sie daran erinnern, dass der Name des Panels vom Namen des Indikators abweichen kann. Nachdem der erforderliche Text gefunden wurde, rufen Sie das Präfix ab und speichern es.

Der folgende Code verwendet das zuvor gespeicherte Präfix, um die veralteten Textbeschriftungen, die in der Vorlage gespeichert wurden, aus dem Chart zu entfernen.

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

Als Nächstes folgt die Prüfung und Auswahl der gewünschten Option für den Panel-Ankerpunkt.

   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;
        }

Ganz am Ende von Init() senden wir die Panel-Objekte, die nicht bearbeitet werden müssen, an das Array der Strukturen. Sie bleiben für alle Panels gleich.

Zwei Rechtecke, Text mit dem Namen des Panels und Symbole zum Ein-/Ausblenden und Anheften/Entheften des Panels.

   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);

Gehen wir nun zur nächsten Methode, Record(), über.

Die Struktur des Future-Objekts wird in der Methode Record() gefüllt. Normalerweise wird der größte Teil der Struktur mit Standardwerten gefüllt. Mit den Parametern, die dieser Funktion übergeben werden, können wir jedoch die Standardwerte geringfügig ändern und z. B. eine andere Farbe für das Objekt festlegen. 

//+------------------------------------------------------------------+
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++;
  }
//+------------------------------------------------------------------+

Zu Beginn der Methode Record() rufen wir die Methode WidthHidthCalc() auf, in der die Breite und Höhe des Panels berechnet wird.

Schauen wir uns die Methode WidthHidthCalc() genauer an.

Bei dieser Methode wird die Breite des Panels unter Berücksichtigung des breitesten Elements berechnet, z. B. wenn wir dem oben beschriebenen Indikator Ind Pnl einen längeren Namen zuweisen.

Vorherige:

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

Aktuell:

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

Ergebnis:

Oder wenn wir z.B. den Namen der Indikatoreinstellung ändern, erhalten wir folgendes.

Vorherige:

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

Aktuell:

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

Ergebnis:

Das Panel passt sich automatisch an die Textgröße an. Alle Berechnungen der Breite und Höhe des Panels werden mit der Funktion WidthHidthCalc() durchgeführt.

Zunächst ermitteln wir die Textbreite der Zelle.

Hier wenden wir eine etwas andere Methode an, um die Textbreite mit dem Panel-Namen und den Symbolen für das Ein- und Ausblenden im Vergleich zu anderen Zellen zu ermitteln.

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

Der Text sollte vom Zellrand aus eingerückt werden, und wir werden diesen Einzug auf ein halbes Zeichen festlegen. Wir haben die Breite eines Zeichens bereits in der Funktion Init() ermittelt und in die Variable widthLetter gesetzt.

Um den Text auf beiden Seiten einzurücken, müssen wir die Breite eines weiteren Symbols zu der sich ergebenden Textbreite hinzufügen, während wir im Fall des Objekts Text-Buttons, OBJ_BUTTON, ein weiteres Zeichen hinzufügen müssen, um einen Einzug von den Rändern der Schaltfläche zu gewährleisten.

Da wir nun die Größe der gesamten Zeile in der Zelle einschließlich der Einzüge kennen, können wir die Größe des Feldes unter Berücksichtigung der unter der Zelle angegebenen Prozentsätze berechnen.

Wir speichern den größten Wert des Panels. Künftig werden alle Zellen auf der Grundlage dieses größten Werts für die Breite des Panels berechnet.

   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;
     }

Die Berechnung der Panel-Breite im Testindikator sieht folgendermaßen aus.

Zunächst wird die Breite des Titels unter Berücksichtigung der Icons berechnet. Im Falle von "Ind Pnl"  + " ∇ " + " ❐ " haben wir die Breite von 71 рх plus die Breite eines Symbols von 7 рх, insgesamt 78 рх — das sind 100 % der Panel-Breite.

Der Text der Zelle ist „param“, die Breite von 36 px unter Berücksichtigung der hinzugefügten Einzüge von 7 px ergibt 43 px, 60 % der Panel-Breite wird dieser Zelle zugewiesen, was bedeutet, dass die Panel-Breite 43 * 100 / 60 = 72 px beträgt. Dies ist weniger als für den Titel des Panels benötigt wird, was bedeutet, dass die Breite des Panels gleich der Zelle mit dem Titel sein wird.

Als Nächstes definieren wir den Spaltenindex und/oder fügen die Panel-Höhe hinzu, wenn es sich um eine neue Zeile handelt.

   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;
  }

Wir haben also die Arbeit von zwei der zehn Methoden der Klasse CPanel im Detail untersucht.

Nachdem das Programm die zukünftigen Abmessungen des Panels bestimmt und die Parameter der Objekte in das Array der Strukturen mObject[] gesetzt hat, gehen wir zur nächsten Methode über, Create(). Bei dieser Methode wird ein Panel auf der Grundlage der zuvor ermittelten Abmessungen erstellt.

Wie üblich wird zu Beginn der Methode geprüft, ob das Panel benötigt wird. Es folgt der Code für zwei vordefinierte Schaltflächen. Eine Taste blendet den Indikator aus, eine andere entfernt ihn. Je nach den ausgewählten Flags kann man Folgendes wählen: 0 - keine Tasten, 1 - eine Taste zum Ein-/Ausblenden der Anzeige, 2 - eine Taste zum Entfernen der Anzeige, 3 - beide Tasten werden erstellt.

Warum befinden sich diese Schaltflächen hier und nicht im Code des Indikators? Dadurch können wir weniger Code in den Indikatorcode einfügen.

Als Nächstes folgt die Initialisierung der Variablen. Dieser Code wird benötigt, wenn das Panel nicht genau wie vorgesehen verwendet werden soll, sondern z. B. als Popup-Panel zur Änderung der Objektparameter. Das Panel muss also an der Stelle erscheinen, an der Sie mit der Maus auf das Chart klicken.

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;
     }

Das Panel kann sowohl für ein Informations- als auch für ein Handels-Panel angepasst werden, ebenso wie für ein Panel wie dieses zum Einrichten von Chart-Objekten. Alles ist einfach, der Fokus liegt auf reiner Funktionalität:

Aber ich schweife ab. Fahren wir fort mit der Analyse der Methode Create(). Als Nächstes folgt der Code, mit dem zwei Rechtecke erstellt werden, ein Rechteck in der Kopfzeile und ein Rechteck für den Hauptteil:

// 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);

Nachdem jedes Objekt erstellt wurde, wird die Funktion Add() aufgerufen, die den Namen und die Koordinaten des Objekts relativ zur oberen linken Ecke des Charts in Arrays schreibt.

//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Diese Arrays mit Koordinaten werden später beim Verschieben des Panels verwendet.

Kehren wir nun zum Code der Methode Create() zurück. Dann werden alle Objekte in einer Schleife in der gleichen Reihenfolge erstellt, in der sie in das mObject[]-Strukturfeld geschrieben wurden. Auf die Berechnung der Koordinaten und Größen folgt die Erstellung eines Objekts.

Da das Panel hochspezialisiert ist, verwendet es nur drei Arten von Objekten, was für seine Funktionalität völlig ausreichend ist.

Beim Füllen des Kopfzeilen-Rechtecks mit einem Text musste ich auf Ausnahmen zurückgreifen und einen Ankerpunkt für die Symbole zum Anheften und Einklappen des Panels implementieren, damit sie sich von den Ankerpunkten aller anderen Objekte des Panels unterscheiden. Dies erleichterte die Positionierung dieser Symbole auf dem Panel, da sie einen Ankerpunkt in der oberen rechten Ecke haben.

 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);
     }

Nachdem alle Panel-Objekte erstellt wurden, entfernen wir das Array der Struktur mObject[], da es nicht mehr benötigt wird. 

   ArrayFree(mObject);

   ApplySaved();

   ChartRedraw();

Ich erstelle normalerweise eine separate Funktion, wenn der Code mehrfach verwendet wird. Wenn jedoch das Wesentliche des Vorgangs nach seiner Bedeutung gruppiert ist, stelle ich es als eigene Methode heraus. Das habe ich mit der Funktion ApplySaved() gemacht. Er prüft, ob bereits Daten für das Panel gespeichert sind, und wendet sie an, wenn dies der Fall ist, oder speichert neue Daten, wenn dies nicht der Fall ist.

ApplySaved()

Wenn der Indikator zum ersten Mal in diesem Chart ausgeführt wird, wird das Array saveBuffer[] mit den Anfangseinstellungen gefüllt.

Wenn das Array saveBuffer[] bereits gespeicherte Daten enthält, werden diese anstelle der ursprünglichen Einstellungen verwendet.

//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Wie Sie vielleicht bemerkt haben, verwendet die Funktion ApplySaved() auch die Funktionen Save(), HideShowInd() und OnEvent(). Wenn Sie dies lesen, schreiben Sie bitte „noticed“ in die Kommentare. Es ist sehr interessant zu wissen, ob jemand diese Beschreibungen liest oder nicht.

Fahren wir nun mit der Beschreibung dieser Funktionen fort. Speichern Sie die erhaltenen Einstellungen mit Save(). Legen Sie einen separaten pnl-Ordner für gespeicherte Panel-Einstellungen an, um den Ordner Files nicht zu überladen

So sieht die Funktion Save() aus:

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

HideShowInd()

Der Zweck dieser Funktion ist es, die Farbe der Kopfzeile des Panels sowie die Farbe und den Text der Schaltflächen zu ändern. Entfernt das vorherige Flag aus dem saveBuffer-Array und setzt ein neues.

Die Variable hideObject wird nur in den Indikatoren benötigt, in denen Objekte (Pfeile, Icons, Text usw.) zum Zeichnen verwendet werden. Bei der Erstellung eines neuen Objekts im Indikator wird der Zustand dieser Variable überprüft und je nach Zustand werden die neu erstellten Objekte entweder sofort ausgeblendet oder es wird nichts unternommen, und die Objekte werden angezeigt. 

//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+

Die Funktion wird nur angewendet, wenn Sie auf die Schaltfläche Einblenden/Ausblenden klicken, sofern diese verwendet wird.

Beispiel für das Ausblenden eines der beiden RSI-Indikatoren 

Der Code enthält auch die Funktion HideShow(), die für das Ein- und Ausblenden von Objekten zuständig ist. Die Funktion wird verwendet, um das Panel einzuklappen und die Panel-Objekte in den Vordergrund zu bringen.

Die Funktion nimmt ein Argument entgegen, das angibt, ob das Panel eingeklappt ist oder nicht (true/false). Wenn das Panel eingeklappt ist, müssen wir nur vier seiner Objekte in den Vordergrund bringen: ein Rechteck mit dem Titel, den Titel selbst und zwei Symbole, Anheften und Einklappen.

Wenn das Flag „true“ ist (das Panel ist eingeklappt), werden nacheinander fünf Objekte ausgeblendet und wieder eingeblendet. Warum fünf und nicht vier? Unter den erforderlichen Objekten gibt es ein zusätzliches, das Rechteck des Panels selbst. Wir erstellen es vor dem Namen und den Symbolen, daher muss dieses Rechteck separat ausgeblendet werden.

Ist das Flag „false“, werden alle Panel-Objekte nacheinander ausgeblendet und dann wieder angezeigt. Dadurch werden sie in den Vordergrund gerückt.

//+------------------------------------------------------------------+
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]);
  }
//+------------------------------------------------------------------+

So sieht das eingeklappte Panel neben dem normalen aus:

Die nächste Funktion, die wir uns ansehen werden, ist OnEvent()

Zu Beginn der Funktion wird geprüft, ob das Panel in den Einstellungen erlaubt ist:

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

Als Nächstes wollen wir uns den Code ansehen, der für das Verschieben des Panels verantwortlich ist. Ich werde versuchen, die Funktionsweise des Codes so detailliert wie möglich zu erklären. 

Wenn das Ereignis „Mausbewegung“ eintritt und das Flag, das die Bewegung des Panels erlaubt, im Speicher gesetzt ist, speichern wir die Mauskoordinaten.

Wenn es sich um einen Linksklick handelt, ist sparam gleich 1, und wenn die Maustaste vorher nicht geklickt wurde, wird der Wert der globalen Variablen gelesen.

Diese globale Variable gilt für alle im Terminal laufenden Panels. Beim Verschieben eines Feldes wird geprüft, ob der Präfixwert eines anderen Feldes auf diese Variable gesetzt ist, und wenn dies nicht der Fall ist, wird der Präfix des Feldes auf diese globale Variable gesetzt. Das bedeutet, dass andere Panels (wenn sie sich unter/über dem zu verschiebenden Paneel befinden) nicht mehr verschoben werden können.

Wenn die globale Variable Null oder das Präfix dieses Panels enthält, werden die aktuellen Ankerpunktkoordinaten des Panels gelesen.

Dann prüfen wir, ob dies ein Mausklick auf das Rechteck mit der Kopfzeile des Panels war. Wenn ja, erhalten wir die Größe des Charts in Pixeln, deaktivieren das Scrollen des Charts, während die globale Variable das Präfix des Panels festlegt. Der Präfix-Eintrag ist sowohl ein Flag, das das Verschieben des Paneels erlaubt, als auch eine Garantie, dass nur dieses Paneel verschoben wird.

Abschließend wird das Panel ausgeblendet/angezeigt, um es in den Vordergrund zu bringen, wobei an die Funktion das das Argument (true/false) übergeben wird, das bestimmt, ob das Panel derzeit ausgeblendet oder vollständig angezeigt wird. 

   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
              }
           }
        }

Nachdem der Nutzer auf das Rechteck mit dem Namen des Panels geklickt und die Maus bewegt hat, wird der folgende Code ausgeführt.

Zunächst wird geprüft, ob das Panel verschoben werden darf. Wenn die globale Variable ein Präfix des Panels hat, dann kann sie verschoben werden. Dann werden die Koordinaten der Ankerpunkte das Panel ermittelt. Wenn wir das Panel entsprechend den Mauskoordinaten verschieben, überprüfen wir die neue Position des Panels. Wenn es über das Chart hinausgehen kann, ändern wir die Werte der Ankerpunktkoordinaten geringfügig, damit das Panel nicht über das Chart hinausgeht.

In der Schleife verschieben wir alle Panel-Objekte.

Setzen der neuen Koordinaten des Ankerpunkts des Panels in das Array, um sie anschließend in die Datei zu schreiben. Danach wird das Chart neu gezeichnet.

      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);
        }

Die letzte Aktion beim Verschieben des Panels findet statt, wenn die Maustaste losgelassen wird, sparam ist dann nicht mehr gleich eins.

Wir bringen die Möglichkeit zurück, das Chart zu verschieben, die globale Variable zurückzusetzen und neue Koordinaten des Panel-Ankerpunkts in die Datei zu schreiben.

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

      previousMouseState = sparam;
     }

Wir haben den Mechanismus zum Ziehen eines Panels im Detail untersucht, jetzt werden wir die Aktionen beim Klicken auf die Symbole zum Anheften/Entheften eines Panels oder zum Ein-/AusklappenErweitern eines Panels untersuchen.

All dies wird in derselben Funktion OnEvent() durchgeführt.

Wenn die Maustaste auf ein grafisches Objekt geklickt wird, enthält die Variable sparam den Namen des angeklickten Objekts. Wenn er mit dem ❐ Objektnamen übereinstimmt, wird das nächste Objekt geprüft, und wenn es sichtbar ist, werden die Objekte des Panels ausgeblendet. Wenn es unsichtbar ist, werden alle Objekte des Panels angezeigt. Wir ändern das Flag für die Sichtbarkeit des Panels und schreiben es in das Array, um es anschließend in einer Datei zu speichern.

Wenn man auf das Objekt klickt, wird manchmal der vorherige Code, der für das Verschieben des Panels verantwortlich ist, ausgelöst, und da sich das ❐-Symbol in dem Bereich befindet, hinter den das Panel gezogen wird, ist das Scrollen des Charts oft deaktiviert. Aktivieren wir also das Scrollen des Charts. Aus demselben Grund müssen wir auch eine globale Variable zurücksetzen.

Wir speichern die Änderungen in einer Datei, um die Änderungen im Chart anzuzeigen und es neu zu zeichnen.

   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);
           }

Der folgende Code ähnelt dem oben beschriebenen, der einzige Unterschied ist der Objektname von ∇.

         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);
              }

Der Code endet mit der Schaltfläche zum Löschen des Indikators:

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

Das letzte Ereignis, das wir behandeln müssen, ist die Erstellung eines grafischen Objekts

Neu erstellte Objekte werden in der Regel im Vordergrund platziert und können das Panel überlappen. Daher merken wir uns zunächst den Status der Objektauswahl, blenden dann das Panel aus/ein, um es in den Vordergrund zu bringen, und stellen schließlich den Status der Objektauswahl wieder her. Warum diese ganze Aufregung? Das programmgesteuerte Ausblenden der Anzeige der Objekte des Panels führt dazu, dass die Auswahl von neu erstellten Objekten entfernt wird.

Dieses Phänomen wird hier ausführlicher beschrieben.

      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;
  }

Damit ist die Beschreibung des Panel-Codes abgeschlossen.


Welche Änderungen müssen an dem Indikatorcode vorgenommen werden?

Unterschiedliche Indikatoren erfordern unterschiedliche Änderungen. Es war nicht möglich, einen universellen Code zu erstellen, der in den Indikator eingefügt werden konnte, um ein fertiges Panel zu erhalten. Jeder Indikator muss separat behandelt werden.

Im Folgenden werde ich einige Beispiele anführen, die Ihnen helfen werden, andere Indikatoren in ähnlicher Weise zu ändern.

Aus Gründen der Einheitlichkeit habe ich den Namen der modifizierten Indikatoren das Wort „Pnl“ hinzugefügt.


Nutzerdefinierter Indikator für gleitenden Durchschnitt

Um den Indikator über das Panel zu steuern, sollten wir die Eingabevariablen ändern können. Die Eingabevariablen sind jedoch konstant und können nicht verändert werden.

Um dieses Problem zu lösen, können wir die Eingabevariablen in reguläre Variablen kopieren, die geändert werden können. Um die Änderungen am Indikatorcode so gering wie möglich zu halten, deklarieren wir die neuen Variablen mit denselben Namen wie die aktuellen Eingabevariablen, fügen ihnen aber einen Unterstrich hinzu.

Vorherige:

ma input

Danach:

Direkt nach den Eingabeparametern binden wir die Datei Panel.mqh ein und deklarieren die Instanz der Klasse CPanel mPanel;

Wenn die #include-Direktive vor den Eingabeparametern gesetzt wird, befinden sich alle in der Include-Datei gesetzten Eingabeparameter über den Eingabeparametern des Indikators, was zu Unannehmlichkeiten beim Starten des Indikators führen wird.

Wenn wir alles richtig machen, sollten wir ein Bild wie dieses sehen:

Wenn wir die Panel-Einstellungen nicht benötigen, können wir einfach alle Wörter „input“ in der Include-Datei Panel.mqh löschen und die Standardeinstellungen verwenden.

Fügen Sie den folgenden Code in die Funktion OnInit() ein.

Als nächstes überprüfen wir, ob das Panel im Indikator aktiviert ist, und laden die zuvor gespeicherten Panel-Einstellungen herunter, wenn der Indikator nicht zum ersten Mal auf diesem Chart gestartet wird. Wenn dies der erste Start ist, ändern wir die Größe des Arrays um die Anzahl der Eingabeparameter (es gibt drei) und setzen die Werte dieser Eingabeparameter in das Array. 

   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;
        }

Name des Panels, Name des Indikators

Dann wird alles auf die gleiche Weise ausgefüllt: Objektname, Objekttyp, Zeilenindex des Bereichs, das Objekt selbst, Prozentsatz der Bereichsbreite

      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();
     }

Wenn wir den Indikator in diesem Stadium ausführen, erhalten wir ein Panel wie dieses:

Wir haben also ein Panel und müssen nur noch den Code für die Kommunikation mit dem Nutzer schreiben.

Dem Indikator fügen wir eine weitere Funktion hinzu, OnChartEvent().

Die Methode, die für das Verschieben des Panels verantwortlich ist, wird oben beschriebenWenn das Ereignis „Ende der Bearbeitung des Textes im grafischen Objekt Bearbeiten“ eintritt, prüfen wir das Präfix des bearbeiteten Objekts, in dem die Bearbeitung abgeschlossen wurde. Wenn das Präfix mit dem Präfix unseres Panels übereinstimmt, prüfen wir, welcher der Indikatorparameter geändert wurde, setzen die Änderungen in die Variable und in das Array zum späteren Speichern.

Anschließend speichern wir alle Änderungen in der Datei und starten den Indikator neu.

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);
        }

Die Bedienung der Ausblenden/Anzeigen-Taste im MA-Indikator ist denkbar einfach.

Um die sich bewegende Linie auszublenden, müssen wir nur den Stil der grafischen Konstruktion DRAW_NONE entsprechend auf DRAW_LINE einstellen. 

   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);
        }
  }

Damit sind die Änderungen am nutzerdefinierten gleitenden Durchschnittsindikator abgeschlossen. Der modifizierte Indikator heißt Custom Moving Average Pnl.


Der Indikator ParabolicSAR

Die Modifikation des Indikators ParabolicSAR ist absolut identisch mit der von Custom Moving Average, mit Ausnahme einiger kleiner Nuancen.

Für den ParabolicSAR-Indikator müssen keine neuen Variablen mit demselben Namen wie die Eingabevariablen angelegt werden, da diese bereits vorhanden sind.

Daher fügen wir sofort die Include-Datei hinzu:

In OnInit() fügen wir folgenden Code ein:

  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();
     }

 Die Funktion OnChartEvent() fügen wir zum Code des Indikators hinzu.

//+------------------------------------------------------------------+
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);
        }
  }
//+------------------------------------------------------------------+

Dies sind alle Änderungen am ParabolicSAR-Indikator.


RSI-Indikator

Beim RSI-Indikator wird alles genauso gehandhabt wie bei den beiden anderen Indikatoren.

Nach den Eingabeeinstellungen fügen wir ein:

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

Als Nächstes 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() unterscheidet sich geringfügig von den vorherigen Indikatoren.

Das Objekt Eingabefeld wird auf die gleiche Weise behandelt. Das Ein- und Ausblenden des Indikators wird jedoch anders gehandhabt. Bis jetzt haben wir mit Indikatoren des Hauptcharts gearbeitet, während der RSI ein Indikator in einem Unterfenster ist.

Wenn Sie auf Ausblenden klicken, setzen wir die Höhe des Indikatorfensters auf Null. Ändern der Farbe des Panels sowie die Farbe und den Text der Schaltflächen.

Wenn wir die Schaltfläche erneut drücken (sie hat jetzt einen anderen Namen, Ind Show), setzen wir den Wert von CHART_HEIGHT_IN_PIXELS auf -1Wir ändern die Farbe des Panels sowie die Farbe und den Text der Schaltfläche.

Zitat aus dem Lehrbuch:

„Wenn wir die Eigenschaft CHART_HEIGHT_IN_PIXELS programmatisch einstellen, kann der Nutzer die Größe des Fensters/der Unterfenster nicht bearbeiten. Um die Größenfixierung zu deaktivieren, setzen Sie den Eigenschaftswert auf -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);
           }
        }
     }
  }
//+------------------------------------------------------------------+



Noch ein weiterer Indikator

Es gibt Indikatoren, die überhaupt keine grafischen Stile verwenden, sondern stattdessen grafische Objekte, meist Pfeile, zeichnen. Dies ist eine weitere Option zur Handhabung der Schaltfläche „Indikator ausblenden/anzeigen“. Schauen wir uns das genauer an.

Anstatt nach einem Indikator mit Pfeilen zu suchen, habe ich einfach einen fraktalen Indikator geschrieben, bei dem die oberen Symbole mit der grafischen Konstruktion PLOT_ARROW angezeigt werden, während die unteren durch das Zeichnen des Objektes OBJ_ARROW dargestellt werden.

Ich werde den Indikatorcode hier vollständig zur Verfügung stellen.

Zu den Einstellungen gehören die Größe der fraktalen Schultern sowie die Anzahl der Tage, an denen OBJ_ARROW gezeichnet werden soll. Wir waren gezwungen, die Anzahl der Tage zu begrenzen, da eine große Anzahl von Objekten das Chart erheblich verlangsamen könnte.

Wie in den vorangegangenen Indikatoren binden wir unmittelbar nach den Eingabevariablen die Datei Panel.mqh ein und deklarieren die Instanz der Klasse CPanel.

Duplizieren von Eingabevariablen mit normalen Variablen.

#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;

Bei OnInit() ist alles wie bei den vorherigen Indikatoren.

//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Der Hauptunterschied zu OnChartEvent() in früheren Indikatoren besteht darin, dass wir jedes Mal, wenn sich die Parameter des Indikators ändern, die vom Indikator gezeichneten Objekte aus dem Chart löschen müssen.

Wenn wir die Schaltfläche zum Ausblenden des Indikators drücken, werden alle vom Indikator gezeichneten Objekte in der Schleife ausgeblendet. Außerdem legen wir auch den Typ der grafischen Konstruktion DRAW_NONE fest.

Im umgekehrten Fall müssen wir nicht nur den Typ der grafischen Konstruktion DRAW_ARROW festlegen, sondern auch den Pfeilindex von Wingdings. Als Nächstes werden in der Schleife alle versteckten Objekte sichtbar gemacht.

//+------------------------------------------------------------------+
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);
        }
     }
  }
//+------------------------------------------------------------------+

Außerdem müssen wir nach jedem neu gezeichneten Objekt die Prüfung auf das Flag hinzufügen, das anzeigt, dass der Indikator ausgeblendet werden muss. Wenn dieses Flag „true“ ist, sollte das neu gezeichnete Objekt ausgeblendet werden.

//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Während der Arbeit an diesem Artikel habe ich ein Beispiel für die Verwendung des Panels als Schnellzugriff auf Objekteinstellungen gegeben. Ich denke, es lohnt sich, den Indikatorcode zu erläutern.


Einstellen der Objekte des Pnl-Indikators

In diesem Indikator brauchen wir kein Objekt der Panel-Klasse in OnInit() zu erstellen, da das Panel für verschiedene Objekte aufgerufen werden soll, d.h. wir erstellen es dynamisch mit dem Operator „new“.

Wir deklarieren ein Handle für das Klassenobjekt. Wenn auf ein grafisches Chartobjekt geklickt und die Umschalttaste gedrückt wurde, wird das zuvor erstellte Handle des Klassenobjekts initialisiert

Wir erstellen ein Panel auf die gleiche Weise wie bei den Indikatoren, mit einer Nuance — die Methode Create() erhält die aktuellen Chart-Koordinaten der Maus als Argument.

Die Änderungen in den Eingabefeldern werden genauso gehandhabt wie bei den Indikatoren, mit dem einzigen Unterschied, dass wir die Änderungen nicht in der Datei speichern müssen.

Sobald die Bearbeitung abgeschlossen ist, kann das Panel durch Drücken der Taste Del Pnl entfernt werden, während das Handle entfernt wird.

Da verschiedene Objekte unterschiedliche Eigenschaften haben können, sollten wir dies beim Zeichnen des Panels berücksichtigen. Wenn wir eine Trendlinie bearbeiten, brauchen wir das Feld im Panel, das für das Füllen des Objekts zuständig ist, nicht. 

Das bedeutet, dass wir ein solches Feld nicht für die Trendlinie anlegen, sondern nur für Objekte, die gefüllt werden sollen. Da wir die genaue Anzahl der Zeilen im Panel nicht im Voraus wissen können, führen wir die Variable line ein, schreiben den aktuellen Zeilenindex hinein und erhöhen ihn nach Bedarf.

#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);}
//+------------------------------------------------------------------+

Das Einstellungsfenster des Objekts wird aufgerufen, indem wir die Umschalttaste gedrückt halten und mit der linken Maustaste auf das Objekt klicken.


Schlussfolgerung

Vorteile:

  • Eine nutzerfreundliche Lösung.

Nachteile

  • Ich wollte den Indikator in der CPanel-Klasse „ausblenden“, aber es hat nicht funktioniert.
  • Wenn wir dem Kurznamen des Indikators keine Eingabevariablen hinzufügen, dann ist der Aufruf mehrerer Indikatoren mit einem Panel aufgrund übereinstimmender Namen nicht möglich.
  • Wenn wir einen Indikator auf einem Chart starten, seine Einstellungen über das Panel ändern und dann die Vorlage speichern, werden beim Laden der Vorlage die Parameter geladen, die beim Start des Indikators in den Einstellungen angegeben waren, und nicht die neuesten.
  • Nicht alle Anzeiger können mit einem Panel ausgestattet werden.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14672

Beigefügte Dateien |
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)

Andere Artikel von diesem Autor

Archery-Algorithmus (AA) Archery-Algorithmus (AA)
Der Artikel wirft einen detaillierten Blick auf den vom Bogenschießen inspirierten Optimierungsalgorithmus, wobei der Schwerpunkt auf der Verwendung der Roulette-Methode als Mechanismus zur Auswahl vielversprechender Bereiche für „Pfeile“ liegt. Die Methode ermöglicht es, die Qualität der Lösungen zu bewerten und die vielversprechendsten Positionen für weitere Untersuchungen auszuwählen.
Von der Grundstufe bis zur Mittelstufe: Die Direktive Include Von der Grundstufe bis zur Mittelstufe: Die Direktive Include
Im heutigen Artikel werden wir eine Kompilierungsdirektive besprechen, die in verschiedenen Codes, die in MQL5 zu finden sind, häufig verwendet wird. Obwohl diese Direktive hier nur oberflächlich erklärt wird, ist es wichtig, dass Sie sich mit ihrer Verwendung vertraut machen, da sie bald unverzichtbar sein wird, wenn Sie sich auf höheren Ebenen der Programmierung bewegen. Der hier dargestellte Inhalt ist ausschließlich für Bildungszwecke bestimmt. Die Anwendung sollte unter keinen Umständen zu einem anderen Zweck als zum Erlernen und Beherrschen der vorgestellten Konzepte verwendet werden.
Quantitativer Ansatz für das Risikomanagement: Anwendung des VaR-Modells zur Optimierung eines Multiwährungsportfolios mit Python und MetaTrader 5 Quantitativer Ansatz für das Risikomanagement: Anwendung des VaR-Modells zur Optimierung eines Multiwährungsportfolios mit Python und MetaTrader 5
In diesem Artikel wird das Potenzial des Value-at-Risk (VaR)-Modells für die Optimierung von Portfolios in mehreren Währungen untersucht. Mit Hilfe von Python und der Funktionalität von MetaTrader 5 demonstrieren wir, wie man eine VaR-Analyse für eine effiziente Kapitalallokation und Positionsverwaltung implementiert. Von den theoretischen Grundlagen bis zur praktischen Umsetzung behandelt der Artikel alle Aspekte der Anwendung eines der robustesten Risikoberechnungssysteme - VaR - im algorithmischen Handel.
Neuronale Netze im Handel: Punktwolkenanalyse (PointNet) Neuronale Netze im Handel: Punktwolkenanalyse (PointNet)
Die direkte Analyse von Punktwolken vermeidet unnötiges Datenwachstum und verbessert die Leistung von Modellen bei Klassifizierungs- und Segmentierungsaufgaben. Solche Ansätze zeigen eine hohe Leistungsfähigkeit und Robustheit gegenüber Störungen in den Originaldaten.