English Русский 中文 Español 日本語 Português
Andere Klassen in der Bibliothek DoEasy (Teil 70): Erweiterte Funktionalität und automatisches Aktualisieren der Kollektion der Chartobjekte

Andere Klassen in der Bibliothek DoEasy (Teil 70): Erweiterte Funktionalität und automatisches Aktualisieren der Kollektion der Chartobjekte

MetaTrader 5Beispiele | 31 Mai 2021, 08:52
369 0
Artyom Trishkin
Artyom Trishkin

Inhalt


Konzept

Im vorherigen Artikel habe ich die Kollektion der Chartobjekte erstellt. Nun wird jeder im Terminal geöffnete Symbolchart durch ein Chart-Objekt repräsentiert. Jedes Chartobjekt verfügt über eine Reihe von Fensterobjekten, die Fensterindikatorobjekte enthalten. Jedes Chartobjekt hat mindestens ein Fensterobjekt — das ist das Hauptfenster des Charts. Alle übrigen Indikatorfenster können der Chart-Fensterliste hinzugefügt und aus ihr entfernt werden. Ich habe die gesamte Objektmenge in die Kollektion der Chartobjekte gelegt.

Beim ausführlichen Testen der Chartobjekte-Kollektion aus dem vorherigen Artikel traten einige Probleme beim Hinzufügen neuer Fenster zum Haupt-Chartfenster auf. Diese werde ich hier beheben. Außerdem werde ich die neue Funktionalität für Chartobjekte hinzufügen — die Navigation innerhalb des Symboldiagrammfensters, das Erstellen von Fenster-Screenshots, sowie das Speichern und Hochladen von Vorlagen in den Chart.

Zusätzlich zu den geplanten Verbesserungen werde ich die automatische Verfolgung einiger Ereignisse implementieren, die in Charts im Client-Terminal und in Chart-Objekt-Fenstern auftreten — Hinzufügen eines neuen/entfernen eines bestehenden Symbol-Charts (Chart-Objekt), Hinzufügen eines neuen/entfernen eines bestehenden Indikator-Fensters aus dem Chart-Objekt, sowie Hinzufügen eines neuen/entfernen eines bestehenden Indikators aus dem Chart-Fenster.


Verbesserung der Bibliothek der Klasse

Wie üblich nimmt die Datei \MQL5\Include\DoEasy\Data.mqh die Indizes der neuen Meldungen auf:

   MSG_CHART_OBJ_CHART_WINDOW,                        // Main chart window
   MSG_CHART_OBJ_CHART_SUBWINDOW,                     // Chart subwindow
   MSG_CHART_OBJ_CHART_SUBWINDOWS_NUM,                // Subwindows
   MSG_CHART_OBJ_INDICATORS_MW_NAME_LIST,             // Indicators in the main chart window
   MSG_CHART_OBJ_INDICATORS_SW_NAME_LIST,             // Indicators in the chart window
   MSG_CHART_OBJ_INDICATOR,                           // Indicator
   MSG_CHART_OBJ_INDICATORS_TOTAL,                    // Indicators
   MSG_CHART_OBJ_WINDOW_N,                            // Window
   MSG_CHART_OBJ_INDICATORS_NONE,                     // No indicators
   MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ,              // Failed to receive the chart window object
   MSG_CHART_OBJ_SCREENSHOT_CREATED,                  // Screenshot created
   MSG_CHART_OBJ_TEMPLATE_SAVED,                      // Chart template saved
   MSG_CHART_OBJ_TEMPLATE_APPLIED,                    // Template applied to chart
  
//--- CChartObjCollection
   MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION,        // Chart collection
   MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ,  // Failed to create a new chart object
   MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART,         // Failed to add a chart object to the collection
   MSG_CHART_COLLECTION_ERR_CHARTS_MAX,               // Cannot open new chart. Number of open charts at maximum
  
  };
//+------------------------------------------------------------------+

und die Nachrichtentexte, die den neu hinzugefügten Indizes entsprechen:

   {"Главное окно графика","Main chart window"},
   {"Подокно графика","Chart subwindow"},
   {"Подокон","Subwindows"},
   {"Индикаторы в главном окне графика","Indicators in the main chart window"},
   {"Индикаторы в окне графика","Indicators in the chart window"},
   {"Индикатор","Indicator"},
   {"Индикаторов","Indicators total"},
   {"Окно","Window"},
   {"Отсутствуют","No indicators"},
   {"Не удалось получить объект-окно графика","Failed to get the chart window object"},
   {"Скриншот создан","Screenshot created"},
   {"Шаблон графика сохранён","Chart template saved"},
   {"Шаблон применён к графику","Template applied to the chart"},
   
//--- CChartObjCollection
   {"Коллекция чартов","Chart collection"},
   {"Не удалось создать новый объект-чарт","Failed to create new chart object"},
   {"Не удалось добавить объект-чарт в коллекцию","Failed to add chart object to collection"},
   {"Нельзя открыть новый график, так как количество открытых графиков уже максимальное","You cannot open a new chart, since the number of open charts is already maximum"},
   
  };
//+---------------------------------------------------------------------+


Da ich die zusätzliche Funktionalität von Chartobjekten implementieren werde, einschließlich der Fähigkeit, Screenshots zu erstellen und Vorlagen zu handhaben, müssen wir Speicherordner für Screenshots und Vorlagen sowie die Standard-Dateinamenerweiterung (die auch das Format der gespeicherten Bilddatei bedeutet) für Screenshots angeben. Mögliche Dateitypen für das Speichern von Screenshots sind *.gif, *.png und *.bmp.

Fügen wir diese neuen Makro-Ersetzungen zu \MQL5\Include\DoEasy\Defines.mqh hinzu:

//--- Data parameters for file operations
#define DIRECTORY                      ("DoEasy\\")               // Library directory for storing object folders
#define RESOURCE_DIR                   ("DoEasy\\Resource\\")     // Library directory for storing resource folders
#define SCREENSHOT_DIR                 ("DoEasy\\ScreenShots\\")  // Library directory for storing screenshot folders
#define TEMPLATE_DIR                   ("DoEasy\\")               // Library directory for storing template folders
#define FILE_EXT_GIF                   (".gif")                   // GIF image file name extension
#define FILE_EXT_PNG                   (".png")                   // PNG image file name extension
#define FILE_EXT_BMP                   (".bmp")                   // BMP image file name extension
#define SCREENSHOT_FILE_EXT            (FILE_EXT_PNG)             // Chart screenshot file format (extension: .gif, .png and .bmp can be used)
//--- Symbol parameters

Die Ordner zum Speichern von Screenshots und Vorlagen im Terminal sind unterschiedlich.

Screenshots werden im Ordner (Terminal-Datenordner)\MQL5\Files\ gespeichert.

Vorlagen werden in (Terminaldaten-Ordner)\ MQL5\Profiles\Templates\ gespeichert.

Durch das Hinzufügen bestimmter Makro-Ersetzungen zum Dateinamen wird die Speicherung von Bibliotheksdateien also zielgerichteter.
Screenshots sind unter \MQL5\Files\DoEasy\ScreenShots\ zu speichern, während Vorlagen unter MQL5\Profiles\Templates\DoEasy\ zu speichern sind.

Um das Speichern von Screenshot-Dateien bequemer zu machen, implementieren wir die Funktion in der Datei der Servicefunktionen \MQL5\Include\DoEasy\Services\DELib.mqh. Die Funktion gibt den Dateinamen zurück, der sich aus dem Namen des Programms, aus dem sie gestartet wird, einem Präfix, das an die Funktionsparameter übergeben wird und einer lokalen PC-Zeit zusammensetzt:

//+------------------------------------------------------------------+
//| Return the file name (program name+local time)                   |
//+------------------------------------------------------------------+
string FileNameWithTimeLocal(const string time_prefix=NULL)
  {
   string name=
     (
      MQLInfoString(MQL_PROGRAM_NAME)+"_"+time_prefix+(time_prefix==NULL ? "" : "_")+
      TimeToString(TimeLocal(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
     );
   ResetLastError();
   if(StringReplace(name," ","_")==WRONG_VALUE)
      CMessage::ToLog(DFUN,GetLastError(),true);
   if(StringReplace(name,":",".")==WRONG_VALUE)
      CMessage::ToLog(DFUN,GetLastError(),true);
   return name;
  }
//+------------------------------------------------------------------+

Die Zeichenkette im Funktionsaufruf besteht aus dem Programmnamen + einem übergebenen Wert in den Funktionsparametern + der lokalen PC-Zeit im Format Datum/Stunden-Minuten/Sekunden. Anschließend werden alle Leerzeichen durch Unterstriche (_) und alle Doppelpunkte durch Punkte (.) ersetzt und die resultierende Zeichenkette zurückgegeben. Wenn die Ersetzungen nicht aktiviert wurden, werden die entsprechenden Meldungen im Journal angezeigt.
Die Funktion gibt den gleichen Dateinamen innerhalb einer Sekunde zurück. D. h., wenn die Funktion mehrmals pro Sekunde aufgerufen wird, liefert sie innerhalb dieser Sekunde immer die gleiche Zeichenkette zurück. Deshalb implementieren wir hier einen Funktionseingang, der die Übergabe zusätzlicher Dateidaten erlaubt, um die Identifikation der Datei eindeutig und aussagekräftiger zu machen.

Wir sind in der Lage, für jedes Chart-Fenster Koordinaten in Pixeln anzugeben. Diese Koordinaten entsprechen den Zeit-/Kurskoordinaten mit ChartXYToTimePrice(). Außerdem können wir mit ChartTimePriceToXY() eine Rücktransformation durchführen. Lassen Sie uns diese Funktionalität zu den Objekten hinzufügen. Die erste Funktion soll im Chartobjekt arbeiten, während die zweite — im Chartfenster-Objekt. Die Funktion ChartXYToTimePrice() verfügt über den Index des Subfensters, in dem sich der Cursor befindet. Der Index wird durch einen Link von der Funktion zurückgegeben. Dies funktioniert in jedem Chart-Fenster. Der Fensterindex, der den Cursor enthält, wird in der Variablen gesetzt, die wir beim Aufruf der Funktion in die Funktionsparameter einbetten. Ein Index des Fensters, dessen Preis/Zeit-Koordinaten wir aus den Bildschirmkoordinaten des Fensters erhalten müssen, sollte dagegen manuell an die zweite Funktion übergeben werden, um Zeit und Preis im Chart-Fenster in die entsprechenden Bildschirmkoordinaten in Pixel zu transformieren. Es ist sinnvoller, die Funktion (d.h. die Methode, um mit ihr zu arbeiten) im Chart-Fenster-Objekt zu platzieren, um die entsprechenden Koordinaten daraus zu erhalten.

Fügen Sie im privaten Teil der Klasse in \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh die Variablen zum Speichern der Cursor-Koordinaten im Fenster hinzu:

//+------------------------------------------------------------------+
//| Chart window object class                                        |
//+------------------------------------------------------------------+
class CChartWnd : public CBaseObj
  {
private:
   CArrayObj         m_list_ind;                                        // Indicator list
   int               m_window_num;                                      // Subwindow index
   int               m_wnd_coord_x;                                     // The X coordinate for the time on the chart in the window
   int               m_wnd_coord_y;                                     // The Y coordinates for the price on the chart in the window
//--- Return the flag indicating the presence of an indicator from the list in the window
   bool              IsPresentInWindow(const CWndInd *ind);
//--- Remove indicators not present in the window from the list
   void              IndicatorsDelete(void);
//--- Add new indicators to the list
   void              IndicatorsAdd(void);
//--- Set a subwindow index
   void              SetWindowNum(const int num)                        { this.m_window_num=num;   }
   
public:

Wir deklarieren im öffentlichen Teil der Klasse die Methode, die die Chart-Koordinaten aus der Zeit-/Kursdarstellung in die X- und Y-Koordinaten umwandelt und schreiben Sie die beiden Methoden, die die bereits erhaltenen Koordinaten an die Variablen zurückgeben, sowie die Methode, die die relative Y-Koordinate im Fenster zurückgibt:

//--- Update data on attached indicators
   void              Refresh(void);
   
//--- Convert the coordinates of a chart from the time/price representation to the X and Y coordinates
   bool              TimePriceToXY(const datetime time,const double price);
//--- Return X and Y coordinates of the cursor location in the window
   int               XFromTimePrice(void)                         const { return this.m_wnd_coord_x;  }
   int               YFromTimePrice(void)                         const { return this.m_wnd_coord_y;  }
//--- Return the relative Y coordinate of the cursor location in the window
   int               YFromTimePriceRelative(void)  const { return this.m_wnd_coord_y-this.YDistance();}
   
  };
//+------------------------------------------------------------------+

Da die Werte der Y-Koordinaten in allen Fenstern vom Anfang der Koordinaten (der linken oberen Ecke des Chart-Hauptfensters) aus angegeben werden, müssen wir den Abstand vom oberen Fensterrand von der Y-Koordinate abziehen, um die Koordinate relativ zum oberen Fensterrand zu erhalten. Dies tun wir in der letzten Methode.

In der Initialisierungsliste des parametrischen Klassenkonstruktors, initialisieren wir neue Variablen mit den Standardwerten:

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CChartWnd::CChartWnd(const long chart_id,const int wnd_num) : m_window_num(wnd_num),
                                                              m_wnd_coord_x(0),
                                                              m_wnd_coord_y(0)
  {
   CBaseObj::SetChartID(chart_id);
   this.IndicatorsListCreate();
  }
//+------------------------------------------------------------------+

Außerhalb des Klassenkörpers implementieren wir die Methode, die die Chart-Koordinaten von der Zeit-/Kursdarstellung in die Koordinaten der X- und Y-Achse umwandelt:

//+------------------------------------------------------------------+
//| Convert chart coordinates from the time/price representation     |
//| to X and Y coordinates                                           |
//+------------------------------------------------------------------+
bool CChartWnd::TimePriceToXY(const datetime time,const double price)
  {
   ::ResetLastError();
   if(!::ChartTimePriceToXY(this.m_chart_id,this.m_window_num,time,price,this.m_wnd_coord_x,this.m_wnd_coord_y))
     {
      //CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Hier geben wir einfach das Ergebnis der Operation der Funktion ChartTimePriceToXY() zurück, indem wir alle notwendigen Werte an sie übergeben. Die Fehlermeldung im Log habe ich auskommentiert, da wir zu viele dieser Meldungen im Journal erhalten, wenn sich der Cursor außerhalb des Chart-Feldes, aber innerhalb des Chart-Fensters befindet.

Die Methode schreibt das erhaltene Ergebnis in die neu hinzugefügten Variablen, um sie zu speichern, während die Methoden XFromTimePrice() und YFromTimePrice() die Variablenwerte zurückgeben. Wir sollten also zunächst die Methode TimePriceToXY() aufrufen. Nachdem sie true zurückgibt, können wir den Wert der benötigten Koordinate erhalten.

Verbessern wir die Methode für die Aktualisierung der Daten von Indikatoren, die an das Fenster angehängt sind. Um eine ständige Neuerstellung der Indikatorliste zu vermeiden, vergleichen wir zunächst die Anzahl der Indikatoren im Fenster mit ihrer Anzahl in der Liste. Wenn es Änderungen gibt, erstellen wir die Indikatorliste neu:

//+------------------------------------------------------------------+
//| Update data on attached indicators                               |
//+------------------------------------------------------------------+
void CChartWnd::Refresh(void)
  {
   int change=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num)-this.m_list_ind.Total();
   if(change!=0)
     {
      this.IndicatorsDelete();
      this.IndicatorsAdd();
     }
  }
//+------------------------------------------------------------------+


Verbessern wir die Objektklasse der Charts in \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh. Im vorigen Artikel habe ich die Methode WindowsTotal() so eingerichtet, dass wir den Wert aus der Umgebung holen und nach einem einzigen Aufruf auf die Objekteigenschaften setzen können. Dies erwies sich jedoch als nicht sehr praktisch in Bezug auf die Übersichtlichkeit der Codelogik und die Anzahl der Referenzen auf die Umgebung, so dass ich mich entschied, diese Idee aufzugeben. Jetzt gibt die Methode einfach den Wert der Objekteigenschaft zurück:

   int WindowsTotal(void) const { return (int)this.GetProperty(CHART_PROP_WINDOWS_TOTAL); }

Der Wert aus der Umgebung soll auf eine Objekteigenschaft gesetzt werden, wo es wirklich notwendig ist.

Das Chartobjekt erhält die restlichen, für den aktuellen Artikel vorgesehenen Zusatzfunktionen: das Navigieren im Chart, das Erstellen von Chart-Screenshots, das Arbeiten mit Chart-Vorlagen und das Konvertieren der X- und Y-Koordinaten des Charts in die Zeit- und Kurswerte.

Binden wir noch die Klassendatei CSelect in die Klassendatei CChartObj ein:

//+------------------------------------------------------------------+
//|                                                     ChartObj.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\..\Objects\BaseObj.mqh"
#include "..\..\Services\Select.mqh"
#include "ChartWnd.mqh"
//+------------------------------------------------------------------+

Sie wird verwendet, um die Liste der Chart-Objekte nach ihren Eigenschaften zu sortieren.

Fügen wir im privaten Bereich der Klasse zwei neue Klassenvariablen zum Speichern der Zeit für die X-Koordinate und des Preises für die Y-Koordinate im Chart hinzu:

//+------------------------------------------------------------------+
//| Chart object class                                               |
//+------------------------------------------------------------------+
class CChartObj : public CBaseObj
  {
private:
   CArrayObj         m_list_wnd;                                  // List of chart window objects
   long              m_long_prop[CHART_PROP_INTEGER_TOTAL];       // Integer properties
   double            m_double_prop[CHART_PROP_DOUBLE_TOTAL];      // Real properties
   string            m_string_prop[CHART_PROP_STRING_TOTAL];      // String properties
   int               m_digits;                                    // Symbol's Digits()
   datetime          m_wnd_time_x;                                // Time for X coordinate on the windowed chart
   double            m_wnd_price_y;                               // Price for Y coordinate on the windowed chart
   

Hier, im privaten Abschnitt der Klasse deklarieren wir die Methode, die der Screenshot-Datei eine Erweiterung hinzufügt, wenn sie fehlt:

//--- Create the list of chart windows
   void              CreateWindowsList(void);
//--- Add an extension to the screenshot file if it is missing
   string            FileNameWithExtention(const string filename);
   
public:

Deklarieren wir die neuen Methoden am Ende des Codes des Klassenhauptteils:

//--- Return the flag indicating that the chart object belongs to the program chart
   bool              IsMainChart(void)                               const { return(this.m_chart_id==CBaseObj::GetMainChartID());            }
//--- Return the chart window specified by index
   CChartWnd        *GetWindowByIndex(const int index)               const { return this.m_list_wnd.At(index);                               }
//--- Return the window object by its subwindow index
   CChartWnd        *GetWindowByNum(const int win_num)               const;
//--- Return the window object by the indicator name in it
   CChartWnd        *GetWindowByIndicator(const string ind_name)     const;
   
//--- Display data of all indicators of all chart windows in the journal
   void              PrintWndIndicators(void);
//--- Display the properties of all chart windows in the journal
   void              PrintWndParameters(void);

//--- Shift the chart by the specified number of bars relative to the specified chart position
   bool              Navigate(const int shift,const ENUM_CHART_POSITION position);
//--- Shift the chart (1) to the left and (2) to the right by the specified number of bars
   bool              NavigateLeft(const int shift);
   bool              NavigateRight(const int shift);
//--- Shift the chart (1) to the beginning and (2) to the end of the history data
   bool              NavigateBegin(void);
   bool              NavigateEnd(void);

//--- Create the chart screenshot
   bool              ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align);
//--- Create the screenshot of the (1) chart window, (2) 800х600 and (3) 750х562 pixels
   bool              ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   bool              ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   bool              ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   
//--- Save the chart template with the current settings
   bool              SaveTemplate(const string filename=NULL);
//--- Apply the specified template to the chart
   bool              ApplyTemplate(const string filename=NULL);
   
//--- Convert X and Y chart window coordinates into time and price
   int               XYToTimePrice(const long x,const double y);
//--- Return (1) time and (2) price from XY coordinates
   datetime          TimeFromXY(void)                                const { return this.m_wnd_time_x;   }
   double            PriceFromXY(void)                               const { return this.m_wnd_price_y;  }
   
  };
//+------------------------------------------------------------------+

In der Initialisierungsliste des parametrischen Konstruktors initialisieren wir neue Klassenvariablen mit den Standardwerten:

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CChartObj::CChartObj(const long chart_id) : m_wnd_time_x(0),m_wnd_price_y(0)
  {
  }

Betrachten wir die Implementierung der neuen Methoden.

Die Methode, die das Fensterobjekt durch einen Indikatornamen darin zurückgibt:

//+------------------------------------------------------------------+
//| Return the window object by the indicator name in it             |
//+------------------------------------------------------------------+
CChartWnd *CChartObj::GetWindowByIndicator(const string ind_name) const
  {
   int index=(this.m_program==PROGRAM_INDICATOR && ind_name==NULL ? ::ChartWindowFind() : ::ChartWindowFind(this.m_chart_id,ind_name));
   return this.GetWindowByIndex(index);
  }
//+------------------------------------------------------------------+

Wenn das gestartete Programm auf Basis der Bibliothek ein Indikator ist, wird die Funktion ChartWindowFind() ohne die Parameter aufgerufen, damit der Indikator weiß, in welchem Fenster er gestartet wurde. Wenn wir ein Fenster eines anderen Indikators finden müssen, sollte ChartWindowFind() die ID des Charts, dessen Fensterindex gefunden werden soll, durch den Indikatornamen erhalten.
Deshalb prüfen wir hier zuerst den Typ des Programms, und wenn es sich um einen Indikator handelt und NULL als Name übergeben wird, rufen wir die Funktion ChartWindowFind() ohne Parameter auf — dies ist eine Anfrage des Indikators für die Suche nach einem eigenen Fenster.
Andernfalls rufen wir ChartWindowFind() auf, die die ID des zum Chartobjekt gehörenden Charts und einen Indikator-Kurznamen an die Methode übergeben bekommt, deren Fensterindex gefunden werden soll.
Um das Fensterobjekt zurückzugeben, in dem sich der angegebene Indikator befindet, verwenden wir also die Methode, die das zum Chartobjekt gehörende Fensterobjekt um den gefundenen Fensterindex mit dem Indikator zurückgibt.

Die Methode verschiebt den Chart um die angegebene Anzahl von Balken relativ zur angegebenen Chartposition:

//+------------------------------------------------------------------+
//| Move the chart by the specified number of bars                   |
//| relative to the specified chart position                         |
//+------------------------------------------------------------------+
bool CChartObj::Navigate(const int shift,const ENUM_CHART_POSITION position)
  {
   ::ResetLastError();
   bool res=::ChartNavigate(m_chart_id,position,shift);
   if(!res)
      CMessage::ToLog(DFUN,::GetLastError(),true);
   return res;
  }
//+------------------------------------------------------------------+

Die Methode ruft einfach die Funktion ChartNavigate() mit den an die Methode übergebenen Shift-Parametern auf — die Anzahl der Balken (Shift) und die Chart-Position, relativ zu der die Verschiebung durchgeführt wird (Position). Wenn die Funktionsausführung nicht erfolgreich ist, zeigt die Methode die Fehlermeldung im Journal an. Es wird das Ergebnis des Aufrufs der Funktion ChartNavigate() zurückgegeben. Vor dem Aufruf der Methode deaktivieren wir den automatischen Bildlauf zum rechten Rand des Charts für das Chart-Objekt, damit die Methode korrekt arbeitet.

Die Methode verschiebt das Chart um die angegebene Anzahl von Balken nach links:

//+------------------------------------------------------------------+
//| Shift the chart to the left by the specified number of bars      |
//+------------------------------------------------------------------+
bool CChartObj::NavigateLeft(const int shift)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(shift,CHART_CURRENT_POS);
  }
//+------------------------------------------------------------------+

Hier deaktivieren wir zunächst den automatischen Bildlauf zum rechten Rand des Charts für das Chartobjekt und geben das Ergebnis der Methode Navigate() zurück. Die Methode erhält den Wert der Verschiebung des Charts (in Balken), der an die Methode übergeben wird.
Verschieben des Charts von der aktuellen Position.

Die Methode verschiebt den Chart um die angegebene Anzahl von Balken nach rechts:

//+------------------------------------------------------------------+
//| Shift the chart to the right by the specified number of bars     |
//+------------------------------------------------------------------+
bool CChartObj::NavigateRight(const int shift)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(-shift,CHART_CURRENT_POS);
  }
//+------------------------------------------------------------------+

Hier deaktivieren wir zunächst den automatischen Bildlauf zum rechten Rand des Charts für das Chartobjekt und geben das Ergebnis der Methode Navigate() zurück. Die Methode empfängt einen negativen Chart-Verschiebungswert (in Balken), der an die Methode übergeben wird.
Verschiebt den Chart ab der aktuellen Position.

Die Methode verschiebt den Chart an den Anfang der historischen Daten:

//+------------------------------------------------------------------+
//| Shift the chart to the beginning of the history data             |
//+------------------------------------------------------------------+
bool CChartObj::NavigateBegin(void)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(0,CHART_BEGIN);
  }
//+------------------------------------------------------------------+

Hier deaktivieren wir zunächst den automatischen Bildlauf zum rechten Rand des Charts für das Chartobjekt und geben das Ergebnis der Methode Navigate() zurück. Die Methode erhält den Wert der Null für die Verschiebung des Charts (in Balken).
Verschiebt den Chart an den Anfang der Historie.

Die Methode, die den Chart an das Ende der historischen Daten (zum aktuellen Zeitpunkt) verschiebt:

//+------------------------------------------------------------------+
//| Shift the chart to the end of the history data                   |
//+------------------------------------------------------------------+
bool CChartObj::NavigateEnd(void)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(0,CHART_END);
  }
//+------------------------------------------------------------------+

Hier deaktivieren wir zunächst den automatischen Bildlauf zum rechten Rand des Charts für das Chartobjekt und geben das Ergebnis der Methode Navigate() zurück. Die Methode erhält den Wert der Null für die Verschiebung des Charts (in Balken).
Das Chart an das Ende der Historie verschieben — auf die aktuelle Zeit.

Die Methode, die den Chart-Screenshot erstellt:

//+------------------------------------------------------------------+
//| Create the chart screenshot                                      |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align)
  {
   ::ResetLastError();
   if(!::ChartScreenShot(m_chart_id,filename,width,height,align))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Die Methode erhält den Namen der Screenshot-Datei, Breite und Höhe eines erhaltenen Bildes und die Ausrichtung (ENUM_ALIGN_MODE). Letzteres wird benötigt, um vertikale Screenshots zu erstellen, wenn die Höhe des Bildes seine Breite übersteigt. In diesem Fall gibt die Ausrichtung den Rand des Charts an, an dem das Bild ausgerichtet werden soll.
Hier machen wir einfach einen Screenshot mit den Parametern, die wir mit der Funktion ChartScreenShot() an die Methode übergeben.
Wenn der Screenshot erfolgreich aufgenommen wurde, wird true zurückgegeben.
Wenn die Erstellung eines Screenshots fehlgeschlagen ist, wird eine entsprechende Meldung im Journal angezeigt und false zurückgegeben.

Als weitere Methoden werden drei Methoden verwendet, die Screenshots in einer bestimmten Größe erstellen:

  1. Screenshot fitting the chart window (passend zum Chart-Fenster),
  2. screenshot 800х600,
  3. screenshot 750х562.

Die zweite und dritte Größe sind aus einem bestimmten Grund gewählt — sie werden oft für die Veröffentlichung von Bildern im MQL5.com-Forum, in Artikeln und in Market-Produktbeschreibungen benötigt. Der Screenshot, angepasst an den Chart, ermöglicht es, das Aussehen und die Größe des Fensters direkt im Terminal einzustellen und einen Screenshot in einer gewünschten Größe zu erstellen.

Die Methode erstellt einen Chart-Screenshot, der zur Auflösung des Chart-Fensters passt (einschließlich der Preis- und Zeitskalen, falls vorhanden):

//+------------------------------------------------------------------+
//| Create the chart screenshot fitting the chart window resolution  |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
//--- Create the file name or use the one passed to the method
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
//--- Get the chart window having the largest number of all windows
   CChartWnd *wnd=this.GetWindowByNum(this.m_list_wnd.Total()-1);
   if(wnd==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ),string(this.m_list_wnd.Total()-1));
      return false;
     }
//--- Calculate the screenshot width and height considering the size of the price and time scales
   int width=this.WidthInPixels()+(IsShowPriceScale() ? 56 : 0);
   int height=wnd.YDistance()+wnd.HeightInPixels()+(this.IsShowDateScale() ? 15 : 0);
//--- Create a screenshot and return the result of the ScreenShot() method
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

Im Methodencode finden Sie die Kommentare, die die Logik im Detail beschreiben. Kurz gesagt, wenn NULL beim Erstellen eines Namens an die Methode übergeben wird, wird der Dateiname erstellt, der standardmäßig aus dem Pfad zu den Bibliotheks-Screenshot-Dateien, dem Programmnamen und einer in Defines.mqh angegebenen Erweiterung besteht. Wenn der an die Methode übergebene Dateiname nicht leer ist, wird die Methode FileNameWithExtention() verwendet (wird später beschrieben), um das Vorhandensein einer Erweiterung im Dateinamen zu prüfen (Screenshots können eine der drei Erweiterungen haben: .gif, .png und .bmp) und es wird eine Erweiterung zum Dateinamen hinzugefügt, wenn sie nicht vorhanden ist.

Um die Screenshot-Größe unter Berücksichtigung aller zum Chart gehörenden Fenster zu berechnen, müssen wir ein Fenster mit dem größten Index finden (0 — Hauptfenster des Charts, 1, 2, 3, N — alle darin geöffneten Fenster von oben nach unten). Mit anderen Worten, das unterste Fenster wird die höchste Nummer haben. Wenn wir den Abstand von der Oberkante des Hauptfensters des Charts zur Oberkante des untersten geöffneten Fensters im Chart kennen, erhalten wir einen Bezugspunkt, zu dem wir die Höhe dieses Fensters addieren müssen. So erhalten wir die volle Höhe des gesamten Charts. Wir müssen nur noch prüfen, ob die Zeitskala im Chart vorhanden ist. Wenn die Skala vorhanden ist, fügen wir 15 Pixel zur berechneten Höhe hinzu (die Größe wurde probeweise gewählt). Wenn keine Skala vorhanden ist, wird nichts hinzugefügt. Auf diese Weise finden wir die Höhe des zukünftigen Screenshots.

Bei der Screenshot-Breite ist der Fall etwas einfacher — ermitteln Sie die Breite des Chartobjekts und addieren Sie 56 Pixel dazu, falls der Chart die Preisskala hat. Wenn keine Skala vorhanden ist, wird nichts hinzugefügt. Wenn es keine Skala gibt, fügen Sie nichts hinzu.

Auf Bildschirmen mit unterschiedlichen Auflösungen kann die Größe der Preis- und Zeitskala unterschiedlich sein. Ich hatte noch nicht die Gelegenheit, mit verschiedenen Monitoren und deren Auflösungen zu experimentieren. Wie auch immer, die Größe der Skalen, die zur Screenshot-Größe addiert werden, führen nicht zu erheblichen Fehlern im Erscheinungsbild des Bildes.

Die Methode, die den Chart-Screenshot von 800x600 Pixel erstellt:

//+------------------------------------------------------------------+
//| Create the chart screenshot of 800x600 pixels                    |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
   int width=800;
   int height=600;
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

Hier ist alles einfacher als bei der obigen Methode. Der Dateiname wird auf die gleiche Weise wie in der vorherigen Methode erstellt, und die Dateigröße ist hier hartcodiert. Die Größe wird an die Methode ScreenShot() übergeben und deren Ergebnis wird zurückgegeben.

Die Methode erstellt den Chart-Screenshot mit 750x562 Pixeln:

//+------------------------------------------------------------------+
//| Create the chart screenshot of 750x562 pixels                    |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
   int width=750;
   int height=562;
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

Die Methode ist ähnlich wie bei der Erstellung eines Screenshots mit 800х600 Pixeln, mit Ausnahme der Bildgröße.

Jeder Chart mit all seinen Einstellungen, Indikatoren und EA kann als Vorlage gespeichert werden, um später auf andere Charts angewendet zu werden. Es gibt zwei Methoden — zum Speichern der Chart-Vorlage und zum Anwenden einer bestimmten Vorlage auf den Chart, der durch das Chartobjekt beschrieben wird.

Die Methode zum Speichern der Chart-Vorlage mit den aktuellen Einstellungen:

//+------------------------------------------------------------------+
//| Save the chart template with the current settings                |
//+------------------------------------------------------------------+
bool CChartObj::SaveTemplate(const string filename=NULL)
  {
   ::ResetLastError();
   string name=
     (filename==NULL || filename=="" ? 
      TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) :  
      filename
     );
   if(!::ChartSaveTemplate(this.m_chart_id,name))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_SAVED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe()));
   return true;
  }
//+------------------------------------------------------------------+

Die Methode übergibt den Dateinamen, der beim Speichern der Vorlage verwendet werden soll. Wenn der Name leer ist (standardmäßig), wird er aus dem Pfad (vorgegeben in Defines.mqh) und einem Programmnamen gebildet.
Wenn die Vorlagendatei erfolgreich gespeichert wurde, wird der entsprechende Eintrag im Journal vorgenommen, der den Chart (Symbol und Zeitrahmen) angibt, dessen Vorlage gespeichert wurde, und true wird zurückgegeben. Wenn das Speichern der Vorlage fehlgeschlagen ist, wird die entsprechende Meldung ebenfalls an das Journal gesendet und die Methode gibt false zurück.

Die Methode wendet die angegebene Vorlage auf das Chart an:

//+------------------------------------------------------------------+
//| Apply the specified template to the chart                        |
//+------------------------------------------------------------------+
bool CChartObj::ApplyTemplate(const string filename=NULL)
  {
   ::ResetLastError();
   string name=
     (filename==NULL || filename=="" ? 
      TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) :  
      filename
     );
   if(!::ChartApplyTemplate(this.m_chart_id,name))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_APPLIED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe()));
   return true;
  }
//+------------------------------------------------------------------+

Die Methode ähnelt derjenigen, die die Chart-Vorlage speichert. Der Dateiname der Vorlage, der an die Methode übergeben oder automatisch erstellt wurde, wird ebenfalls verwendet. Anschließend wird die Vorlage auf den Chart angewendet und ein Eintrag über das Ergebnis der Operation im Journal angezeigt.

Beachten Sie, dass wenn wir die Vorlage mit einem anderen EA auf den aktuellen Chart mit einem laufenden EA anwenden, der die Methode aufgerufen hat, wird der aktuelle EA aus dem Speicher entfernt und funktioniert nicht mehr. Er wird durch einen neuen EA aus der Vorlage ersetzt. Die Methode prüft nicht die Möglichkeit einer solchen Kollision. Daher müssen wir unsere Templates überwachen und die Wahrscheinlichkeit prüfen, dass der aktuelle EA durch denjenigen ersetzt wird, der von dem auf den Chart angewendeten Template gestartet werden kann.

Die Methode konvertiert die X- und Y-Koordinaten des Charts im Fenster in Zeit- und Preiswerte:

//+------------------------------------------------------------------------------+
//|Convert X and Y coordinates of the chart window into the time and price values|
//+------------------------------------------------------------------------------+
int CChartObj::XYToTimePrice(const long x,const double y)
  {
   int sub_window=WRONG_VALUE;
   ::ResetLastError();
   if(!::ChartXYToTimePrice(this.m_chart_id,(int)x,(int)y,sub_window,this.m_wnd_time_x,this.m_wnd_price_y))
     {
      //CMessage::ToLog(DFUN,::GetLastError(),true);
      return WRONG_VALUE;
     }
   return sub_window;
  }
//+------------------------------------------------------------------+

Die Funktion ChartXYToTimePrice() konvertiert die X- und Y-Koordinaten eines Charts in die Zeit- und Preiswerte. Dabei erhält die ihr von der Verknüpfung übergebene Variable sub_window den Index des Subfensters, in dem sich die Chart X- und Y-Koordinaten befinden, für die Zeit und Preis zurückgegeben werden sollen.
Darauf basierend gibt die Methode den Index des Chart-Subfensters zurück: 0 — wenn sich die Koordinaten im Hauptchartfenster befinden, 1,2,3 usw. — wenn die Koordinaten in das entsprechende Chart-Unterfenster fallen, -1 — wenn die Berechnung der Koordinaten fehlgeschlagen ist. Wenn der nach dem Aufruf der Methode erhaltene Wert nicht -1 ist, ist es möglich, die Zeit und den Preis mit Hilfe der Methoden TimeFromXY() und PriceFromXY() zu erhalten, die einfach die Variablen zurückgeben, die die Zeit und den Preis enthalten, die durch die Funktion ChartXYToTimePrice() erhalten wurden.

Die Methode fügt der Screenshot-Datei eine Erweiterung hinzu, falls diese fehlt:

//+------------------------------------------------------------------+
//| Add an extension to the screenshot file if it is missing         |
//+------------------------------------------------------------------+
string CChartObj::FileNameWithExtention(const string filename)
  {
   if(::StringFind(filename,FILE_EXT_GIF)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_PNG)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_BMP)>WRONG_VALUE)
      return filename;
   return filename+SCREENSHOT_FILE_EXT;
  }
//+------------------------------------------------------------------+

Die Methode erhält die verifizierte Zeichenkette, in der wir die Erweiterung der Screenshot-Datei finden müssen. Da Screenshot-Dateiformate streng definiert sind und nur drei Typen — GIF, PNG und BMP — umfassen, gibt die Methode wenn der an die Methode übergebene String mindestens einen Teilstring mit einer solchen Erweiterung enthält (d. h. die Erweiterung ist bereits gesetzt), den übergebenen String unverändert zurück. Andernfalls erhält die Zeichenkette die Dateinamenerweiterung, die standardmäßig in Defines.mqh gesetzt ist. Dies ist eine .png-Datei. Die geänderte Zeichenkette wird als Ergebnis zurückgegeben.

Bei der Hinzufügung eines neuen Fensters zum Chart wurden einige Probleme festgestellt:
Bei einem ausführlichen Test wurde festgestellt, dass, wenn wir einen neuen Indikator im Fenster zum Chart hinzufügen, dessen Fenster erscheint (obwohl wir noch nicht auf ОК oder Abbrechen geklickt haben). Dieses Fenster wird im Terminal sofort als ein bereits vorhandenes Fenster gesehen. Die Bibliothek sieht es und fügt es der Fensterliste der Chartobjekte hinzu, während der Indikator nicht mehr im Fenster vorhanden ist. Wenn wir aber im Fenster des Hinzufügens eines neuen Indikators mit Fenster auf Abbrechen klicken, wird das Fenster nicht mehr in der Liste des Chart-Fensters des Client-Terminals vorhanden sein. Die Bibliothek entfernt das Fenster bei der nächsten Prüfung aus der Liste.

Um solche unnötigen Aktionen und den versehentlichen Zugriff auf ein leeres Chart-Fenster zu vermeiden, das im Client-Terminal nicht existiert, müssen wir sicherstellen, dass das Fenster den Indikator hat, bevor wir das Fenster zur Liste hinzufügen. Wenn der Indikator vorhanden ist, fügen wir das Fenster hinzu. Wenn er nicht vorhanden ist, ist das Fenster zwar geöffnet, aber nicht zum Chart hinzugefügt. Es sollte übersprungen werden.

So wird die Methode zum Erstellen der Chart-Fensterliste verbessert:

//+------------------------------------------------------------------+
//| Create the list of chart windows                                 |
//+------------------------------------------------------------------+
void CChartObj::CreateWindowsList(void)
  {
   //--- Clear the chart window list
   this.m_list_wnd.Clear();
   //--- Get the total number of chart windows from the environment
   int total=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL);
   //--- In the loop by the total number of windows
   for(int i=0;i<total;i++)
     {
      //--- Create a new chart window object
      CChartWnd *wnd=new CChartWnd(this.m_chart_id,i);
      if(wnd==NULL)
         continue;
      //--- If the window index exceeds 0 (not the main chart window) and it still has no indicator,
      //--- remove the newly created chart window object and go to the next loop iteration
      if(wnd.WindowNum()!=0 && wnd.IndicatorsTotal()==0)
        {
         delete wnd;
         continue;
        }
      //--- If the object was not added to the list, remove that object
      this.m_list_wnd.Sort();
      if(!this.m_list_wnd.Add(wnd))
         delete wnd;
     }
   //--- If the number of objects in the list corresponds to the number of windows on the chart,
   //--- write that value to the chart object property
   //--- If the number of objects in the list does not correspond to the number of windows on the chart,
   //--- write the number of objects in the list to the chart object property.
   int value=int(this.m_list_wnd.Total()==total ? total : this.m_list_wnd.Total());
   this.SetProperty(CHART_PROP_WINDOWS_TOTAL,value);
  }
//+------------------------------------------------------------------+

Die Logik der Methode ist in ihrem Code detailliert beschrieben. Die Prüfung auf die Notwendigkeit des Hinzufügens eines noch nicht erstellten Fensters zur Liste befindet sich im hervorgehobenen Codeblock. Mit anderen Worten, wenn wir nur einen Indikator mit Fenster hinzufügen, kann es sein, dass wir ein Fenster haben, das noch nicht erstellt wurde. Deshalb analysieren wir nicht das Fenster mit dem Index 0. Dies ist das Hauptfenster des Charts und es ist definitiv vorhanden. Nur neue Indikatoren können ihm hinzugefügt werden. Wenn ein neues Fenster ohne Indikator entdeckt wurde (was unmöglich ist, wenn das Indikatorfenster bereits zum Chart hinzugefügt wurde), dann ist dies das Fenster, bei dem wir noch nicht auf OK geklickt haben, um es zum Chart hinzuzufügen. Überspringen wir ein solches Fenster, um zu vermeiden, dass es der Fensterliste hinzugefügt wird.

Nach Beendigung der gesamten Schleife schreiben wir die Anzahl der Fenster in die Eigenschaft des Chartobjekts. Hier stellen wir sicher, dass alle Fenster erfolgreich zur Liste hinzugefügt wurden. Wenn ihre reale Anzahl gleich der Anzahl in der Liste ist, bedeutet dies, dass alle Fenster hinzugefügt wurden — schreiben wir die Anzahl der Fenster auf dem Chart in die Objekteigenschaft. Wenn die Zahlen nicht gleich sind, wird die in der Liste vorhandene Zahl in die Eigenschaft geschrieben (sie ist nicht gleich der tatsächlichen), so dass die nächste Prüfung wieder die Ungleichheit zeigt und wir das erstellte Fenster zur Liste hinzufügen können. Wenn wir beim Hinzufügen eines neuen Fensters auf Abbrechen klicken, werden die Anzahl der Fenster im Chart und in der Liste gleich, und es ist nicht nötig, eine Änderung zu behandeln.

Damit ist die Verbesserung der Bibliotheksklassen abgeschlossen.


Automatische Aktualisierung der Kollektion von Chartobjekten und Fenstern

Stellen wir nun sicher, dass bei jeder Änderung der Anzahl der geöffneten Charts, der Anzahl der Fenster auf den Charts und der Anzahl der Indikatoren in diesen Fenstern, die Bibliothek all diese Daten automatisch aktualisiert, so dass wir davon nicht abgelenkt werden, während wir immer die relevanten Daten haben.

Ergänzen wir in \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh der Klasse der Chartobjekte die Methode Refresh(), damit wir nicht nur die Änderung in der Anzahl der geöffneten Fenster auf dem Chart (im Chart-Objekt) überprüfen können, sondern auch die Anzahl der Indikatoren in bereits geöffneten Fenstern verwalten können (in ein Fenster passen mehrere Indikatoren).

Die Methode aktualisiert das Chart-Objekt und die Liste seiner Fenster:

//+------------------------------------------------------------------+
//| Update the chart object and its window list                      |
//+------------------------------------------------------------------+
void CChartObj::Refresh(void)
  {
   for(int i=0;i<m_list_wnd.Total();i++)
     {
      CChartWnd *wnd=m_list_wnd.At(i);
      if(wnd==NULL)
         continue;
      wnd.Refresh();
     }
   int change=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL)-this.WindowsTotal();
   if(change==0)
      return;
   this.CreateWindowsList();
  }
//+------------------------------------------------------------------+

Bevor wir die Änderungen in der Anzahl der geöffneten Fenster im Chart-Objekt überprüfen, sollten wir uns zunächst entlang der Liste aller Objektfenster bewegen und die Aktualisierungsmethode für jedes folgende Fenster in der Schleife aufrufen. Die Methode Refresh() des Chart-Fenster-Objekts prüft die Änderung der Anzahl der darin platzierten Indikatoren und erstellt deren Liste neu, wenn die Änderungen registriert werden.

In der Klassendatei der Chartobjekte \MQL5\Include\DoEasy\Collections\ChartObjCollection.mqh wurde der logische Fehler behoben, der uns daran hindert, die Chartobjekte in der Liste der Kollektion zu aktualisieren und dementsprechend ihre Fenster und Indikatoren darin zu aktualisieren.

Bisher befand sich der Block der Aktualisierung von Chartobjekten unterhalb der Prüfung auf Änderungen in der Anzahl der offenen Charts, wodurch wir ihn nicht erreichen konnten wenn es keine Änderungen in der Anzahl der offenen Charts gab:

//+------------------------------------------------------------------+
//| Update the collection list of chart objects                      |
//+------------------------------------------------------------------+
void CChartObjCollection::Refresh(void)
  {
   //--- Get the number of open charts in the terminal and
   int charts_total=this.ChartsTotal();
   //--- calculate the difference between the number of open charts in the terminal
   //--- and chart objects in the collection list. These values are displayed in the chart comment
   int change=charts_total-this.m_list.Total();
   Comment(DFUN,", list total=",DataTotal(),", charts total=",charts_total,", change=",change);
   //--- If there are no changes, leave
   if(change==0)
      return;
   //--- If a chart is added in the terminal
   if(change>0)
     {
      //--- Find the missing chart object, create and add it to the collection list
      this.FindAndCreateMissingChartObj();
      //--- Get the current chart and return to it since
      //--- adding a new chart switches the focus to it
      CChartObj *chart=this.GetChart(GetMainChartID());
      if(chart!=NULL)
         chart.SetBringToTopON(true);
     }
   //--- If a chart is removed in the terminal
   else if(change<0)
    {
     //--- Find an extra chart object in the collection list and remove it from the list
     this.FindAndDeleteExcessChartObj();
    }
   //--- In the loop by the number of chart objects in the list,
   for(int i=0;i<this.m_list.Total();i++)
     {
      //--- get the next chart object and
      CChartObj *chart=this.m_list.At(i);
      if(chart==NULL)
         continue;
      //--- update it
      chart.Refresh();
     }
  }
//+------------------------------------------------------------------+

Die Lösung ist ganz trivial — verschieben wir einfach den Codeblock für die Aktualisierung des Chartobjekts nach obenvor die Überprüfung der Anzahl der offenen Charts im Client-Terminal:

//+------------------------------------------------------------------+
//| Update the collection list of chart objects                      |
//+------------------------------------------------------------------+
void CChartObjCollection::Refresh(void)
  {
   //--- In the loop by the number of chart objects in the list,
   for(int i=0;i<this.m_list.Total();i++)
     {
      //--- get the next chart object and
      CChartObj *chart=this.m_list.At(i);
      if(chart==NULL)
         continue;
      //--- update it
      chart.Refresh();
     }
   //--- Get the number of open charts in the terminal and
   int charts_total=this.ChartsTotal();
   //--- calculate the difference between the number of open charts in the terminal
   //--- and chart objects in the collection list. These values are displayed in the chart comment
   int change=charts_total-this.m_list.Total();
   //--- If there are no changes, leave
   if(change==0)
      return;
   //--- If a chart is added in the terminal
   if(change>0)
     {
      //--- Find the missing chart object, create and add it to the collection list
      this.FindAndCreateMissingChartObj();
      //--- Get the current chart and return to it since
      //--- adding a new chart switches the focus to it
      CChartObj *chart=this.GetChart(GetMainChartID());
      if(chart!=NULL)
         chart.SetBringToTopON(true);
     }
   //--- If a chart is removed in the terminal
   else if(change<0)
    {
     //--- Find an extra chart object in the collection list and remove it from the list
     this.FindAndDeleteExcessChartObj();
    }
  }
//+------------------------------------------------------------------+

Bevor wir nun die Änderung in der Anzahl der geöffneten Charts überprüfen (und die Methode verlassen, wenn sich die Anzahl nicht geändert hat), durchlaufen wir zunächst eine Schleife durch alle Chart-Objekte in der Liste der Kollektion und überprüfen die Änderungen ihrer Fenster-Objekte in ihrer Methode Refresh(), in der ihre eigene Refresh()-Methode aufgerufen wird, um die Anzahl der Indikatoren im Fenster zu überprüfen. Wir führen also zunächst eine vollständige Prüfung aller möglichen Änderungen in der Anzahl der Indikatoren in Fenstern und in der Anzahl der Indikatoren in Charts durch und prüfen anschließend die Änderungen in der Anzahl der geöffneten Charts.

Fügen wir die Methoden zum Öffnen eines neuen Charts und zum Schließen eines bereits vorhandenen Charts in die Klasse der Chartobjekte Kollektion ein.
Deklarieren wir im öffentlichen Abschnitt der Klasse zwei neue Methoden:

//--- Update (1) the chart object collection list and (2) the specified chart object
   void                    Refresh(void);
   void                    Refresh(const long chart_id);

//--- (1) Open a new chart with the specified symbol and period, (2) close the specified chart
   bool                    Open(const string symbol,const ENUM_TIMEFRAMES timeframe);
   bool                    Close(const long chart_id);

  };
//+------------------------------------------------------------------+

Schreiben wir ihre Implementierung außerhalb des Klassenkörpers.

Die Methode öffnet einen neuen Chart mit dem angegebenen Symbol und Zeitraum:

//+------------------------------------------------------------------+
//| Open a new chart with the specified symbol and period            |
//+------------------------------------------------------------------+
bool CChartObjCollection::Open(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   if(this.m_list.Total()==CHARTS_MAX)
     {
      ::Print(CMessage::Text(MSG_CHART_COLLECTION_ERR_CHARTS_MAX)," (",(string)CHARTS_MAX,")");
      return false;
     }
   ::ResetLastError();
   long chart_id=::ChartOpen(symbol,timeframe);
   if(chart_id==0)
      CMessage::ToLog(::GetLastError(),true);
   return(chart_id>0);
  }
//+------------------------------------------------------------------+

Hier gilt: Wenn die Anzahl der Chartobjekte in der Kollektion den Schwellenwert (CHARTS_MAX) überschreitet, ist der Versuch, ein neues Chart zu öffnen, nutzlos. Darüber wird informiert und es wird false zurückgegeben. Wenn wir dann immer noch einen neuen Chart öffnen können, rufen wir die Funktion ChartOpen() mit den angegebenen Parametern des geöffneten Charts auf. Im Falle eines Fehlers gibt es einen Eintrag ins Journal. Rückgabe des Flags, das anzeigt, dass die Funktion zum Öffnen eines neuen Charts einen Wert ungleich Null zurückgegeben hat.

Die Methode schließt ein angegebenes Chart:

//+------------------------------------------------------------------+
//| Close a specified chart                                          |
//+------------------------------------------------------------------+
bool CChartObjCollection::Close(const long chart_id)
  {
   ::ResetLastError();
   bool res=::ChartClose(chart_id);
   if(!res)
      CMessage::ToLog(DFUN,::GetLastError(),true);
   return res;
  }
//+------------------------------------------------------------------+

Hier gilt: Wenn der Versuch, ein durch die ID angegebenes Chart zu schließen, nicht erfolgreich ist, erfolgt ein Eintrag im Journal.
Die Methode gibt das Ergebnis der Funktion ChartClose() zurück.

Fügen wir in \MQL5\Include\DoEasy\Engine.mqh des Hauptobjekts der CEngine-Bibliothek die Methoden zur Verwaltung der Chart-Kollektion hinzu.

Die zwei Methoden, die die Listen der Chartobjekte nach Symbol und Zeitrahmen zurückgeben

//--- Return the list of chart objects by (1) symbol and (2) timeframe
   CArrayObj           *ChartGetChartsList(const string symbol)                        { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *ChartGetChartsList(const ENUM_TIMEFRAMES timeframe)            { return this.m_charts.GetChartsList(timeframe);      }

werden umbenannt, um ähnlichen Methoden anderer Klassen zu entsprechen, und in der Auflistung etwas nach oben verschoben:

//--- Current the chart collection
   bool                 ChartCreateCollection(void)                                    { return this.m_charts.CreateCollection();            }
//--- Return (1) the chart collection and (2) the list of charts from the chart collection
   CChartObjCollection *GetChartObjCollection(void)                                    { return &this.m_charts;                              }
   CArrayObj           *GetListCharts(void)                                            { return this.m_charts.GetList();                     }
//--- Return the list of chart objects by (1) symbol and (2) timeframe
   CArrayObj           *GetListCharts(const string symbol)                             { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *GetListCharts(const ENUM_TIMEFRAMES timeframe)                 { return this.m_charts.GetChartsList(timeframe);      }

Fügen wir die Methode, die das Chart-Objekt des zuletzt geöffneten Charts zurückgibt, die Methode, die die Anzahl der Chart-Objekte in der Kollektion zurückgibt und zwei Methoden zum Öffnen und Schließen des angegebenen Charts hinzu:

//--- Current the chart collection
   bool                 ChartCreateCollection(void)                                    { return this.m_charts.CreateCollection();            }
//--- Return (1) the chart collection and (2) the list of charts from the chart collection
   CChartObjCollection *GetChartObjCollection(void)                                    { return &this.m_charts;                              }
   CArrayObj           *GetListCharts(void)                                            { return this.m_charts.GetList();                     }
//--- Return the list of chart objects by (1) symbol and (2) timeframe
   CArrayObj           *GetListCharts(const string symbol)                             { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *GetListCharts(const ENUM_TIMEFRAMES timeframe)                 { return this.m_charts.GetChartsList(timeframe);      }

//--- Return (1) the specified chart object, (2) the chart object with the program and (3) the chart object of the last open chart
   CChartObj           *ChartGetChartObj(const long chart_id)                          { return this.m_charts.GetChart(chart_id);            }
   CChartObj           *ChartGetMainChart(void)                                        { return this.m_charts.GetChart(this.m_charts.GetMainChartID());}
   CChartObj           *ChartGetLastOpenedChart(void)                                  { return this.m_charts.GetChart(this.GetListCharts().Total()-1);}
   
//--- Return the number of charts in the collection list
   int                  ChartsTotal(void)                                              { return this.m_charts.DataTotal();                   }

//--- Update (1) the chart specified by ID and (2) all charts
   void                 ChartRefresh(const long chart_id)                              { this.m_charts.Refresh(chart_id);                    }
   void                 ChartsRefreshAll(void)                                         { this.m_charts.Refresh();                            }

//--- (1) Open and (2) close the specified chart
   bool                 ChartOpen(const string symbol,const ENUM_TIMEFRAMES timeframe) { return this.m_charts.Open(symbol,timeframe);        }
   bool                 ChartClose(const long chart_id)                                { return this.m_charts.Close(chart_id);               }
   
//--- Return (1) the buffer collection and (2) the buffer list from the collection 

Die Methode ChartGetLastOpenedChart() liefert einfach den Zeiger auf das allerletzte Objekt, das sich in der Liste der Chart-Objekte in der Kollektion befindet, während die Methode ChartsTotal() die Größe der Liste der Chart-Objekte in der Kollektion zurückgibt.
Die Methoden ChartOpen() und ChartClose() geben die Ergebnisse der Methoden Open() und Close() der Chart Kollektion Klasse entsprechend zurück.

Dies sind alle Änderungen und Verbesserungen, die ich für den aktuellen Artikel geplant habe.


Test

Um den Test durchzuführen, verwenden wir den EA aus dem vorigen Artikel und speichern ihn in \MQL5\Experts\TestDoEasy\TestDoEasyPart70\ als TestDoEasyPart70.mq5.

Was soll wir tun? Fügen wir dem EA-Bedienfeld neue Schaltflächen mit den folgenden Symbolen hinzu:

  • "<" und ">" — die Schaltfläche zum Verschieben eines Charts um jeweils einen Balken nach links und rechts;

  • "<<" und ">>" — die Schaltfläche für die Verschiebung eines Charts um zehn Balken nach links bzw. nach rechts;

  • "|<" und ">|" — die Schaltfläche zum Setzen eines Charts an den Anfang bzw. an das Ende der Historie;

  • "N" und "X" — die Schaltfläche zum Öffnen eines neuen bzw. Schließen des zuletzt geöffneten Symbol-Charts;

  • "[O]" — die Schaltfläche zum Erstellen eines Screenshots des aktuellen Charts mit einem EA.

Die Logik des Testens einer neuen Funktionalität ist wie folgt:

  • Bei Klick auf die Schaltflächen zum Verschieben des Charts verschiebt der EA den Chart um einen Balken nach links bzw. um 10 Balken nach rechts.
  • Beim Anklicken der Schaltflächen zum Setzen eines Charts am Anfang und am Ende der Historie wird der Chart entsprechend gesetzt.
  • Beim Anklicken der Schaltfläche zum Öffnen des Charts öffnet der EA die Charts der in der Liste der Chartobjekte-Kollektion eingestellten Symbole nacheinander in der Reihenfolge, in der sie dort erscheinen, und nicht im Fenster Market Watch (da die Sortierung im Fenster Market Watch anders sein kann).
    Um die Chart-Vorlage zu speichern und anzuwenden, speichert der EA die Vorlage des zuletzt geöffneten Charts und wendet sie an, wenn ein neuer Chart geöffnet wird. Mit anderen Worten, wenn wir einen neuen Chart, z.B. GBPUSD, manuell öffnen und sein Aussehen anders konfigurieren als den aktuellen, bevor wir auf die Schaltfläche zum Öffnen eines neuen Charts klicken, werden alle nachfolgenden Charts das gleiche Aussehen haben. Um genauer zu sein, wird die Vorlage mit dem Aussehen des vordefinierten Charts nach dem Öffnen auf sie angewendet.
    Das bedeutet, dass wir, um den Test durchzuführen, zunächst einen neuen GBPUSD-Chart öffnen und sein Aussehen konfigurieren müssen. Die gespeicherte Vorlage, die auf dem manuell konfigurierten GBPUSD-Chart basiert, wird dann auf alle anderen Charts angewendet, die vom EA geöffnet werden.
  • Bei einem Klick auf die Schaltfläche "Chart schließen" schließt der EA den allerletzten der geöffneten Charts.
  • Bei sequentiellem Anklicken der Schaltfläche für einen Screenshot erstellt der EA Chart-Screenshots:
    sequentiell im Kreis in folgender Auflösung: 800х600 --> 750x562 --> Aktuelle Chartgröße.

Fügen wir in OnInit() des EA die Einbindung der Berechtigung zum Verfolgen von Mausereignissen für den aktuellen Chart hinzu:

//--- Check playing a standard sound by macro substitution and a custom sound by description
   engine.PlaySoundByDescription(SND_OK);
//--- Wait for 600 milliseconds
   engine.Pause(600);
   engine.PlaySoundByDescription(TextByLanguage("Звук упавшей монетки 2","Falling coin 2"));


//--- Check the calculation of the cursor coordinates in the chart windows.
//--- Allow the current chart to track mouse movement events
   engine.ChartGetMainChart().SetEventMouseMoveON();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Dies ermöglicht es dem Programm, Nachrichten über Maustastenbewegungen und Druckereignisse zu empfangen (CHARTEVENT_MOUSE_MOVE).

Beim Empfang eines Mausbewegungsereignisses müssen wir in OnChartEvent() die Cursor-Koordinaten auf dem Chart in Pixeln abrufen, sie mit Hilfe der erstellten Bibliotheksmethoden in Zeit, Preis und Index des Fensters, in dem sich der Cursor befindet, umwandeln und schließlich die neu gewonnene Zeit und den Preis wieder in die Cursor-Koordinaten auf dem Chart umwandeln und alle diese Werte im Chart-Kommentar anzeigen.

Schreiben wir die Ereignisbehandlung der Cursor-Bewegungen:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If working in the tester, exit
   if(MQLInfoInteger(MQL_TESTER))
      return;
//--- Handling mouse events
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Handle pressing the buttons in the panel
      if(StringFind(sparam,"BUTT_")>0)
         PressButtonEvents(sparam);
     }
//--- Handling DoEasy library events
   if(id>CHARTEVENT_CUSTOM-1)
     {
      OnDoEasyEvent(id,lparam,dparam,sparam);
     }
//--- Check ChartXYToTimePrice()
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Get the chart object of the current (main) program chart
      CChartObj *chart=engine.ChartGetMainChart();
      if(chart==NULL)
         return;
      //--- Get the index of a subwindow the cursor is located at
      int wnd_num=chart.XYToTimePrice(lparam,dparam);
      if(wnd_num==WRONG_VALUE)
         return;
      //--- Get the calculated cursor location time and price
      datetime time=chart.TimeFromXY();
      double price=chart.PriceFromXY();
      //--- Get the window object of the chart the cursor is located in by the subwindow index
      CChartWnd *wnd=chart.GetWindowByNum(wnd_num);
      if(wnd==NULL)
         return;
      //--- If X and Y coordinates are calculated by time and price (make a reverse conversion),
      if(wnd.TimePriceToXY(time,price))
        {
         //--- in the comment, show the time, price and index of the window that are calculated by X and Y cursor coordinates,
         //--- as well as the cursor X and Y coordinates converted back from the time and price
         Comment
           (
            DFUN,"time: ",TimeToString(time),", price: ",DoubleToString(price,Digits()),
            ", win num: ",(string)wnd_num,": x: ",(string)wnd.XFromTimePrice(),
            ", y: ",(string)wnd.YFromTimePrice()," (",(string)wnd.YFromTimePriceRelative(),")")
           ;
        }
     }
  }
//+------------------------------------------------------------------+

Die Logik der Behandlung eines Mausbewegungsereignisses ist im Code von OnChartEvent() des EA detailliert beschrieben.

Fügen wir in der Funktion CreateButtons() des EAs den Code für die Erstellung neuer Schaltflächen des Panels ein:

//+------------------------------------------------------------------+
//| Create the buttons panel                                         |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int h=18,w=82,offset=2,wpt=14;
   int cx=offset+shift_x+wpt*2+2,cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1;
   int x=cx,y=cy;
   int shift=0;
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==7 ? w+2 : 0);
      if(i==TOTAL_BUTT-6) x=cx;
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-6 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text);
         return false;
        }
     }
   
   h=18; offset=2;
   cx=offset+shift_x; cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1;
   x=cx; y=cy;
   shift=0;
   for(int i=0;i<18;i++)
     {
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name+"_PRICE",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5 : x),y,wpt,h,"P",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"P\"");
         return false;
        }
      if(!ButtonCreate(butt_data[i].name+"_TIME",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5+wpt+1 : x+wpt+1),y,wpt,h,"T",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"T\"");
         return false;
        }
     }
   //--- Left and Right buttons
   int xbn=x+wpt*2+w*2+5;
   int ybn=y+h*3+3;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT1",xbn,ybn,wpt,h,"<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT1");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT1",xbn+wpt+1,ybn,wpt,h,">",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT1");
      return false;
     }
   //--- Left 10 and Right 10 buttons
   ybn=y+h*2+2;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT10",xbn,ybn,wpt,h,"<<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT10");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT10",xbn+wpt+1,ybn,wpt,h,">>",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT10");
      return false;
     }
   //--- Home and End buttons
   ybn=y+h+1;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_HOME",xbn,ybn,wpt,h,"|<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_HOME");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_END",xbn+wpt+1,ybn,wpt,h,">|",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_END");
      return false;
     }
   //--- Open and Close buttons
   ybn=y;
   if(!ButtonCreate(prefix+"BUTT_CHART_OPEN",xbn,ybn,wpt,h,"N",clrBlue))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_OPEN");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_CHART_CLOSE",xbn+wpt+1,ybn,wpt,h,"X",clrRed))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_CLOSE");
      return false;
     }
   //--- ScreenShot button
   ybn=y-h-1;
   if(!ButtonCreate(prefix+"BUTT_CHART_SCREENSHOT",xbn,ybn,wpt*2+offset,h,"[O]",clrBlue))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_SCREENSHOT");
      return false;
     }
   
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

Hier ist alles einfach: Koordinaten werden für jede neue Schaltfläche berechnet und die Schaltfläche wird mit der Funktion ButtonCreate() erstellt, die einen Namen eines erstellten grafischen Objekts, seine Koordinaten, Breite, Höhe, Beschriftung und Farbe erhält. Wenn das Erstellen der Schaltfläche fehlschlägt, wird die Warnung aktiviert und false zurückgegeben. In OnInit(), wenn die Funktion zur Erstellung des Panels false zurückgibt, mit dem Rückgabewert INIT_FAILED verlassen.

Fügen wir in der Funktion PressButtonEvents() zur Behandlung von Tastenklicks die Behandlung neuer Tastenklicks hinzu:

//+------------------------------------------------------------------+
//| Handle pressing the buttons                                      |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   bool comp_magic=true;   // Temporary variable selecting the composite magic number with random group IDs
   string comment="";
   //--- Convert button name into its string ID
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- Random group 1 and 2 numbers within the range of 0 - 15
   group1=(uchar)Rand();
   group2=(uchar)Rand();
   uint magic=(comp_magic ? engine.SetCompositeMagicNumber(magic_number,group1,group2) : magic_number);
   //--- If the button is pressed
   if(ButtonState(button_name))
     {
      //--- If the button of shifting a chart 1 bar to the left is clicked
      if(button=="BUTT_NAVIGATE_LEFT1")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateLeft(1);
        }
      //--- If the button of shifting a chart 1 bar to the right is clicked
      if(button=="BUTT_NAVIGATE_RIGHT1")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateRight(1);
        }
      //--- If the button of shifting a chart 10 bars to the left is clicked
      if(button=="BUTT_NAVIGATE_LEFT10")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateLeft(10);
        }
      //--- If the button of shifting a chart 10 bars to the right is clicked
      if(button=="BUTT_NAVIGATE_RIGHT10")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateRight(10);
        }
      //--- If the button of shifting a chart to the start of history is clicked
      if(button=="BUTT_NAVIGATE_HOME")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateBegin();
        }
      //--- If the button of shifting a chart to the end of history is clicked
      if(button=="BUTT_NAVIGATE_END")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateEnd();
        }
      //--- If the new chart open button is pressed
      if(button=="BUTT_CHART_OPEN")
        {
         int total_charts=engine.ChartsTotal();
         static int first_index=total_charts;
         string name=SymbolName(total_charts-first_index,true);
         if(engine.ChartOpen(name,PERIOD_CURRENT))
           {
            engine.ChartsRefreshAll();
            CChartObj *chart=engine.ChartGetMainChart();
            if(chart!=NULL)
               chart.SetBringToTopON(true);
           }
         //--- This code block is needed only for the test and only if there is an open GBPUSD chart
         //--- GBPUSD chart settings should differ from that of charts opened by default
         CArrayObj *list_gbpusd=engine.GetListCharts("GBPUSD");
         if(list_gbpusd!=NULL && list_gbpusd.Total()>0)
           {
            CChartObj *chart=list_gbpusd.At(0);
            if(chart.SaveTemplate())
              {
               chart=engine.ChartGetLastOpenedChart();
               if(chart!=NULL)
                  chart.ApplyTemplate();
              }
           }
         //--- End of the test code block
        }
      //--- If the the last chart close button is pressed
      if(button=="BUTT_CHART_CLOSE")
        {
         CArrayObj *list_charts=engine.GetListCharts();
         if(list_charts!=NULL)
           {
            list_charts.Sort(SORT_BY_CHART_ID);
            CChartObj *chart=list_charts.At(list_charts.Total()-1);
            if(chart!=NULL && !chart.IsMainChart())
              engine.ChartClose(chart.ID());
           }
        }
      //--- If the ScreenShot button is pressed
      if(button=="BUTT_CHART_SCREENSHOT")
        {
         static int num=0;
         if(++num>3) num=1;
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
           {
            switch(num)
              {
               case 1 : chart.ScreenShot800x600(); break;
               case 2 : chart.ScreenShot750x562(); break;
               default: chart.ScreenShotWndSize(); break;
              }
           }
        }
      
      //--- If the 'BUTT_BUY: Open Buy position' is pressed
      if(button==EnumToString(BUTT_BUY))
        {
         ...
         ...
         ...
       ...
       ...
     ...
     ...

Anstatt den gesamten Funktionscode anzuzeigen, werden nur die hinzugefügten Änderungen angezeigt.
Hier behandeln wir einfach das Anklicken jeder neuen Schaltfläche. Die Logik ist einfach und es macht keinen Sinn, sie zu beschreiben. Ich überlasse sie einer eigenständigen Analyse, zumal dieser Handler nur als Beispiel dafür dient, wie Sie mit Bibliotheksmethoden in Ihren Programmen arbeiten können. Sie können jedoch gerne den Kommentarbereich nutzen, wenn Sie Fragen zum Code haben.

Dies sind alle Verbesserungen, die ich in dem neuen Test-EA vornehmen musste.

Kompilieren Sie den EA und starten Sie ihn im Chart mit den Einstellungen "Work only with the current Symbol" und "Work only with the current timeframe" ("Nur mit dem aktuellen Symbol arbeiten" und "Nur mit dem aktuellen Zeitrahmen arbeiten"):


Bevor Sie den EA starten, stellen Sie sicher, dass Sie einen neuen Chart des GBPUSD-Symbols öffnen und sein Aussehen anders konfigurieren als die Charts, die standardmäßig geöffnet werden, wenn Sie die Vorlage default.tpl verwenden, z. B. auf folgende Weise (der GBPUSD-Chart wurde im Voraus geöffnet):



Jetzt können wir die neue Funktionalität der Bibliothek testen, indem wir auf die Schaltflächen des Panels klicken:


Jedes Mal, wenn ein neuer Chart geöffnet wurde, speicherte der EA die Vorlage eines zuvor konfigurierten GBPUSD-Symbol-Charts und wurde sie sofort auf jeden neu geöffneten Chart angewendet, indem die entsprechenden Journaleinträge vorgenommen wurden:

CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: USDCHF H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: GBPUSD H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURUSD H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: USDRUB H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURJPY H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURGBP H1
CChartObjCollection::Close: Wrong chart ID (4101)

Das Schließen der Charts erzeugt einen Fehler. Die Bibliothek aktualisiert den Status der offenen Charts einmal pro halbe Sekunde.
Dies ist so in der Datei Defines.mqh eingestellt:

//--- Parameters of the chart collection timer
#define COLLECTION_CHARTS_PAUSE        (500)                      // Chart collection timer pause in milliseconds
#define COLLECTION_CHARTS_COUNTER_STEP (16)                       // Chart timer counter increment
#define COLLECTION_CHARTS_COUNTER_ID   (9)                        // Chart timer counter ID

Ich habe die Schaltfläche zum Schließen des letzten geöffneten Charts schneller als zweimal pro Sekunde angeklickt, so dass versucht wurde, den bereits geschlossenen vorherigen Chart (der noch den entsprechenden Eintrag in der Liste der Chart Kollektion hatte) zu schließen. Die Häufigkeit der Aktualisierung des aktuellen Zustands der geöffneten Charts, ihrer Fenster und Indikatoren kann durch Änderung der angegebenen Makro-Substitution angepasst werden. Verringern Sie den Wert der Konstante, um die Umgebung schneller zu aktualisieren. In diesem Fall steigt die Belastung der CPU durch eine höhere Aktualisierungsfrequenz. Es ist wichtig, hier die "goldene Mitte" zu finden, da diese Funktionalität immer noch für die manuelle Steuerung von Charts gedacht ist, so dass die Häufigkeit der Aktualisierungen anpassbar ist. Gelegentlich auftretende Fehler beim Versuch, auf fehlende Charts zuzugreifen, sind unkritisch. Drücken Sie einfach ein zweites Mal auf die Schaltfläche, wenn das nächste Umgebungsupdate stattfindet, und die Liste der Chart-Objekte wird in der Bibliothek mit dem Status der Chart-Objekte im Client-Terminal synchronisiert.

Nun ist es an der Zeit, das Erstellen von Screenshots des aktuellen Charts zu testen. Jeder Schaltflächenklick erzeugt einen Chart-Screenshot in einer bestimmten Größe. Erster Klick — 800x600, zweiter — 750x562, dritter — die aktuelle Chart-Größe:


Nach dem Erstellen von drei Screenshots in verschiedenen Auflösungen (begleitet von den entsprechenden Journaleinträgen),

CChartObj::ScreenShot800x600: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.25.png (800 x 600)
CChartObj::ScreenShot750x562: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.28.png (750 x 562)
CChartObj::ScreenShotWndSize: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.29.png (726 x 321)

haben wir uns auch den Inhalt des Ordners angesehen, in dem diese Screenshots gespeichert sind.
Bevor ich die Screenshots erstellt habe, habe ich den Cursor über verschiedene Bereiche von zwei Fenstern des aktuellen Charts bewegt, und der Chart-Kommentar zeigt die Zeit, den Preis und den Index des Teilfensters sowie die Cursor-X/Y-Koordinaten in Pixel an. Die Cursor-Y-Koordinate hat zwei Werte. Der erste Wert zeigt die Y-Koordinate der Anfangskoordinaten des Symbolhauptfensters an. Der zweite angezeigte Wert (in Klammern) zeigt die Y-Koordinate relativ zum oberen Rand des Fensters an, in dem sich der Cursor befindet.

Wie wir sehen, funktioniert die ganze Funktionsweise, die für den aktuellen Artikel geplant ist, einwandfrei.


Was kommt als Nächstes?

Im nächsten Artikel werde ich die automatische Verfolgung von Ereignissen zur Änderung der Eigenschaften der Chart-Objekte und ihrer Fenster implementieren.

Alle Dateien der aktuellen Version der Bibliothek sind unten zusammen mit der Test-EA-Datei für MQL5 zum Testen und Herunterladen angehängt.
Ihre Fragen und Vorschläge schreiben Sie bitte in den Kommentarteil.

Zurück zum Inhalt

*Frühere Artikel dieser Serie:

Andere Klassen in der Bibliothek DoEasy (Teil 67): Objektklasse der Charts
Andere Klassen in der Bibliothek DoEasy (Teil 68): Die Chartfenster-Objektklasse und die Indikator-Objektklassen im Chartfenster
Andere Klassen in der Bibliothek DoEasy (Teil 69): Kollektionsklasse der Chart-Objekte

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

Beigefügte Dateien |
MQL5.zip (3955.46 KB)
Andere Klassen in der Bibliothek DoEasy (Teil 71): Ereignisse der Kollektion von Chartobjekten Andere Klassen in der Bibliothek DoEasy (Teil 71): Ereignisse der Kollektion von Chartobjekten
In diesem Artikel werde ich die Funktionalität für die Verfolgung einiger Ereignisse von Chartobjekten erstellen — Hinzufügen/Entfernen von Symbolcharts und Chart-Unterfenstern, sowie Hinzufügen/Entfernen/Ändern von Indikatoren in Chart-Fenstern.
Tipps von einem professionellen Programmierer (Teil I): Code speichern, debuggen und kompilieren. Arbeiten mit Projekten und Protokollen Tipps von einem professionellen Programmierer (Teil I): Code speichern, debuggen und kompilieren. Arbeiten mit Projekten und Protokollen
Dies sind einige Tipps von einem professionellen Programmierer über Methoden, Techniken und Hilfsmittel, die das Programmieren erleichtern können.
Swaps (Teil I): Locking und synthetische Positionen Swaps (Teil I): Locking und synthetische Positionen
In diesem Artikel werde ich versuchen, das klassische Konzept der Swap-Handelsmethoden zu erweitern. Ich werde erklären, warum ich zu dem Schluss gekommen bin, dass dieses Konzept besondere Aufmerksamkeit verdient und unbedingt zum Studium empfohlen wird.
Combination Scalping: Analyse von Positionen aus der Vergangenheit, um die Performance zukünftiger Positionen zu steigern Combination Scalping: Analyse von Positionen aus der Vergangenheit, um die Performance zukünftiger Positionen zu steigern
Der Artikel beschreibt die Technologie, die darauf abzielt, die Effektivität jedes automatisierten Handelssystems zu erhöhen. Er bietet eine kurze Erläuterung der Idee, sowie die zugrundeliegenden Grundlagen, Möglichkeiten und Nachteile.