English Русский 中文 Español 日本語 Português
preview
Erstellung eines Dashboards zur Anzeige von Daten in Indikatoren und EAs

Erstellung eines Dashboards zur Anzeige von Daten in Indikatoren und EAs

MetaTrader 5Beispiele | 22 Januar 2024, 10:52
428 0
Artyom Trishkin
Artyom Trishkin

Inhalt


Einführung

In diesem Artikel werde ich ein Dashboard erstellen, das vom Entwickler festgelegte Daten anzeigen kann. Ein solches Panel eignet sich für die visuelle Darstellung von Daten in einem Chart und für das visuelle Debugging, da es bequemer ist, die erforderlichen Werte auf dem Panel zu sehen als sie im Debugger zu verfolgen. Ich meine Fälle, in denen die Strategie in Abhängigkeit von einigen Datenwerten debuggt wird.

Ich werde das Panel in Form eines Prototyps des Terminal-Datenfensters erstellen und es mit denselben Daten füllen:


Abb. 1. Datenfenster und Dashboard

Mit unserem nutzerdefinierten Panel können wir jede beliebige Menge der erforderlichen Daten hinzufügen, es signieren und die Messwerte über den Programmcode anzeigen und aktualisieren.

Es sollte möglich sein, das Panel mit der Maus auf dem Chart zu verschieben, es an der gewünschten Position anzudocken und es zu komprimieren/erweitern. Um die Daten bequem auf der Tafel zu platzieren, kann eine Tabelle mit der angegebenen Anzahl von Zeilen und Spalten angezeigt werden. Die Daten dieser Tabelle können im Journal angezeigt werden (X- und Y-Koordinaten jeder Tabellenzelle) und programmatisch abgerufen werden, um den Index der Zeile und der Spalte anzugeben, in der sich diese Daten befinden sollen, oder man kann die Koordinaten einfach ausdrucken und die gewünschten Koordinaten in den Code eingeben, direkt bei der Anzeige von Text und Daten. Die erste Methode ist bequemer, da sie vollständig automatisiert ist. Das Panel wird auch eine aktive Schließtaste haben, aber wir delegieren deren Handhabung an das Steuerprogramm, da nur der Programmentwickler entscheiden sollte, wie er auf das Drücken der Schließtaste reagiert. Wenn eine Schaltfläche angeklickt wird, wird ein nutzerdefiniertes Ereignis an die Ereignisbehandlung des Programms gesendet. Der Entwickler kann sie nach eigenem Ermessen bearbeiten.


Klassen zum Erhalt der Tabellendaten

Da es zweckmäßig ist, die Daten auf der Tafel nach einigen visuell oder virtuell vorgegebenen Koordinaten anzuordnen, werden wir zunächst Klassen für die Anordnung von Tabellendaten erstellen. Die Tabelle kann als einfaches Gitter dargestellt werden, dessen Linienschnittpunkte die Koordinaten der Tabellenzellen sind. Es wird möglich sein, beliebige visuelle Daten an diesen Koordinaten zu platzieren. Die Tabelle hat eine bestimmte Anzahl von Zeilen (horizontale Linien) und jede Zeile hat eine bestimmte Anzahl von Zellen (vertikale Linien). In einer einfachen Gittertabelle haben alle Zeilen die gleiche Anzahl von Zellen.

Auf dieser Grundlage benötigen wir drei Klassen:

  1. Klasse der Tabellenzellen,
  2. Klasse der Tabellenzeile,
  3. Tabellenklasse.

Die Klasse der Tabellenzelle enthält den Zeilen- und Spaltenindex in der Tabelle und die Koordinaten der visuellen Position der Tabellenzelle im Panel - die X- und Y-Koordinaten relativ zum Tabellenursprung in der oberen linken Ecke des Panels.

Die Tabellenzeilenklasse umfasst die Tabellenzellenklasse. Wir können die erforderliche Anzahl von Zellen in einer Zeile erstellen.

Die Tabellenklasse enthält eine Liste von Tabellenzeilen. Die Zeilen in der Tabelle können in der gewünschten Anzahl erstellt und hinzugefügt werden.

Werfen wir einen kurzen Blick auf alle drei Klassen.


Klasse der Tabellenzelle

//+------------------------------------------------------------------+
//| Table cell class                                                 |
//+------------------------------------------------------------------+
class CTableCell : public CObject
  {
private:
   int               m_row;                     // Row
   int               m_col;                     // Column
   int               m_x;                       // X coordinate
   int               m_y;                       // Y coordinate
public:
//--- Methods of setting values
   void              SetRow(const uint row)     { this.m_row=(int)row;  }
   void              SetColumn(const uint col)  { this.m_col=(int)col;  }
   void              SetX(const uint x)         { this.m_x=(int)x;      }
   void              SetY(const uint y)         { this.m_y=(int)y;      }
   void              SetXY(const uint x,const uint y)
                       {
                        this.m_x=(int)x;
                        this.m_y=(int)y;
                       }
//--- Methods of obtaining values
   int               Row(void)            const { return this.m_row;    }
   int               Column(void)         const { return this.m_col;    }
   int               X(void)              const { return this.m_x;      }
   int               Y(void)              const { return this.m_y;      }
//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableCell *compared=node;
                        return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0);
                       }
//--- Constructor/destructor
                     CTableCell(const int row,const int column) : m_row(row),m_col(column){}
                    ~CTableCell(void){}
  };

Die Klasse wird von der Basisklasse für den Aufbau der MQL5-Standardbibliothek geerbt, da sie in CArrayObj-Listen der MQL5-Standardbibliothek passt. Die Listen können nur CObject-Objekte oder vom Basis-CObject geerbte Objekte enthalten.

Die Funktion aller Variablen und Methoden ist sehr transparent und leicht verständlich. Die Variablen werden verwendet, um die Werte einer Tabellenzeile (Row) und -spalte (Column) zu speichern, während die Koordinaten die relativen Koordinaten der oberen linken Ecke der Tabellenzelle im Panel sind. Anhand dieser Koordinaten können Sie etwas zeichnen oder auf der Tafel platzieren.

Die virtuelle Methode Compare wird benötigt, um zwei Tabellenzellenobjekte zu finden und zu vergleichen. Die Methode wird in der Basisklasse CObject deklariert:

   //--- method of comparing the objects
   virtual int       Compare(const CObject *node,const int mode=0) const { return(0);      }

Sie gibt Null zurück und sollte in abgeleiteten Klassen überschrieben werden.

Da Tabellenzellen an die Tabellenzeile angehängt werden, d.h. visuell horizontal, sollte die Suche und der Vergleich nach horizontalen Zellennummern erfolgen - nach dem Spaltenwert. Dies ist genau das, was die überschriebene virtuelle Vergleichsmethode hier tut:

//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableCell *compared=node;
                        return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0);
                       }

Wenn der Spaltenwert des aktuellen Objekts größer ist als der des zu vergleichenden Objekts (dessen Zeiger an die Methode übergeben wird), wird 1 zurückgegeben. Ist der Spaltenwert kleiner als der der zu vergleichenden Spalte, so wird -1 zurückgegeben. Andernfalls wird true zurückgegeben. Der von der Methode zurückgegebene Wert Null bedeutet also, dass die Werte der zu vergleichenden Objekte gleich sind.


Klasse der Tabellenzeile

Die Zellenobjekte werden der Tabellenzeile hinzugefügt. Wenn die Zellen in einer Zeile horizontal nebeneinander liegen, sind die Zeilen in der Tabelle vertikal untereinander angeordnet.
In diesem Fall müssen wir nur den Zeilenindex und die Y-Koordinate auf der Tafel kennen:

//+------------------------------------------------------------------+
//| Table row class                                                  |
//+------------------------------------------------------------------+
class CTableRow : public CObject
  {
private:
  CArrayObj          m_list_cell;               // Cell list
  int                m_row;                     // Row index
  int                m_y;                       // Y coordinate
public:
//--- Return the list of table cells in a row
   CArrayObj        *GetListCell(void)       { return &this.m_list_cell;         }
//--- Return (1) the number of table cells in a row (2) the row index in the table
   int               CellsTotal(void)  const { return this.m_list_cell.Total();  }
   int               Row(void)         const { return this.m_row;                }
//--- (1) Set and (2) return the Y row coordinate
   void              SetY(const int y)       { this.m_y=y;                       }
   int               Y(void)           const { return this.m_y;                  }
//--- Add a new table cell to the row
   bool              AddCell(CTableCell *cell)
                       {
                        this.m_list_cell.Sort();
                        if(this.m_list_cell.Search(cell)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        if(!this.m_list_cell.InsertSort(cell))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        return true;
                       }
//--- Return the pointer to the specified cell in the row
   CTableCell       *GetCell(const int column)
                       {
                        const CTableCell *obj=new CTableCell(this.m_row,column);
                        int index=this.m_list_cell.Search(obj);
                        delete obj;
                        return this.m_list_cell.At(index);
                       }
//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableRow *compared=node;
                        return(this.Row()>compared.Row() ? 1 : this.Row()<compared.Row() ? -1 : 0);
                       }
//--- Constructor/destructor
                     CTableRow(const int row) : m_row(row)  { this.m_list_cell.Clear();   }
                    ~CTableRow(void)                        { this.m_list_cell.Clear();   }
  };

Die CArrayObj-Liste, die neu hinzugefügte Zellenobjekte enthält, wird in der Klasse deklariert.

In der virtuellen Methode Compare werden die Objekte anhand des Zeilenindexwerts (Row) verglichen, da wir beim Hinzufügen einer neuen Zeile nur nach dem Zeilenindex suchen müssen. Wenn keine Zeile mit einem solchen Index gefunden wird, gibt die Suchmethode(Search) -1 zurück, andernfalls gibt die Suche den Index der Position des gefundenen Objekts in der Liste zurück. Die Methode Search wird in der Klasse CArrayObj deklariert und implementiert:

//+------------------------------------------------------------------+
//| Search of position of element in a sorted array                  |
//+------------------------------------------------------------------+
int CArrayObj::Search(const CObject *element) const
  {
   int pos;
//--- check
   if(m_data_total==0 || !CheckPointer(element) || m_sort_mode==-1)
      return(-1);
//--- search
   pos=QuickSearch(element);
   if(m_data[pos].Compare(element,m_sort_mode)==0)
      return(pos);
//--- not found
   return(-1);
  }

Wie wir sehen können, wird die virtuelle Compare-Methode zum Vergleich zweier Objekte verwendet, um die Gleichheit der Objekte festzustellen.


Die Methode, die eine neue Zelle zur Liste hinzufügt:

//--- Add a new table cell to the row
   bool              AddCell(CTableCell *cell)
                       {
                        this.m_list_cell.Sort();
                        if(this.m_list_cell.Search(cell)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        if(!this.m_list_cell.InsertSort(cell))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        return true;
                       }

Da die Zellen in der Liste strikt nach Spaltennummern (Column) geordnet sind und wir sie in Sortierreihenfolge hinzufügen, sollte in der Liste zuerst das Flag für sortierte Liste gesetzt werden. Wenn die Suche nicht -1 ergibt, ist ein solches Objekt bereits in der Liste vorhanden. Die entsprechende Meldung wird an das Protokoll gesendet, und false wird zurückgegeben. Wenn es nicht gelungen ist, den Objektzeiger zur Liste hinzuzufügen, wird dies ebenfalls gemeldet und false zurückgegeben. Wenn alles in Ordnung ist, wird true zurückgegeben.


Die Methode, die den Zeiger auf die angegebene Zelle in der Zeile zurückgibt:

//--- Return the pointer to the specified cell in the row
   CTableCell       *GetCell(const int column)
                       {
                        const CTableCell *obj=new CTableCell(this.m_row,column);
                        int index=this.m_list_cell.Search(obj);
                        delete obj;
                        return this.m_list_cell.At(index);
                       }

Die Methode Search der Klasse CArrayObj der Standardbibliothek sucht in der Liste nach Gleichheit auf der Grundlage der Instanz des Objekts, dessen Zeiger an die Methode übergeben wird. Daher erstellen wir hier ein neues temporäres Objekt unter Angabe der Spaltennummer, die der Methode in ihrem Konstruktor übergeben wird, und erhalten den Objektindex in der Liste oder -1, wenn kein Objekt mit solchen Parametern in der Liste gefunden wird. Außerdem stellen wir sicher, dass wir ein temporäres Objekt löschen und den Zeiger auf das gefundene Objekt in der Liste zurückgeben.
Wenn das Objekt nicht gefunden wird und der Index gleich -1 ist, gibt die Methode At der Klasse CArrayObj NULL zurück.


Tabelle Klasse

Die Tabelle besteht aus einer Liste von Zeilen, die ihrerseits aus Zelllisten bestehen. Mit anderen Worten, die Tabellendatenklasse enthält nur das Objekt CArrayObj, in das die zu erstellenden Zeilen sowie die Methoden zum Hinzufügen und Empfangen von Tabellenzeilen und Zellen eingefügt werden:

//+------------------------------------------------------------------+
//| Table data class                                                 |
//+------------------------------------------------------------------+
class CTableData : public CObject
  {
private:
   CArrayObj         m_list_rows;               // List of rows
public:
//--- Return the list of table rows
   CArrayObj        *GetListRows(void)       { return &this.m_list_rows;   }
//--- Add a new row to the table
   bool              AddRow(CTableRow *row)
                       {
                        //--- Set the sorted list flag
                        this.m_list_rows.Sort();
                        //--- If such an object is already in the list (the search returns the object index, not -1),
                        //--- inform of that in the journal and return 'false'
                        if(this.m_list_rows.Search(row)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table row with index %lu is already in the list",__FUNCTION__,row.Row());
                           return false;
                          }
                        //--- If failed to add the pointer to the sorted list, inform of that and return 'false'
                        if(!this.m_list_rows.InsertSort(row))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,row.Row());
                           return false;
                          }
                        //--- Successful - return 'true'
                        return true;
                       }
//--- Return the pointer to the (1) specified row and (2) specified cell in the specified table row
   CTableRow        *GetRow(const int index) { return this.m_list_rows.At(index);   }
   CTableCell       *GetCell(const int row,const int column)
                       {
                        //--- Get a pointer to a string object in a list of strings
                        CTableRow *row_obj=this.GetRow(row);
                        //--- If failed to get the object, return NULL
                        if(row_obj==NULL)
                           .return NULL;
                        //--- Get the pointer to the cell object in the row by a column number and
                        CTableCell *cell=row_obj.GetCell(column);
                        //--- return the result (object pointer or NULL)
                        return cell;
                       }
//--- Write the X and Y coordinates of the specified table cell into the variables passed to the method
   void              CellXY(const uint row,const uint column, int &x, int &y)
                       {
                        x=WRONG_VALUE;
                        y=WRONG_VALUE;
                        CTableCell *cell=this.GetCell(row,column);
                        if(cell==NULL)
                           return;
                        x=cell.X();
                        y=cell.Y();
                       }
//--- Return the X coordinate of the specified table cell
   int               CellX(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.X() : WRONG_VALUE);
                       }
//--- Return the Y coordinate of the specified table cell
   int               CellY(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.Y() : WRONG_VALUE);
                       }
//--- Return the number of table (1) rows and (2) columns
   int               RowsTotal(void)            { return this.m_list_rows.Total();  }
   int               ColumnsTotal(void)
                       {
                        //--- If there is no row in the list, return 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Get a pointer to the first row and return the number of cells in it
                        CTableRow *row=this.GetRow(0);
                        return(row!=NULL ? row.CellsTotal() : 0);
                       }
//--- Return the total number of cells in the table
   int               CellsTotal(void){ return this.RowsTotal()*this.ColumnsTotal(); }
//--- Clear lists of rows and table cells
   void              Clear(void)
                       {
                        //--- In the loop by the number of rows in the list of table rows, 
                        for(int i=0;i<this.m_list_rows.Total();i++)
                          {
                           //--- get the pointer to the next row
                           CTableRow *row=this.m_list_rows.At(i);
                           if(row==NULL)
                              continue;
                           //--- get cell list from the obtained row object,
                           CArrayObj *list_cell=row.GetListCell();
                           //--- clear cell list
                           if(list_cell!=NULL)
                              list_cell.Clear();
                          }
                        //--- Clear cell list
                        this.m_list_rows.Clear();
                       }                
//--- Print the table cell data in the journal
   void              Print(const uint indent=0)
                       {
                        //--- Print the header in the journal
                        ::PrintFormat("Table: Rows: %lu, Columns: %lu",this.RowsTotal(),this.ColumnsTotal());
                        //--- In the loop by table rows
                        for(int r=0;r<this.RowsTotal();r++)
                           //--- in the loop by the next row cells,
                           for(int c=0;c<this.ColumnsTotal();c++)
                             {
                              //--- get the pointer to the next cell and display its data in the journal
                              CTableCell *cell=this.GetCell(r,c);
                              if(cell!=NULL)
                                 ::PrintFormat("%*s%-5s %-4lu %-8s %-6lu %-8s %-6lu %-8s %-4lu",indent,"","Row",r,"Column",c,"Cell X:",cell.X(),"Cell Y:",cell.Y());
                             }
                       }
//--- Constructor/destructor
                     CTableData(void)  { this.m_list_rows.Clear();   }
                    ~CTableData(void)  { this.m_list_rows.Clear();   }
  };

Fast alle Methoden hier sind im Code kommentiert. Ich werde nur auf die Methoden eingehen, die die Anzahl der Zeilen und Spalten in der Tabelle sowie die Gesamtzahl der Tabellenzellen zurückgeben.

Die Anzahl der Zeilen ist eine Zeilenlistengröße. Wir erhalten die genaue Anzahl der Tabellenzeilen:

   int               RowsTotal(void)            { return this.m_list_rows.Total();  }

Anders als bei den Zeilen wird hier die Anzahl der Spalten nur unter der Annahme zurückgegeben, dass ihre Anzahl in jeder Zeile gleich ist. Es wird also nur die Anzahl der Zellen in der allerersten Zeile zurückgegeben (Zeile mit dem Index Null in der Liste):

   int               ColumnsTotal(void)
                       {
                        //--- If there is no row in the list, return 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Get a pointer to the first row and return the number of cells in it
                        CTableRow *row=this.GetRow(0);
                        return(row!=NULL ? row.CellsTotal() : 0);
                       }

Bei der Erweiterung und Finalisierung dieser Klasse wird es möglich sein, Methoden hinzuzufügen, die die Anzahl der Zellen in einer bestimmten Zeile zurückgeben und dementsprechend nicht die Gesamtzahl der Zellen in der Tabelle zurückgeben, indem die (genaue) Anzahl der Zeilen in der Tabelle mit der Anzahl der Zellen in der ersten Zeile multipliziert wird (unter der oben genannten Annahme):

   int               CellsTotal(void){ return this.RowsTotal()*this.ColumnsTotal(); }

Dies ist ausreichend für genaue Berechnungen für diese Version der Klasse der tabellarischen Daten, und es gibt keine Notwendigkeit, die Dinge noch komplizierter zu machen - dies sind nur Hilfsklassen für die Klasse der Informationstafel, in der wir tabellarische (Gitter) Markup verwenden werden, um Daten auf der Tafel zu platzieren.


Dashboard-Klasse

Lassen Sie uns alle möglichen Mauszustände definieren:

  • Die Maustasten (links, rechts) sind nicht gedrückt,
  • Die Maustaste wird außerhalb des Bedienfeldfensters gedrückt,
  • Die Maustaste wird innerhalb des Panel-Fensters gedrückt,
  • Die Maustaste wird innerhalb des Fenstertitels gedrückt,
  • Die Maustaste wird auf dem Steuerelement "close" (schließen) gedrückt,
  • Die Maustaste wird auf dem Steuerelement "collapse/expand" (zusammenklappen/expandieren) gedrückt,
  • Die Maustaste wird auf dem Steuerelement "pin" (anheften) gedrückt,
  • Der Mauszeiger befindet sich außerhalb des Bedienfeldfensters,
  • Der Mauszeiger befindet sich innerhalb des Bedienfeldfensters,
  • Der Mauszeiger befindet sich innerhalb des Fenstertitels,
  • Der Mauszeiger befindet sich innerhalb des Steuerelements "close",
  • Der Mauszeiger befindet sich innerhalb des Steuerelements "collapse/expand",
  • Der Mauszeiger befindet sich innerhalb des Steuerelements "Pin".

Lassen Sie uns die entsprechende Enumerationen erstellen:

enum ENUM_MOUSE_STATE
  {
   MOUSE_STATE_NOT_PRESSED,
   MOUSE_STATE_PRESSED_OUTSIDE_WINDOW,
   MOUSE_STATE_PRESSED_INSIDE_WINDOW,
   MOUSE_STATE_PRESSED_INSIDE_HEADER,
   MOUSE_STATE_PRESSED_INSIDE_CLOSE,
   MOUSE_STATE_PRESSED_INSIDE_MINIMIZE,
   MOUSE_STATE_PRESSED_INSIDE_PIN,
   MOUSE_STATE_OUTSIDE_WINDOW,
   MOUSE_STATE_INSIDE_WINDOW,
   MOUSE_STATE_INSIDE_HEADER,
   MOUSE_STATE_INSIDE_CLOSE,
   MOUSE_STATE_INSIDE_MINIMIZE,
   MOUSE_STATE_INSIDE_PIN
  };

Momentan ist die Verfolgung des Haltens oder Anklickens der Maustaste auf den Bedienelementen des Panels implementiert worden. Mit anderen Worten: Das erste Drücken ist ein Auslöser für die Behebung des Zustands. In Windows-Anwendungen ist ein solcher Auslöser jedoch das Loslassen einer Schaltfläche, nachdem sie gedrückt wurde - ein Klick. Gedrückthalten wird zum Ziehen von Objekten verwendet. Aber im Moment reicht uns eine einfache Lösung - das erste Drücken wird entweder als Klick oder als Halten gewertet. Wenn wir das Panel weiterentwickeln, wird es möglich sein, die Handhabung der Maustasten so zu ergänzen, dass sie dem oben beschriebenen Verhalten entspricht.

Die Klasse CDashboard-Informationspanel besteht aus zwei Elementen: einer Leinwand oder canvas (Hintergrund), auf der das Design und die Steuerelemente des Panels gezeichnet werden, und einem Arbeitsbereich, auf dem die auf dem Panel platzierten Daten gezeichnet werden. Der Arbeitsbereich wird immer vollständig transparent sein, und die Leinwand wird separate Transparenzwerte haben - für die Kopfzeile und für alles andere:

Abb. 2. Nur die Leinwand mit unterschiedlicher Transparenz des Titels und das Feld mit einem Rahmen


Der Bereich unter dem Titel, der von einem Rahmen umrandet ist, dient dazu, einen Arbeitsbereich darauf zu platzieren. Der Bereich ist vollständig transparent und enthält Datentexte. Außerdem kann die Fläche unterhalb des Titels für die visuelle Gestaltung genutzt werden. In diesem Fall werden Tabellen darauf gezeichnet:

Abb. 3. Tabelle mit 12 Zeilen und 4 Spalten

Ein Arbeitsbereich mit Daten wird über der gestalteten Leinwand eingeblendet. Als Ergebnis erhalten wir ein vollwertiges Panel:

Abb. 4. Das Panel mit der 12x2-Hintergrundtabelle und den darauf befindlichen Daten


Wir werden die Werte einiger Panel-Parameter in globalen Terminalvariablen speichern, sodass sich das Panel seine Zustände merkt und sie bei einem Neustart wiederherstellt - X- und Y-Koordinaten, der minimierte Zustand und das Flag für die Beweglichkeit des Panels. Beim Anheften des Feldes an das Chart in eingeklappter Form wird diese angeheftete Position gespeichert, und beim nächsten Einklappen des angehefteten Feldes erscheint es an der gespeicherten Stelle.

Abb. 5. Das Paneel „merkt“ sich seine Ankerposition, wenn es in zusammengeklappter Form angeheftet wurde


Um sich den Bindungsort eines eingeklappten Bereichs zu merken, muss dieser eingeklappt, an den Bindungsort verschoben und angeheftet werden. Wenn das Bedienfeld in eingeklapptem Zustand angeheftet ist, wird seine Position gespeichert. Dann kann es erweitert, lösen und verschoben werden. Damit das Bedienfeld an die gespeicherte Bindungsposition zurückkehrt, muss es angeheftet und zusammengeklappt werden. Ohne Anheften wird das Panel an seiner aktuellen Position zusammengeklappt.


Klassenkörper:

//+------------------------------------------------------------------+
//| Dashboard class                                                  |
//+------------------------------------------------------------------+
class CDashboard : public CObject
  {
private:
   CCanvas           m_canvas;                  // Canvas
   CCanvas           m_workspace;               // Work space
   CTableData        m_table_data;              // Table cell array
   ENUM_PROGRAM_TYPE m_program_type;            // Program type
   ENUM_MOUSE_STATE  m_mouse_state;             // Mouse button status
   uint              m_id;                      // Object ID
   long              m_chart_id;                // ChartID
   int               m_chart_w;                 // Chart width
   int               m_chart_h;                 // Chart height
   int               m_x;                       // X coordinate
   int               m_y;                       // Y coordinate
   int               m_w;                       // Width
   int               m_h;                       // Height
   int               m_x_dock;                  // X coordinate of the pinned collapsed panel
   int               m_y_dock;                  // Y coordinate of the pinned collapsed panel
   
   bool              m_header;                  // Header presence flag
   bool              m_butt_close;              // Close button presence flag
   bool              m_butt_minimize;           // Collapse/expand button presence flag
   bool              m_butt_pin;                // Pin button presence flag
   bool              m_wider_wnd;               // Flag for exceeding the horizontal size of the window width panel
   bool              m_higher_wnd;              // Flag for exceeding the vertical size of the window height panel
   bool              m_movable;                 // Panel movability flag 
   int               m_header_h;                // Header height
   int               m_wnd;                     // Chart subwindow index
   
   uchar             m_header_alpha;            // Header transparency
   uchar             m_header_alpha_c;          // Current header transparency
   color             m_header_back_color;       // Header background color
   color             m_header_back_color_c;     // Current header background color
   color             m_header_fore_color;       // Header text color
   color             m_header_fore_color_c;     // Current header text color
   color             m_header_border_color;     // Header border color
   color             m_header_border_color_c;   // Current header border color
   
   color             m_butt_close_back_color;   // Close button background color
   color             m_butt_close_back_color_c; // Current close button background color
   color             m_butt_close_fore_color;   // Close button icon color
   color             m_butt_close_fore_color_c; // Current close button color
   
   color             m_butt_min_back_color;     // Expand/collapse button background color
   color             m_butt_min_back_color_c;   // Current expand/collapse button background color
   color             m_butt_min_fore_color;     // Expand/collapse button icon color
   color             m_butt_min_fore_color_c;   // Current expand/collapse button icon color
   
   color             m_butt_pin_back_color;     // Pin button background color
   color             m_butt_pin_back_color_c;   // Current pin button background color
   color             m_butt_pin_fore_color;     // Pin button icon color
   color             m_butt_pin_fore_color_c;   // Current pin button icon color
   
   uchar             m_alpha;                   // Panel transparency
   uchar             m_alpha_c;                 // Current panel transparency
   uchar             m_fore_alpha;              // Text transparency
   uchar             m_fore_alpha_c;            // Current text transparency
   color             m_back_color;              // Background color 

   color             m_back_color_c;            // Current background color 

   color             m_fore_color;              // Text color
   color             m_fore_color_c;            // Current text color
   color             m_border_color;            // Border color
   color             m_border_color_c;          // Current border color
   
   string            m_title;                   // Title text
   string            m_title_font;              // Title font
   int               m_title_font_size;         // Title font size
   string            m_font;                    // Font
   int               m_font_size;               // Font size
   
   bool              m_minimized;               // Collapsed panel window flag 
   string            m_program_name;            // Program name
   string            m_name_gv_x;               // Name of the global terminal variable storing the X coordinate 
   string            m_name_gv_y;               // Name of the global terminal variable storing the Y coordinate
   string            m_name_gv_m;               // Name of the global terminal variable storing the collapsed panel flag 

   string            m_name_gv_u;               // Name of the global terminal variable storing the flag of the pinned panel 

   uint              m_array_wpx[];             // Array of pixels to save/restore the workspace 
   uint              m_array_ppx[];             // Array of pixels to save/restore the panel background 

//--- Return the flag that the panel exceeds (1) the height and (2) the width of the corresponding chart size
   bool              HigherWnd(void)      const { return(this.m_h+2>this.m_chart_h);   }
   bool              WiderWnd(void)       const { return(this.m_w+2>this.m_chart_w);   }
//--- Enable/disable modes of working with the chart
   void              SetChartsTool(const bool flag);
   
//--- Save (1) the working space and (2) the panel background to the pixel array
   void              SaveWorkspace(void);
   void              SaveBackground(void);
//--- Restore (1) the working space and (2) the panel background from the pixel array
   void              RestoreWorkspace(void);
   void              RestoreBackground(void);

//--- Save the pixel array (1) of the working space and the (2) panel background to the file
   bool              FileSaveWorkspace(void);
   bool              FileSaveBackground(void);
//--- Load the pixel array of the (1) working space and (2) the panel background from the file
   bool              FileLoadWorkspace(void);
   bool              FileLoadBackground(void);

//--- Return the subwindow index
   int               GetSubWindow(void) const
                       {
                        return(this.m_program_type==PROGRAM_EXPERT || this.m_program_type==PROGRAM_SCRIPT ? 0 : ::ChartWindowFind());
                       }
   
protected:
//--- (1) Hide, (2) show and (3) bring the panel to the foreground
   void              Hide(const bool redraw=false);
   void              Show(const bool redraw=false);
   void              BringToTop(void);
//--- Return the chart ID
   long              ChartID(void)        const { return this.m_chart_id;              }
//--- Draw the header area
   void              DrawHeaderArea(const string title);
//--- Redraw the header area using a new color and text values
   void              RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- Draw the panel frame
   void              DrawFrame(void);
//--- (1) Draw and (2) redraw the panel closing button
   void              DrawButtonClose(void);
   void              RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Draw and (2) redraw the panel collapse/expand button
   void              DrawButtonMinimize(void);
   void              RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Draw and (2) redraw the panel pin button
   void              DrawButtonPin(void);
   void              RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);

//--- Return the flag for working in the visual tester
   bool              IsVisualMode(void) const
                       { return (bool)::MQLInfoInteger(MQL_VISUAL_MODE);               }
//--- Return the timeframe description
   string            TimeframeDescription(const ENUM_TIMEFRAMES timeframe) const
                       { return ::StringSubstr(EnumToString(timeframe),7);             }

//--- Return the state of mouse buttons
   ENUM_MOUSE_STATE  MouseButtonState(const int x,const int y,bool pressed);
//--- Shift the panel to new coordinates
   void              Move(int x,int y);

//--- Convert RGB to color
   color             RGBToColor(const double r,const double g,const double b) const;
//--- Write RGB component values to variables
   void              ColorToRGB(const color clr,double &r,double &g,double &b);
//--- Return (1) Red, (2) Green, (3) Blue color components
   double            GetR(const color clr)      { return clr&0xff ;                    }
   double            GetG(const color clr)      { return(clr>>8)&0xff;                 }
   double            GetB(const color clr)      { return(clr>>16)&0xff;                }
//--- Return a new color
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Draw a panel
   void              Draw(const string title);
//--- (1) Collapse and (2) expand the panel
   void              Collapse(void);
   void              Expand(void);

//--- Set the (1) X and (2) Y panel coordinates
   bool              SetCoordX(const int coord_x);
   bool              SetCoordY(const int coord_y);
//--- Set the panel (1) width and (2) height
   bool              SetWidth(const int width,const bool redraw=false);
   bool              SetHeight(const int height,const bool redraw=false);

public:
//--- Display the panel
   void              View(const string title)   { this.Draw(title);                    }
//--- Return the (1) CCanvas object, (2) working space, (3) object ID
   CCanvas          *Canvas(void)               { return &this.m_canvas;               }
   CCanvas          *Workspace(void)            { return &this.m_workspace;            }
   uint              ID(void)                   { return this.m_id;                    }
   
//--- Return the panel (1) X and (2) Y coordinates
   int               CoordX(void)         const { return this.m_x;                     }
   int               CoordY(void)         const { return this.m_y;                     }
//--- Return the panel (1) width and (2) height
   int               Width(void)          const { return this.m_w;                     }
   int               Height(void)         const { return this.m_h;                     }

//--- Return the (1) width, (2) height and (3) size of the specified text
   int               TextWidth(const string text)
                       { return this.m_workspace.TextWidth(text);                      }
   int               TextHeight(const string text)
                       { return this.m_workspace.TextHeight(text);                     }
   void              TextSize(const string text,int &width,int &height)
                       { this.m_workspace.TextSize(text,width,height);                 }
   
//--- Set the close button (1) presence, (2) absence flag
   void              SetButtonCloseOn(void);
   void              SetButtonCloseOff(void);
//--- Set the collapse/expand button (1) presence, (2) absence flag
   void              SetButtonMinimizeOn(void);
   void              SetButtonMinimizeOff(void);
   
//--- Set the panel coordinates
   bool              SetCoords(const int x,const int y);
//--- Set the panel size
   bool              SetSizes(const int w,const int h,const bool update=false);
//--- Set panel coordinates and size
   bool              SetParams(const int x,const int y,const int w,const int h,const bool update=false);

//--- Set the transparency of the panel (1) header and (2) working space
   void              SetHeaderTransparency(const uchar value);
   void              SetTransparency(const uchar value);
//--- Set default panel font parameters
   void              SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0);
//--- Display a text message at the specified coordinates
   void              DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE);
//--- Draw a (1) background grid (2) with automatic cell size
   void              DrawGrid(const uint x,const uint y,const uint rows,const uint columns,const uint row_size,const uint col_size,const color line_color=clrNONE,bool alternating_color=true);
   void              DrawGridAutoFill(const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true);
//--- Print grid data (line intersection coordinates)
   void              GridPrint(const uint indent=0)   { this.m_table_data.Print(indent);  }
//--- Write the X and Y coordinate values of the specified table cell to variables
   void              CellXY(const uint row,const uint column, int &x, int &y) { this.m_table_data.CellXY(row,column,x,y);  }
//--- Return the (1) X and (2) Y coordinate of the specified table cell
   int               CellX(const uint row,const uint column)         { return this.m_table_data.CellX(row,column);         }
   int               CellY(const uint row,const uint column)         { return this.m_table_data.CellY(row,column);         }

//--- Event handler
   void              OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
//--- Constructor/destructor
                     CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1);
                    ~CDashboard();
  };

Die deklarierten Variablen und Klassenmethoden werden im Code ausführlich kommentiert. Schauen wir uns nun die Implementierung einiger Methoden an.


Konstruktor der Klasse:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CDashboard::CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1) : 
                        m_id(id),
                        m_chart_id(::ChartID()),
                        m_program_type((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
                        m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)),
                        m_wnd(wnd==-1 ? GetSubWindow() : wnd),
                        m_chart_w((int)::ChartGetInteger(m_chart_id,CHART_WIDTH_IN_PIXELS,m_wnd)),
                        m_chart_h((int)::ChartGetInteger(m_chart_id,CHART_HEIGHT_IN_PIXELS,m_wnd)),
                        m_mouse_state(MOUSE_STATE_NOT_PRESSED),
                        m_x(x),
                        m_y(::ChartGetInteger(m_chart_id,CHART_SHOW_ONE_CLICK) ? (y<79 ? 79 : y) : y),
                        m_w(w),
                        m_h(h),
                        m_x_dock(m_x),
                        m_y_dock(m_y),
                        m_header(true),
                        m_butt_close(true),
                        m_butt_minimize(true),
                        m_butt_pin(true),
                        m_header_h(18),
                        
                        //--- Panel header implementation
                        m_header_alpha(128),
                        m_header_alpha_c(m_header_alpha),
                        m_header_back_color(C'0,153,188'),
                        m_header_back_color_c(m_header_back_color),
                        m_header_fore_color(C'182,255,244'),
                        m_header_fore_color_c(m_header_fore_color),
                        m_header_border_color(C'167,167,168'),
                        m_header_border_color_c(m_header_border_color),
                        m_title("Dashboard"),
                        m_title_font("Calibri"),
                        m_title_font_size(-100),
                        
                        //--- close button
                        m_butt_close_back_color(C'0,153,188'),
                        m_butt_close_back_color_c(m_butt_close_back_color),
                        m_butt_close_fore_color(clrWhite),
                        m_butt_close_fore_color_c(m_butt_close_fore_color),
                        
                        //--- collapse/expand button
                        m_butt_min_back_color(C'0,153,188'),
                        m_butt_min_back_color_c(m_butt_min_back_color),
                        m_butt_min_fore_color(clrWhite),
                        m_butt_min_fore_color_c(m_butt_min_fore_color),
                        
                        //--- pin button
                        m_butt_pin_back_color(C'0,153,188'),
                        m_butt_pin_back_color_c(m_butt_min_back_color),
                        m_butt_pin_fore_color(clrWhite),
                        m_butt_pin_fore_color_c(m_butt_min_fore_color),
                        
                        //--- Panel implementation
                        m_alpha(240),
                        m_alpha_c(m_alpha),
                        m_fore_alpha(255),
                        m_fore_alpha_c(m_fore_alpha),
                        m_back_color(C'240,240,240'),
                        m_back_color_c(m_back_color),
                        m_fore_color(C'53,0,0'),
                        m_fore_color_c(m_fore_color),
                        m_border_color(C'167,167,168'),
                        m_border_color_c(m_border_color),
                        m_font("Calibri"),
                        m_font_size(-100),
                        
                        m_minimized(false),
                        m_movable(true)
  {
//--- Set the permission for the chart to send messages about events of moving and pressing mouse buttons,
//--- mouse scroll events, as well as graphical object creation/deletion
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_WHEEL,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_CREATE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_DELETE,true);
   
//--- Set the names of global terminal variables to store panel coordinates, collapsed/expanded state and pinning
   this.m_name_gv_x=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_X";
   this.m_name_gv_y=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Y";
   this.m_name_gv_m=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Minimize";
   this.m_name_gv_u=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Unpin";
   
//--- If a global variable does not exist, create it and write the current value,
//--- otherwise - read the value from the terminal global variable into it
//--- X coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_x))
      ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
   else
      this.m_x=(int)::GlobalVariableGet(this.m_name_gv_x);
//--- Y coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_y))
      ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
   else
      this.m_y=(int)::GlobalVariableGet(this.m_name_gv_y);
//--- Collapsed/expanded
   if(!::GlobalVariableCheck(this.m_name_gv_m))
      ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
   else
      this.m_minimized=(int)::GlobalVariableGet(this.m_name_gv_m);
//--- Collapsed/not collapsed
   if(!::GlobalVariableCheck(this.m_name_gv_u))
      ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
   else
      this.m_movable=(int)::GlobalVariableGet(this.m_name_gv_u);

//--- Set the flags for the size of the panel exceeding the size of the chart window
   this.m_higher_wnd=this.HigherWnd();
   this.m_wider_wnd=this.WiderWnd();

//--- If the panel graphical resource is created,
   if(this.m_canvas.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"P"+(string)this.m_id,this.m_x,this.m_y,this.m_w,this.m_h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the canvas font and fill the canvas with the transparent color 
      this.m_canvas.FontSet(this.m_title_font,this.m_title_font_size,FW_BOLD);
      this.m_canvas.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for canvas failed",(string)__FUNCTION__);

//--- If a working space of a graphical resource is created,
   if(this.m_workspace.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"W"+(string)this.m_id,this.m_x+1,this.m_y+this.m_header_h,this.m_w-2,this.m_h-this.m_header_h-1,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the font for the working area and fill it with the transparent color 
      this.m_workspace.FontSet(this.m_font,this.m_font_size);
      this.m_workspace.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for workspace failed",(string)__FUNCTION__);
  }

Die Klasse hat einen parametrischen Konstruktor und einen, der standardmäßig erstellt wird. Natürlich sind wir nur an der parametrischen Variante interessiert. Sie wird bei der Erstellung eines Klassenobjekts verwendet. Eine eindeutige Objekt-ID, die Anfangskoordinaten des Panels, seine Breite und Höhe und der Index des Teilfensters, in dem das Panel platziert werden soll, werden dem Konstruktor als formale Parameter übergeben.

Die eindeutige Panel-ID wird benötigt, damit die Klasse Objekte mit eindeutigen Namen erstellen kann. Wenn wir mehrere Indikatoren mit Panels in einem Chart verwenden, benötigen wir zur Vermeidung von Konflikten bei den Objektnamen diese eindeutige Nummer, die dem Namen des Panel-Objekts hinzugefügt wird, wenn es erstellt wird. Die Eindeutigkeit der ID sollte wiederholbar sein - bei jedem neuen Start sollte die Nummer die gleiche sein wie beim letzten Start. Zum Beispiel ist GetTickCount() für die ID nicht geeignet.
Wenn der Index des Unterfensters standardmäßig gesetzt ist (-1), wird es programmatisch durchsucht, andernfalls wird der im Parameter angegebene Index verwendet.

Die Standardparameter werden in der Initialisierungsliste des Konstruktors festgelegt. Bei einigen Parametern, die für die visuelle Darstellung verantwortlich sind, werden zwei Variablen verwendet: der Standardwert und der aktuelle Eigenschaftswert. Dies ist für interaktive Änderungen notwendig, z. B. wenn man mit dem Mauszeiger über den Bereich des Panels fährt, für den diese Parameter zuständig sind.

Der Hauptteil des Konstruktors enthält die Werte der globalen Terminalvariablen. Es werden zwei grafische Objekte erstellt - die Leinwand und der Arbeitsbereich des Panels.

Der gesamte Konstruktorcode ist ausführlich kommentiert.


Der Destruktor der Klasse:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CDashboard::~CDashboard()
  {
//--- Write the current values to global terminal variables
   ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
   ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
   ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
   ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
//--- Delete panel objects
   this.m_canvas.Destroy();
   this.m_workspace.Destroy();
  }

Hier setzen wir zunächst die Koordinaten und Flaggen auf globale Terminalvariablen zurück und löschen dann die Objekte Leinwand und Arbeitsfläche.

Um mit dem Cursor und den Maustasten mit dem Panel interagieren zu können, müssen wir die Position des Cursors relativ zum Panel und seinen Bedienelementen kennen. Während sich der Cursor bewegt, können wir seine Koordinaten und die Zustände der Schaltflächen im Ereignis-Handler der Klasse verfolgen. Die Ereignisbehandlung der Klassen hat die gleichen Parameter wie die standardmäßige Ereignisbehandlung OnChartEvent:

void  OnChartEvent()
   const int       id,       // event ID 
   const long&     lparam,   // long type event parameter
   const double&   dparam,   // double type event parameter
   const string&   sparam    // string type event parameter
   );

Parameter

id

[in] Ereignis-ID aus der Enumeration ENUM_CHART_EVENT.

lparam

[in] Ereignisparameter vom Typ long

dparam

[in] Ereignisparameter vom Typ double

sparam

[in] Ereignisparameter vom Typ string

Rückgabewert

Kein Rückgabewert

Hinweis

Es gibt 11 Typen von Ereignissen, die mit der vordefinierten Funktion OnChartEvent() behandelt werden können. 65535 IDs von CHARTEVENT_CUSTOM bis einschließlich CHARTEVENT_CUSTOM_LAST werden für nutzerdefinierte Ereignisse bereitgestellt. Um ein nutzerdefiniertes Ereignis zu erzeugen, verwenden wir die Funktion EventChartCustom().

Kurze Ereignisbeschreibung aus der Enumeration ENUM_CHART_EVENT:

  • CHARTEVENT_KEYDOWN — Drücken einer Taste auf der Tastatur, wenn ein Chartfenster im Fokus ist;
  • CHARTEVENT_MOUSE_MOVE — Bewegen der Maus und Klicken der Maustaste (wenn https://www.mql5.com/de/docs/con für das Chart eingestellt ist);
  • CHARTEVENT_OBJECT_CREATE — Erstellung eines grafischen Objekts (wenn CHART_EVENT_OBJECT_CREATE=true für das Chart eingestellt ist);
  • CHARTEVENT_OBJECT_CHANGE — Objekteigenschaften über den Eigenschaftsdialog ändern;
  • CHARTEVENT_OBJECT_DELETE — löscht ein grafisches Objekt (wenn CHART_EVENT_OBJECT_DELETE für das Chart eingestellt ist);
  • CHARTEVENT_CLICK — Klicken auf ein Chart;
  • CHARTEVENT_OBJECT_CLICK — Mausklick auf ein grafisches Objekt eines Charts;
  • CHARTEVENT_OBJECT_DRAG — Ziehen eines grafischen Objekts mit der Maus;
  • CHARTEVENT_OBJECT_ENDEDIT — Beendet die Bearbeitung des Textes im Eingabefeld Bearbeiten eines grafischen Objekts (OBJ_EDIT);
  • CHARTEVENT_CHART_CHANGE — ändert ein Chart;
  • CHARTEVENT_CUSTOM+n — eigene Ereignis-ID, wobei n im Bereich von 0 bis 65535 liegt. CHARTEVENT_CUSTOM_LAST enthält die letzte akzeptable nutzerdefinierte Ereignis-ID (CHARTEVENT_CUSTOM+65535).

Der Parameter lparam enthält die X-Koordinate, dparam die Y-Koordinate und sparam die Kombination von Merkerwerten zur Bestimmung des Zustands der Maustasten. Alle diese Parameter müssen in Bezug auf die Koordinaten der Tafel und ihrer Elemente empfangen und verarbeitet werden. Der Zustand sollte bestimmt und an den Klassen-Ereignishandler gesendet werden, wo die Reaktion auf all diese Zustände festgelegt wird.


Die Methode, die den Zustand des Cursors und der Maustaste relativ zum Panel zurückgibt:

//+------------------------------------------------------------------+
//| Returns the state of the mouse cursor and button                 |
//+------------------------------------------------------------------+
ENUM_MOUSE_STATE CDashboard::MouseButtonState(const int x,const int y,bool pressed)
  {
//--- If the button is pressed
   if(pressed)
     {
      //--- If the state has already been saved, exit
      if(this.m_mouse_state!=MOUSE_STATE_NOT_PRESSED)
         return this.m_mouse_state;
      //--- If the button is pressed inside the window 
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- If the button is pressed inside the header
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Bring the panel to the foreground 
            this.BringToTop();
            //--- Coordinates of the close, collapse/expand and pin buttons 
            int wc=(this.m_butt_close ? this.m_header_h : 0);
            int wm=(this.m_butt_minimize ? this.m_header_h : 0);
            int wp=(this.m_butt_pin ? this.m_header_h : 0);
            //--- If the close button is pressed, return this state
            if(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_PRESSED_INSIDE_CLOSE;
            //--- If the collapse/expand button is pressed, return this state
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_PRESSED_INSIDE_MINIMIZE;
            //--- If the pin button is pressed, return this state
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_PRESSED_INSIDE_PIN;
            //--- If the button is not pressed on the control buttons of the panel, record and return the state of the button press inside the header
            this.m_mouse_state=MOUSE_STATE_PRESSED_INSIDE_HEADER;
            return this.m_mouse_state;
           }
         //--- If a button inside the window is pressed, write the state to a variable and return it
         else if(y>this.m_y+this.m_header_h && y<this.m_y+this.m_h)
           {
            this.m_mouse_state=MOUSE_STATE_PRESSED_INSIDE_WINDOW;
            return this.m_mouse_state;
           }
        }
      //--- The button is pressed outside the window - write the state to a variable and return it
      else
        {
         this.m_mouse_state=MOUSE_STATE_PRESSED_OUTSIDE_WINDOW;
         return this.m_mouse_state;
        }
     }
//--- If the button is not pressed
   else
     {
      //--- Write the state of the unpressed button to the variable
      this.m_mouse_state=MOUSE_STATE_NOT_PRESSED;
      //--- If the cursor is inside the panel 
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- If the cursor is inside the header
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Specify the width of the close, collapse/expand and pin buttons
            int wc=(this.m_butt_close ? this.m_header_h : 0);
            int wm=(this.m_butt_minimize ? this.m_header_h : 0);
            int wp=(this.m_butt_pin ? this.m_header_h : 0);
            //--- If the cursor is inside the close button, return this state 
            if(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_INSIDE_CLOSE;
            //--- If the cursor is inside the minimize/expand button, return this state 
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_INSIDE_MINIMIZE;
            //--- If the cursor is inside the pin button, return this state 
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_INSIDE_PIN;
            //--- If the cursor is outside the buttons inside the header area, return this state 
            return MOUSE_STATE_INSIDE_HEADER;
           }
         //--- Otherwise, the cursor is inside the working space. Return this state 
         else
            return MOUSE_STATE_INSIDE_WINDOW;
        }
     }
//--- In any other case, return the state of the unpressed mouse button
   return MOUSE_STATE_NOT_PRESSED;
  }

Die Methodenlogik wurde in den Codekommentaren ausführlich beschrieben. Wir bestimmen einfach die gegenseitigen Koordinaten des Cursors, des Panels und seiner Elemente und geben den Zustand zurück. Das Flag der gedrückten oder losgelassenen Maustaste wird sofort an die Methode gesendet. Für jeden dieser Zustände gibt es einen eigenen Codeblock, der den Zustand definiert, wenn die Taste gedrückt oder losgelassen wird. Die Verwendung der Logik auf diese Weise ist recht einfach und schnell. Es gibt jedoch einige Nachteile: Sie können einen Mausklick auf ein Steuerelement nicht erkennen. Stattdessen können Sie nur einen Klick darauf erkennen. Normalerweise wird ein Klick registriert, wenn die Maustaste losgelassen wird, und ein Gedrückthalten wird registriert, wenn sie gedrückt wird. Bei der hier verwendeten Logik wird nur das Drücken der Maustaste als Klick und Gedrückthalten betrachtet.

Die in dieser Methode erhaltenen Zustände sollten an die Ereignisbehandlung gesendet werden, wobei jedes Ereignis seinen eigenen Handler hat, der das Verhalten und das Aussehen des Panels ändert:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CDashboard::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- If a graphical object is created
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }
//--- If the chart is changed
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- Get the chart subwindow index (it may change when removing the window of any indicator)
      this.m_wnd=this.GetSubWindow();
      //--- Get the new chart size
      int w=(int)::ChartGetInteger(this.m_chart_id,CHART_WIDTH_IN_PIXELS,this.m_wnd);
      int h=(int)::ChartGetInteger(this.m_chart_id,CHART_HEIGHT_IN_PIXELS,this.m_wnd);
      //--- Determine whether the panel dimensions extend beyond the chart window
      this.m_higher_wnd=this.HigherWnd();
      this.m_wider_wnd=this.WiderWnd();
      //--- If the chart height has changed, adjust the panel vertical position
      if(this.m_chart_h!=h)
        {
         this.m_chart_h=h;
         int y=this.m_y;
         if(this.m_y+this.m_h>h-1)
            y=h-this.m_h-1;
         if(y<1)
            y=1;
         this.Move(this.m_x,y);
        }
      //--- If the chart weight has changed, adjust the panel horizontal position
      if(this.m_chart_w!=w)
        {
         this.m_chart_w=w;
         int x=this.m_x;
         if(this.m_x+this.m_w>w-1)
            x=w-this.m_w-1;
         if(x<1)
            x=1;
         this.Move(x,this.m_y);
        }
     }

//--- Declare variables to store the current cursor shift relative to the initial coordinates of the panel
   static int diff_x=0;
   static int diff_y=0;
   
//--- Get the flag of the held mouse button. We also take into account the right button for the visual tester (sparam=="2")
   bool pressed=(!this.IsVisualMode() ? (sparam=="1" || sparam=="" ? true : false) : sparam=="1" || sparam=="2" ? true : false);
//--- Get the cursor X and Y coordinates. Take into account the shift for the Y coordinate when working in the chart subwindow
   int  mouse_x=(int)lparam;
   int  mouse_y=(int)dparam-(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);
//--- Get the state of the cursor and mouse buttons relative to the panel
   ENUM_MOUSE_STATE state=this.MouseButtonState(mouse_x,mouse_y,pressed);
//--- If the cursor moves
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- If a button is pressed inside the working area of the panel
      if(state==MOUSE_STATE_PRESSED_INSIDE_WINDOW)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
         return;
        }
      //--- If a button is pressed inside the panel header area
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,-10,-10,-10);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
         //--- Shift the panel following the cursor taking into account the amount of cursor displacement relative to the initial coordinates of the panel
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }
        
      //--- If the close button is pressed 
      else if(state==MOUSE_STATE_PRESSED_INSIDE_CLOSE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the close button with a new background color 
         color new_color=this.NewColor(clrRed,0,40,40);
         if(this.m_butt_close_back_color_c!=new_color)
           {
            this.RedrawButtonClose(new_color);
            this.m_canvas.Update();
           }
         //--- Close button press handling should be defined in the program.
         //--- Send the click event of this button to its OnChartEvent handler.
         //--- Event ID 1001,
         //--- lparam=panel ID (m_id),
         //--- dparam=0
         //--- sparam="Close button pressed"
         ushort event=CHARTEVENT_CUSTOM+1;
         ::EventChartCustom(this.m_chart_id,ushort(event-CHARTEVENT_CUSTOM),this.m_id,0,"Close button pressed");
        }
      //--- If the panel collapse/expand button is pressed 
      else if(state==MOUSE_STATE_PRESSED_INSIDE_MINIMIZE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- "flip" the panel collapse flag,
         this.m_minimized=!this.m_minimized;
         //--- redraw the panel taking into account the new state of the flag,
         this.Draw(this.m_title);
         //--- redraw the panel header area 
         this.RedrawHeaderArea();
         //--- If the panel is pinned and expanded, move it to the stored location coordinates
         if(this.m_minimized && !this.m_movable)
            this.Move(this.m_x_dock,this.m_y_dock);
         //--- Update the canvas with chart redrawing and
         this.m_canvas.Update();
         //--- write the state of the panel expand flag to the global terminal variable
         ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
        }
      //--- If the panel pin button is pressed
      else if(state==MOUSE_STATE_PRESSED_INSIDE_PIN)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- "flip" the panel collapse flag,
         this.m_movable=!this.m_movable;
         //--- Redraw the pin button with a new background color
         color new_color=this.NewColor(this.m_butt_pin_back_color,30,30,30);
         if(this.m_butt_pin_back_color_c!=new_color)
            this.RedrawButtonPin(new_color);
         //--- If the panel is collapsed and pinned, save its coordinates
         //--- When expanded and collapsed again, the panel returns to these coordinates
         //--- Relevant for pinning a collapsed panel at the bottom of the screen
         if(this.m_minimized && !this.m_movable)
           {
            this.m_x_dock=this.m_x;
            this.m_y_dock=this.m_y;
           }
         //--- Update the canvas with chart redrawing and
         this.m_canvas.Update();
         //--- write the state of the panel movability flag to the global terminal variable
         ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
        }
        
      //--- If the cursor is inside the panel header area
      else if(state==MOUSE_STATE_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,20,20,20);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the close button
      else if(state==MOUSE_STATE_INSIDE_CLOSE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the collapse/expand button with the default background color 
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_color);
         //--- Redraw the pin button with the default background color 
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_color);
         //--- Redraw the close button with the red background color
         if(this.m_butt_close_back_color_c!=clrRed)
           {
            this.RedrawButtonClose(clrRed);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the collapse/expand button
      else if(state==MOUSE_STATE_INSIDE_MINIMIZE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the close button with the default background color
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Redraw the pin button with the default background color 
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_color);
         //--- Redraw the collapse/expand button with a new background color 
         new_color=this.NewColor(this.m_butt_min_back_color,20,20,20);
         if(this.m_butt_min_back_color_c!=new_color)
           {
            this.RedrawButtonMinimize(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the pin button
      else if(state==MOUSE_STATE_INSIDE_PIN)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the close button with the default background color
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Redraw the collapse/expand button with the default background color 
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_color);
         //--- Redraw the pin button with a new background color
         new_color=this.NewColor(this.m_butt_pin_back_color,20,20,20);
         if(this.m_butt_pin_back_color_c!=new_color)
           {
            this.RedrawButtonPin(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the working space
      else if(state==MOUSE_STATE_INSIDE_WINDOW)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Otherwise (the cursor is outside the panel, and we need to restore the chart parameters) 
      else
        {
         //--- Enable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(true);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Write the cursor shift by X and Y relative to the panel initial coordinates
      diff_x=mouse_x-this.m_x;
      diff_y=mouse_y-this.m_y;
     }
  }

Die Logik der Ereignisbehandlung ist im Code hinreichend ausführlich kommentiert. Wir sollten hier einige bemerkenswerte Punkte beachten.

Die Behandlung des Ereignisses der Erstellung eines neuen grafischen Objekts wird ganz am Anfang festgelegt:

//--- If a graphical object is created
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }

Was sind seine Funktionen? Wenn wir ein neues grafisches Objekt erstellen, wird es über anderen grafischen Objekten auf dem Chart platziert und dementsprechend über dem Bedienfeld eingeblendet. Daher wird bei der Definition eines solchen Ereignisses das Panel sofort in den Vordergrund gebracht. Dann wird ein neues grafisches Objekt hervorgehoben. Warum? Wenn dies nicht geschieht, werden grafische Objekte, die mehrere Punkte für die Konstruktion benötigen, z. B. eine Trendlinie, nicht normal erstellt - alle ihre Kontrollpunkte liegen dann auf derselben Koordinate, und das Objekt selbst ist nicht sichtbar. Dies geschieht durch den Verlust der Kontrolle über das grafische Objekt während seiner Erstellung, wenn das Panel in den Vordergrund gebracht wird. Daher sollte das neue grafische Objekt zwangsweise ausgewählt werden, nachdem das Panel in den Vordergrund gebracht wurde.
Daher verhalten sich das Panel und die grafischen Objekte bei ihrer Erstellung wie folgt:

Abb. 6. Das neue grafische Objekt wird „unter“ dem Panel erstellt und verliert nicht den Fokus, wenn es erstellt wird.


Die Ereignisbehandlung hat für jeden Zustand einen eigenen Verarbeitungsblock. Die Logik all dieser Blöcke ist identisch. Verarbeiten Sie zum Beispiel einen Mausklick und halten Sie die Maustaste in der Kopfzeile des Panels gedrückt:

      //--- If a button is pressed inside the panel header area
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,-10,-10,-10);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
         //--- Shift the panel following the cursor taking into account the amount of cursor displacement relative to the initial coordinates of the panel
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }

Um zu verhindern, dass sich das Chart mit dem Panel mitbewegt, sind die Ereignisse zum Scrollen des Charts mit der Maus, das Rechtsklick-Menü und das Fadenkreuz für das Chart deaktiviert. Da die Kopfzeile visuell auf Mausbewegungen reagieren soll, wird sie dunkler eingefärbt. Bevor wir die Farbe ändern, müssen wir prüfen, ob sie bereits geändert wurde. Es macht keinen Sinn, ständig denselben zu verwenden und dabei CPU-Ressourcen zu verschwenden. Wenn die Bewegung nicht deaktiviert ist (das Panel ist nicht angeheftet), wird es zu neuen Koordinaten verschoben, die aus den Mauskoordinaten abzüglich der Verschiebung der Cursorposition relativ zur oberen linken Ecke des Panels berechnet werden. Wenn die Verschiebung nicht berücksichtigt wird, wird das Panel genau an den Cursor-Koordinaten in der oberen linken Ecke positioniert.


Die Methode zum Verschieben des Panels zu bestimmten Koordinaten:

//+------------------------------------------------------------------+
//| Move the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Move(int x,int y)
  {
   int h=this.m_canvas.Height();
   int w=this.m_canvas.Width();
   if(!this.m_wider_wnd)
     {
      if(x+w>this.m_chart_w-1)
         x=this.m_chart_w-w-1;
      if(x<1)
         x=1;
     }
   else
     {
      if(x>1)
         x=1;
      if(x<this.m_chart_w-w-1)
         x=this.m_chart_w-w-1;
     }
   if(!this.m_higher_wnd)
     {
      if(y+h>this.m_chart_h-2)
         y=this.m_chart_h-h-2;
      if(y<1)
         y=1;
     }
   else
     {
      if(y>1)
         y=1;
      if(y<this.m_chart_h-h-2)
         y=this.m_chart_h-h-2;
     }
   if(this.SetCoords(x,y))
      this.m_canvas.Update();
  }

Die Koordinaten, an die das Panel verschoben werden soll, werden an die Methode übergeben. Wenn das Feld beim Ändern der Koordinaten außerhalb des Charts liegt, werden die Koordinaten so angepasst, dass sich das Feld immer innerhalb des Chartfensters befindet, das von jeder Kante um 1 Pixel eingerückt ist. Nach Abschluss aller Überprüfungen und Anpassungen der Tafelkoordinaten werden neue Koordinaten für die Lage der Tafel auf der Karte festgelegt.


Die Methoden, die die Panelkoordinaten festlegen:

//+------------------------------------------------------------------+
//| Set the panel X coordinate                                       |
//+------------------------------------------------------------------+
bool CDashboard::SetCoordX(const int coord_x)
  {
   int x=(int)::ObjectGetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_XDISTANCE);
   if(x==coord_x)
      return true;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_XDISTANCE,coord_x))
      return false;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_XDISTANCE,coord_x+1))
      return false;
   this.m_x=coord_x;
   return true;
  }
//+------------------------------------------------------------------+
//| Set the panel Y coordinate                                       |
//+------------------------------------------------------------------+
bool CDashboard::SetCoordY(const int coord_y)
  {
   int y=(int)::ObjectGetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_YDISTANCE);
   if(y==coord_y)
      return true;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_YDISTANCE,coord_y))
      return false;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_YDISTANCE,coord_y+this.m_header_h))
      return false;
   this.m_y=coord_y;
   return true;
  }

Wird der Methode eine Koordinate übergeben, die mit der Koordinate der Tafel übereinstimmt, muss diese nicht erneut festgelegt werden - der Erfolg der Methode wird einfach zurückgegeben. Zuerst wird die Leinwand verschoben, dann die Arbeitsfläche. Der Arbeitsbereich wird unter Berücksichtigung seiner relativen Position auf der Leinwand verschoben - links um ein Pixel innerhalb des Panels und oben um die Höhe der Kopfzeile.


Die Methoden zum Festlegen der Plattenabmessungen:

//+------------------------------------------------------------------+
//| Set the panel width                                              |
//+------------------------------------------------------------------+
bool CDashboard::SetWidth(const int width,const bool redraw=false)
  {
   if(width<4)
     {
      ::PrintFormat("%s: Error. Width cannot be less than 4px",(string)__FUNCTION__);
      return false;
     }
   if(width==this.m_canvas.Width())
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
      return false;
   if(width-2<1)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   else
     {
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
      if(!this.m_workspace.Resize(width-2,this.m_workspace.Height()))
         return false;
     }
   this.m_w=width;
   return true;
  }
//+------------------------------------------------------------------+
//| Set the panel height                                             |
//+------------------------------------------------------------------+
bool CDashboard::SetHeight(const int height,const bool redraw=false)
  {
   if(height<::fmax(this.m_header_h,1))
     {
      ::PrintFormat("%s: Error. Width cannot be less than %lupx",(string)__FUNCTION__,::fmax(this.m_header_h,1));
      return false;
     }
   if(height==this.m_canvas.Height())
      return true;
   if(!this.m_canvas.Resize(this.m_canvas.Width(),height))
      return false;
   if(height-this.m_header_h-2<1)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   else
     {
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
      if(!this.m_workspace.Resize(this.m_workspace.Width(),height-this.m_header_h-2))
         return false;
     }
   this.m_h=height;
   return true;
  }

Hier ist alles genau so wie beim Setzen von Koordinaten - wenn die an die Methode übergebene Größe die gleiche ist, die das Panel bereits hat, dann geben die Methoden einfach true zurück. Ein wichtiges Detail ist, dass die Arbeitsfläche immer kleiner ist als die Leinwand. Wenn sich bei der Verkleinerung des Arbeitsbereichs herausstellt, dass die Größe kleiner als 1 wird, wird der Arbeitsbereich einfach ausgeblendet, ohne seine Größe zu ändern, um Größenänderungsfehler zu vermeiden.


Hilfsmethoden, die zwei Koordinaten auf einmal, alle Dimensionen und Koordinaten sowie die Dimensionen des Panels auf einmal festlegen:

//+------------------------------------------------------------------+
//| Set the panel coordinates                                        |
//+------------------------------------------------------------------+
bool CDashboard::SetCoords(const int x,const int y)
  {
   bool res=true;
   res &=this.SetCoordX(x);
   res &=this.SetCoordY(y);
   return res;
  }
//+------------------------------------------------------------------+
//| Set the panel size                                               |
//+------------------------------------------------------------------+
bool CDashboard::SetSizes(const int w,const int h,const bool update=false)
  {
   bool res=true;
   res &=this.SetWidth(w);
   res &=this.SetHeight(h);
   if(res && update)
      this.Expand();
   return res;
  }
//+------------------------------------------------------------------+
//| Set panel coordinates and size                                   |
//+------------------------------------------------------------------+
bool CDashboard::SetParams(const int x,const int y,const int w,const int h,const bool update=false)
  {
   bool res=true;
   res &=this.SetCoords(x,y);
   res &=this.SetSizes(w,h);
   if(res && update)
      this.Expand();
   return res;
  }

Die Methoden erhalten die Parameter und das Aktualisierungsflag. Wenn die Parameter und das Aktualisierungsflag erfolgreich gesetzt sind, wird die Methode zur Erweiterung des Panels aufgerufen, bei der alle Paneelelemente neu gezeichnet werden.


Die Methode, die den Kopfbereich zeichnet:

//+------------------------------------------------------------------+
//| Draw the header area                                             |
//+------------------------------------------------------------------+
void CDashboard::DrawHeaderArea(const string title)
  {
//--- Exit if the header is not used
   if(!this.m_header)
      return;
//--- Set the title text
   this.m_title=title;
//--- The Y coordinate of the text is located vertically in the center of the header area
   int y=this.m_header_h/2;
//--- Fill the area with color
   this.m_canvas.FillRectangle(0,0,this.m_w-1,this.m_header_h-1,::ColorToARGB(this.m_header_back_color,this.m_header_alpha));
//--- Display the header text
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(this.m_header_fore_color,this.m_header_alpha),TA_LEFT|TA_VCENTER);
//--- Save the current header background color
   this.m_header_back_color_c=this.m_header_back_color;
//--- Draw control elements (close, collapse/expand and pin buttons) and
   this.DrawButtonClose();
   this.DrawButtonMinimize();
   this.DrawButtonPin();
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

Die Methode zeichnet den Kopfbereich - malt einen rechteckigen Bereich an den Kopfkoordinaten und zeichnet die Steuerelemente - Schließen, Verkleinern/Erweitern und Pin-Schaltflächen. Es werden die Standardfarben und -transparenz des Kopfbereichs verwendet.


Methode, mit der der Kopfbereich neu gezeichnet wird:

//+------------------------------------------------------------------+
//| Redraw header area                                               |
//+------------------------------------------------------------------+
void CDashboard::RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the header is not used or all passed parameters have default values
   if(!this.m_header || (new_color==clrNONE && title=="" && title_new_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- Exit if all passed parameters are equal to those already set
   if(new_color==this.m_header_back_color && title==this.m_title && title_new_color==this.m_header_fore_color && new_alpha==this.m_header_alpha)
      return;
//--- If the title is not equal to the default value, set a new title
   if(title!="")
      this.m_title=title;
//--- Define new background and text colors, and transparency
   color back_clr=(new_color!=clrNONE ? new_color : this.m_header_back_color);
   color fore_clr=(title_new_color!=clrNONE ? title_new_color : this.m_header_fore_color);  
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- The Y coordinate of the text is located vertically in the center of the header area
   int y=this.m_header_h/2;
//--- Fill the area with color
   this.m_canvas.FillRectangle(0,0,this.m_w-1,this.m_header_h-1,::ColorToARGB(back_clr,alpha));
//--- Display the header text
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(fore_clr,alpha),TA_LEFT|TA_VCENTER);
//--- Save the current header background color, text and transparency
   this.m_header_back_color_c=back_clr;
   this.m_header_fore_color_c=fore_clr;
   this.m_header_alpha_c=alpha;
//--- Draw control elements (close, collapse/expand and pin buttons) and
   this.RedrawButtonClose(back_clr,clrNONE,alpha);
   this.RedrawButtonMinimize(back_clr,clrNONE,alpha);
   this.RedrawButtonPin(back_clr,clrNONE,alpha);
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(true);
  }

Die neue Hintergrundfarbe, der neue Kopftext, die neue Kopftextfarbe und die neue Transparenz werden an die Methode übergeben. Wir beenden die Methode, wenn die übergebenen Parameter vollständig mit den bereits gesetzten übereinstimmen. Die Methode wird verwendet, um die Farbe, den Text und die Transparenz der Kopfzeile zu aktualisieren.


Die Methode, die den Panelrahmen zeichnet:

//+------------------------------------------------------------------+
//| Draw the panel frame                                             |
//+------------------------------------------------------------------+
void CDashboard::DrawFrame(void)
  {
   this.m_canvas.Rectangle(0,0,this.m_w-1,this.m_h-1,::ColorToARGB(this.m_border_color,this.m_alpha));
   this.m_border_color_c=this.m_border_color;
   this.m_canvas.Update(false);
  }

Wir zeichnen einen Rahmen um den Umfang der Leinwand und speichern die eingestellte Farbe als die aktuelle Farbe.


Die Methode, die die Schaltfläche zum Schließen des Fensters zeichnet:

//+------------------------------------------------------------------+
//| Draws the panel close button                                     |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonClose(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_close)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- Button coordinates and size
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_close_back_color,this.m_header_alpha));
//--- Draw the close button
   this.m_canvas.LineThick(x1+shift+1,y1+shift+1,x2-shift,y2-shift,::ColorToARGB(this.m_butt_close_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
   this.m_canvas.LineThick(x1+shift+1,y2-shift-1,x2-shift,y1+shift,::ColorToARGB(this.m_butt_close_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_close_back_color_c=this.m_butt_close_back_color;
   this.m_butt_close_fore_color_c=this.m_butt_close_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

Die gesamte Logik ist in den Codekommentaren beschrieben: Zeichne den Hintergrund und das Schließsymbol (ein Kreuz) darüber.


Die Methode, mit der die Schaltfläche zum Schließen des Bedienfelds neu gezeichnet wird:

//+------------------------------------------------------------------+
//| Redraw the panel close button                                    |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_close || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- Button coordinates and size
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_close_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_close_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Draw the close button
   this.m_canvas.LineThick(x1+shift+1,y1+shift+1,x2-shift,y2-shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
   this.m_canvas.LineThick(x1+shift+1,y2-shift-1,x2-shift,y1+shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_close_back_color_c=back_color;
   this.m_butt_close_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

Um neu zu zeichnen, muss sich mindestens einer der übergebenen Parameter von dem aktuellen Parameter unterscheiden. Der Rest ist identisch mit der Methode zum Zeichnen von Schaltflächen, mit Ausnahme der Auswahl und Einstellung neuer Zeichenparameter.


Andere Methoden zum Zeichnen und erneuten Zeichnen von Schaltflächen zum Ein- und Ausklappen und Anheften:

//+------------------------------------------------------------------+
//| Draw the panel collapse/expand button                            |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonMinimize(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_minimize)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close button is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_min_back_color,this.m_header_alpha));
//--- If the panel is collapsed, draw a rectangle
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(this.m_butt_min_fore_color,255));
//--- Otherwise, the panel is expanded. Draw a line segment
   else
      this.m_canvas.LineThick(x1+shift,y2-shift,x2-shift,y2-shift,::ColorToARGB(this.m_butt_min_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_min_back_color_c=this.m_butt_min_back_color;
   this.m_butt_min_fore_color_c=this.m_butt_min_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Redraw the panel collapse/expand button                          |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_minimize || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close button is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_min_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_min_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- If the panel is collapsed, draw a rectangle
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255));
//--- Otherwise, the panel is expanded. Draw a line segment
   else
      this.m_canvas.LineThick(x1+shift,y2-shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_min_back_color_c=back_color;
   this.m_butt_min_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Draw the panel pin button                                        |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonPin(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_pin)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close and collapse buttons is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_pin_back_color,this.m_header_alpha));
//--- Coordinates of the broken line points
   int x[]={x1+3, x1+6, x1+3,x1+4,x1+6,x1+9,x1+9,x1+10,x1+15,x1+14,x1+13,x1+10,x1+10,x1+9,x1+6};
   int y[]={y1+14,y1+11,y1+8,y1+7,y1+7,y1+4,y1+3,y1+2, y1+7, y1+8, y1+8, y1+11,y1+13,y1+14,y1+11};
//--- Draw the "button" shape 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- If the movability flag is reset (pinned) - cross out the drawn button
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Remember the current background color and button design
   this.m_butt_pin_back_color_c=this.m_butt_pin_back_color;
   this.m_butt_pin_fore_color_c=this.m_butt_pin_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Redraw the panel pin button                                      |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_pin || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close and collapse buttons is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_pin_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_pin_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Coordinates of the broken line points
   int x[]={x1+3, x1+6, x1+3,x1+4,x1+6,x1+9,x1+9,x1+10,x1+15,x1+14,x1+13,x1+10,x1+10,x1+9,x1+6};
   int y[]={y1+14,y1+11,y1+8,y1+7,y1+7,y1+4,y1+3,y1+2, y1+7, y1+8, y1+8, y1+11,y1+13,y1+14,y1+11};
//--- Draw the "button" shape 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- If the movability flag is reset (pinned) - cross out the drawn button
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Remember the current background color and button design
   this.m_butt_pin_back_color_c=back_color;
   this.m_butt_pin_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

Die Methoden sind identisch mit den Methoden zum Zeichnen und erneuten Zeichnen der Schließen-Schaltfläche. Die Logik ist genau dieselbe, und sie ist in den Kommentaren zum Code beschrieben.


Die Methode, die das Panel zeichnet:

//+------------------------------------------------------------------+
//| Draw the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Draw(const string title)
  {
//--- Set the title text
   this.m_title=title;
//--- If the collapse flag is not set, expand the panel
   if(!this.m_minimized)
      this.Expand();
//--- Otherwise, collapse the panel
   else
      this.Collapse();
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
//--- Update the working space and redraw the chart
   this.m_workspace.Update();
  }

Wenn das Flag zum Zusammenklappen nicht gesetzt ist, erweitern Sie den Bereich (zeichnen Sie ihn in erweiterter Form). Wenn das Flag „collapse“ gesetzt ist, wird das Panel zusammengeklappt: Das Panel wird in zusammengeklappter Form gezeichnet, wobei nur die Kopfzeile übrig bleibt.


Die Methode zum Zusammenklappen des Panels:

//+------------------------------------------------------------------+
//| Collapse the panel                                               |
//+------------------------------------------------------------------+
void CDashboard::Collapse(void)
  {
//--- Save the pixels of the working space and the panel background into arrays
   this.SaveWorkspace();
   this.SaveBackground();
//--- Remember the current height of the panel
   int h=this.m_h;
//--- Change the dimensions (height) of the canvas and working space
   if(!this.SetSizes(this.m_canvas.Width(),this.m_header_h))
      return;
//--- Draw the header area
   this.DrawHeaderArea(this.m_title);
//--- Return the saved panel height to the variable
   this.m_h=h;
  }

Bevor wir das Panel zusammenklappen, müssen wir alle Pixel des Hintergrunds und der Arbeitsfläche in Arrays speichern. Dies ist notwendig, um das Panel schnell zu erweitern, indem man einfach die Panel-Bilder und den Arbeitsbereich aus Pixel-Arrays wiederherstellt, anstatt sie neu zu zeichnen. Außerdem können wir einige zusätzliche Verzierungen auf den Hintergrund der Tafel malen (z. B. ein Namensschild). Auch sie werden zusammen mit dem Hintergrund gespeichert und anschließend wiederhergestellt.


Die Methode, mit der das Panel erweitert wird:

//+------------------------------------------------------------------+
//| Expand the panel                                                 |
//+------------------------------------------------------------------+
void CDashboard::Expand(void)
  {
//--- Resize the panel
   if(!this.SetSizes(this.m_canvas.Width(),this.m_h))
      return;
//--- If the panel background pixels have never been saved into an array
   if(this.m_array_ppx.Size()==0)
     {
      //--- Draw the panel and
      this.m_canvas.Erase(::ColorToARGB(this.m_back_color,this.m_alpha));
      this.DrawFrame();
      this.DrawHeaderArea(this.m_title);
      //--- save the background pixels of the panel and working space into arrays
      this.SaveWorkspace();
      this.SaveBackground();
     }
//--- If the background pixels of the panel and working space were previously saved,
   else
     {
      //--- restore the background pixels of the panel and working space from arrays
      this.RestoreBackground();
      if(this.m_array_wpx.Size()>0)
         this.RestoreWorkspace();
     }
//--- If, after expanding, the panel goes beyond the chart window, adjust the panel location
   if(this.m_y+this.m_canvas.Height()>this.m_chart_h-1)
      this.Move(this.m_x,this.m_chart_h-1-this.m_canvas.Height());
  }

Wenn die Arrays, in denen die Hintergrund- und Arbeitsbereichspixel gespeichert sind, leer sind, zeichnen wir das gesamte Panel mit den Zeichenmethoden. Wenn die Felder bereits gefüllt sind, stellen wir einfach den Hintergrund des Panels und seinen Arbeitsbereich aus den Feldern wieder her.


Hilfsmethoden für die Arbeit mit Farbe:

//+------------------------------------------------------------------+
//| Returns color with a new color component                         |
//+------------------------------------------------------------------+
color CDashboard::NewColor(color base_color, int shift_red, int shift_green, int shift_blue)
  {
   double clR=0, clG=0, clB=0;
   this.ColorToRGB(base_color,clR,clG,clB);
   double clRn=(clR+shift_red  < 0 ? 0 : clR+shift_red  > 255 ? 255 : clR+shift_red);
   double clGn=(clG+shift_green< 0 ? 0 : clG+shift_green> 255 ? 255 : clG+shift_green);
   double clBn=(clB+shift_blue < 0 ? 0 : clB+shift_blue > 255 ? 255 : clB+shift_blue);
   return this.RGBToColor(clRn,clGn,clBn);
  }
//+------------------------------------------------------------------+
//| Convert RGB to color                                             |
//+------------------------------------------------------------------+
color CDashboard::RGBToColor(const double r,const double g,const double b) const
  {
   int int_r=(int)::round(r);
   int int_g=(int)::round(g);
   int int_b=(int)::round(b);
   int clr=0;
   clr=int_b;
   clr<<=8;
   clr|=int_g;
   clr<<=8;
   clr|=int_r;
//---
   return (color)clr;
  }
//+------------------------------------------------------------------+
//| Getting values of the RGB components                             |
//+------------------------------------------------------------------+
void CDashboard::ColorToRGB(const color clr,double &r,double &g,double &b)
  {
   r=GetR(clr);
   g=GetG(clr);
   b=GetB(clr);
  }

Die Methoden sind erforderlich, um die Farbe zu ändern, wenn der Cursor mit den Bedienelementen des Panels interagiert.


Die Methode zur Einstellung der Transparenz der Kopfzeile:

//+------------------------------------------------------------------+
//| Set the header transparency                                      |
//+------------------------------------------------------------------+
void CDashboard::SetHeaderTransparency(const uchar value)
  {
   this.m_header_alpha=value;
   if(this.m_header_alpha_c!=this.m_header_alpha)
      this.RedrawHeaderArea(clrNONE,NULL,clrNONE,value);
   this.m_header_alpha_c=value;
  }

Zunächst wird der an die Methode übergebene Transparenzwert in eine Variable geschrieben, die die Standardtransparenz speichert, dann wird der neue Wert mit dem aktuellen verglichen. Wenn die Werte nicht gleich sind, wird der Kopfbereich komplett neu gezeichnet. Am Ende wird die eingestellte Transparenz in die aktuelle Transparenz geschrieben.


Die Methode zur Einstellung der Transparenz des Panels:

//+------------------------------------------------------------------+
//| Set the panel transparency                                       |
//+------------------------------------------------------------------+
void CDashboard::SetTransparency(const uchar value)
  {
   this.m_alpha=value;
   if(this.m_alpha_c!=this.m_alpha)
     {
      this.m_canvas.Erase(::ColorToARGB(this.m_back_color,value));
      this.DrawFrame();
      this.RedrawHeaderArea(clrNONE,NULL,clrNONE,value);
      this.m_canvas.Update(false);
     }
   this.m_alpha_c=value;
  }

Die Logik ist ähnlich wie bei der obigen Methode. Wenn die an die Methode übergebene Transparenz nicht mit der aktuellen Transparenz übereinstimmt, wird das Panel komplett mit der neuen Transparenz neu gezeichnet.


Die Methode zur Einstellung der Standard-Schriftartenparameter des Arbeitsbereichs:

//+------------------------------------------------------------------+
//| Set the default font parameters of the working space             |
//+------------------------------------------------------------------+
void CDashboard::SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0)
  {
   if(!this.m_workspace.FontSet(name,size*-10,flags,angle))
     {
      ::PrintFormat("%s: Failed to set font options. Error %lu",(string)__FUNCTION__,::GetLastError());
      return;
     }
   this.m_font=name;
   this.m_font_size=size*-10;
  }

Die an die Methode übergebenen Schriftparameter (Schriftname, Größe, Flags und Winkel) werden auf das CCanvas-Objekt des Arbeitsbereichs gesetzt und in Variablen gespeichert.

Die an die Methode übergebene Schriftgröße wird aus dem in der Anmerkung zur Funktion TextSetFont beschriebenen Grund mit -10 multipliziert:

Wenn der Schriftname „::“ enthält, wird die Schrift aus der EX5-Ressource geladen. Wenn der Name der Schriftart mit einer Erweiterung angegeben ist, wird die Schriftart aus der Datei geladen, und wenn der Pfad mit "\" oder "/" beginnt, wird die Datei relativ zum MQL5-Verzeichnis gesucht, andernfalls relativ zum Pfad der EX5-Datei, die die Funktion TextSetFont() aufgerufen hat.

Die Schriftgröße wird mit positiven oder negativen Werten eingestellt. Das Zeichen legt fest, ob die Textgröße von den Einstellungen des Betriebssystems abhängt (Schriftgröße).

  • Ist die Größe positiv, wird sie in physische Einheiten (Pixel) umgerechnet, wenn die logische Schriftart als physische Schriftart angezeigt wird. Die Größe entspricht der Höhe der Symbolzellen der verfügbaren Schriftarten. Es wird nicht empfohlen, Texte, die mit der Funktion TextOut() angezeigt werden, und Texte, die mit dem grafischen Objekt OBJ_LABEL („Text label“) angezeigt werden, gemeinsam zu verwenden.
  • Wenn die Größe negativ ist, wird angenommen, dass sie in Zehntel eines logischen Punktes festgelegt wird (der Wert -350 entspricht 35 logischen Punkten) und durch 10 geteilt wird. Der resultierende Wert wird in physikalische Einheiten des Geräts (Pixel) umgerechnet und entspricht dem absoluten Wert der Zeichenhöhe aus den verfügbaren Schriftarten. Um einen Text in der Größe des Objekts OBJ_LABEL auf dem Bildschirm zu erhalten, multiplizieren wir die in den Objekteigenschaften angegebene Schriftgröße mit -10.


Die Methode zum Aktivieren/Deaktivieren von Arbeitsmodi mit einem Chart:

//+------------------------------------------------------------------+
//| Enable/disable modes of working with the chart                   |
//+------------------------------------------------------------------+
void CDashboard::SetChartsTool(const bool flag)
  {
//--- If the 'true' flag is passed and if chart scrolling is disabled
   if(flag && !::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- enable chart scrolling, right-click menu and crosshair
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,true);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,true);
     }
//--- otherwise, if the 'false' flag is passed and if chart scrolling is enabled
   else if(!flag && ::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- disable chart scrolling, right-click menu and crosshair
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,false);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,false);
     }
  }

Abhängig von dem übergebenen Flag prüft die Methode den Status des Scrollens des Charts mit der Maus und aktiviert entweder alle Modi der Arbeit mit dem Chart oder deaktiviert sie. Die Überprüfung des Bildlaufmodus ist notwendig, um nicht ständig einen Befehl zum Einstellen der Modi zu senden, wenn sich der Cursor innerhalb oder außerhalb des Bedienfeldfensters befindet. Der Moduswechsel erfolgt nur, wenn der Cursor in das Feld eintritt oder wenn der Cursor seine Grenzen verlässt.


Die Methode, die eine Textnachricht an den angegebenen Koordinaten ausgibt:

//+------------------------------------------------------------------+
//| Display a text message at the specified coordinates              |
//+------------------------------------------------------------------+
void CDashboard::DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE)
  {
//--- Declare variables to record the text width and height in them
   int w=width;
   int h=height;
//--- If the width and height of the text passed to the method have zero values,
//--- then the entire working space is completely cleared using the transparent color
   if(width==0 && height==0)
      this.m_workspace.Erase(0x00FFFFFF);
//--- Otherwise
   else
     {
      //--- If the passed width and height have default values (-1), we get its width and height from the text 
      if(width==WRONG_VALUE && height==WRONG_VALUE)
         this.m_workspace.TextSize(text,w,h);
      //--- otherwise,
      else
        {
         //--- if the width passed to the method has the default value (-1) - get the width from the text, or
         //--- if the width passed to the method has a value greater than zero, use the width passed to the method, or
         //--- if the width passed to the method has a zero value, use the value 1 for the width
         w=(width ==WRONG_VALUE ? this.m_workspace.TextWidth(text)  : width>0  ? width  : 1);
         //--- if the height passed to the method has a default value (-1), get the height from the text, or
         //--- if the height passed to the method has a value greater than zero, use the height passed to the method, or
         //--- if the height passed to the method has a zero value, use value 1 for the height
         h=(height==WRONG_VALUE ? this.m_workspace.TextHeight(text) : height>0 ? height : 1);
        }
      //--- Fill the space according to the specified coordinates and the resulting width and height with a transparent color (erase the previous entry)
      this.m_workspace.FillRectangle(x,y,x+w,y+h,0x00FFFFFF);
     }
//--- Display the text to the space cleared of previous text and update the working space without redrawing the screen
   this.m_workspace.TextOut(x,y,text,::ColorToARGB(this.m_fore_color));
   this.m_workspace.Update(false);
  }

Die Arbeit mit einer Leinwand, auf der Grafiken angezeigt werden, beinhaltet das Zeichnen dieser Grafiken auf der Leinwand, so wie man einen Pinsel auf einer Leinwand nutzt. Ein auf die Leinwand gemaltes Bild wird über ein anderes, zuerst gemaltes Bild gelegt. Um ein Bild zu ersetzen, sollten Sie entweder die gesamte Leinwand neu zeichnen oder die Abmessungen des vorherigen Bildes berechnen, diesen Bereich löschen und das nächste Bild auf dem gelöschten Bereich zeichnen.

Bei Texten können wir die Abmessungen des bereits gezeichneten Textes auf der Leinwand ermitteln, um diesen Bereich zu löschen, bevor wir den nächsten Text zeichnen. Alle zuvor gezeichneten Texte und Formen irgendwo in einem Objekt zu speichern, ist keineswegs eine optimale Lösung. Daher wird hier eine Wahl zwischen Genauigkeit und Einfachheit gepaart mit Optimalität getroffen. Hier werden die Abmessungen des aktuellen Textes ermittelt (die möglicherweise nicht mit der Breite des zuvor an dieser Stelle gezeichneten Textes übereinstimmen), und diese Abmessungen werden verwendet, um den zuvor gezeichneten Text zu löschen, bevor der nächste Text angezeigt wird.

Wenn der vorherige Text nicht die gleiche Breite wie der aktuelle hat oder sogar größer ist, wird nicht der gesamte Text gelöscht. Für solche Fälle sieht die Methode die Übergabe von Parametern vor, die die gewünschten Abmessungen des gelöschten Bereichs in Höhe und Breite angeben:

  1. wenn die an die Methode übergebenen Werte für Breite und Höhe gleich -1 sind (Standardeinstellung), wird der Bereich, der der Breite und Höhe des aktuellen Textes entspricht, gelöscht,
  2. wenn Nullen übergeben werden, wird der gesamte Arbeitsbereich vollständig gelöscht,
  3. wenn ein Breiten- oder Höhenwert größer als Null übergeben wird, werden diese Werte für die Breite bzw. Höhe verwendet.
Wenn Text angezeigt wird, ist seine Höhe in der Regel gleich der vorherigen, wenn die gleiche Schriftart und die gleiche Größe verwendet werden. Die Breite kann jedoch variieren. Daher können Sie eine Breite wählen, die nur den für den Text vorgesehenen Bereich löscht (ohne die benachbarten Bereiche zu beeinträchtigen, wobei sichergestellt wird, dass der vorherige Text gelöscht wird), und diese an die Methode übergeben.


Die Panel-Klasse ist für die Anzeige von Daten in Tabellenform vorgesehen. Um die Berechnung der Koordinaten der auf dem Panel angezeigten Daten und die visuelle Gestaltung des Panels zu erleichtern, sind zwei Methoden vorgesehen, die die Koordinaten der Tabellenzellen berechnen und (gegebenenfalls) Namensschilder auf den Panelhintergrund zeichnen.

  • Die erste Methode berechnet die Koordinaten der Tabellenzellen auf der Grundlage der ihr übergebenen Daten: die anfänglichen X- und Y-Koordinaten der Tabelle im Panel, die Anzahl der Zeilen, Spalten, Zeilenhöhe und Spaltenbreite. Wir können auch eine nutzerdefinierte eigene Farbe für die Tabellengitterlinien und das Attribut der „alternierenden“ Zeilen festlegen.
  • Die zweite Methode berechnet die Größe der Zeilen und Spalten automatisch in Abhängigkeit von ihrer Anzahl und der Breite des Tabelleneinzugs von den Rändern der Platte. Bei der zweiten Methode können wir auch eine eigene Farbe für die Linien des Tabellengitters und das Attribut „abwechselnd“ für die Zeilen festlegen.


Die Methode, die ein Hintergrundgitter auf der Grundlage der angegebenen Parameter zeichnet:

//+------------------------------------------------------------------+
//| Draw the background grid                                         |
//+------------------------------------------------------------------+
void CDashboard::DrawGrid(const uint x,const uint y,const uint rows,const uint columns,const uint row_size,const uint col_size,
                          const color line_color=clrNONE,bool alternating_color=true)
  {
//--- If the panel is collapsed, leave
   if(this.m_minimized)
      return;
//--- Clear all lists of the tabular data object (remove cells from rows and all rows)
   this.m_table_data.Clear();
//--- Line height cannot be less than 2
   int row_h=int(row_size<2 ? 2 : row_size);
//--- Column width cannot be less than 2
   int col_w=int(col_size<2 ? 2 : col_size);
   
//--- The X1 (left) coordinate of the table cannot be less than 1 (to leave one pixel around the perimeter of the panel for the frame)
   int x1=int(x<1 ? 1 : x);
//--- Calculate the X2 coordinate (right) depending on the number of columns and their width
   int x2=x1+col_w*int(columns>0 ? columns : 1);
//--- The Y1 coordinate is located under the panel title area
   int y1=this.m_header_h+(int)y;
//--- Calculate the Y2 coordinate (bottom) depending on the number of lines and their height
   int y2=y1+row_h*int(rows>0 ? rows : 1);
   
//--- Get the color of the table grid lines, either by default or passed to the method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- If the initial X coordinate is greater than 1, draw a table frame
//--- (in case of the coordinate 1, the table frame is the panel frame)
   if(x1>1)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
//--- In the loop by table rows,
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate the Y coordinate of the next horizontal grid line (Y coordinate of the next table row)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" line colors is passed and the line is even
      if(alternating_color && i%2==0)
        {
         //--- lighten the table background color and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         this.m_canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw a table grid horizontal line
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create a new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to the list of rows of the tabular data object
      //--- (if adding an object failed, delete the created object)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Set its Y coordinate in the created row object taking into account the offset from the panel title
      row_obj.SetY(row_y-this.m_header_h);
     }
//--- In the loop by table columns,
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate the X coordinate of the next vertical grid line (X coordinate of the next table row)
      int col_x=x1+col_w*i;
      //--- If the grid line goes beyond the panel, interrupt the loop
      if(x1==1 && col_x>=x1+m_canvas.Width()-2)
         break;
      //--- Draw a vertical line of the table grid
      this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from the table data object 
      int total=this.m_table_data.RowsTotal();
      //--- In the loop by table rows
      for(int j=0;j<total;j++)
        {
         //--- get the next row
         CTableRow *row=m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create a new table cell 
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add the created cell to the row
         //--- (if adding an object failed, delete the created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In the created cell object, set its X coordinate and the Y coordinate from the row object 
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
  }

Die Logik der Methode und die Abfolge des Zeichnens einer Tabelle und des Erstellens ihrer Instanz in einem tabellarischen Datenobjekt sind im Code in fast jeder Zeile detailliert beschrieben. Die Daten, die in die Instanz der gezeichneten Tabelle im Tabellendatenobjekt geschrieben werden, werden benötigt, um die Koordinaten jeder Zelle zu erhalten, sodass es bequem ist, die erforderlichen Koordinaten anzugeben, wenn die Daten auf der Tafel angezeigt werden. Wir müssen nur die Zellennummer nach ihrer Position in der Tabelle (Zeile und Spalte) angeben und die Koordinaten der linken oberen Ecke dieser Zelle auf dem Panel ermitteln.


Die Methode zeichnet ein Hintergrundgitter mit automatischer Zellengröße:

//+------------------------------------------------------------------+
//| Draws the background grid with automatic cell sizing             |
//+------------------------------------------------------------------+
void CDashboard::DrawGridAutoFill(const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true)
  {
//--- If the panel is collapsed, leave
   if(this.m_minimized)
      return;
//--- X1 (left) table coordinate
   int x1=(int)border;
//--- X2 (right) table coordinate
   int x2=this.m_canvas.Width()-(int)border-1;
//--- Y1 (upper) table coordinate
   int y1=this.m_header_h+(int)border;
//--- Y2 (lower) table coordinate
   int y2=this.m_canvas.Height()-(int)border-1;

//--- Get the color of the table grid lines, either by default or passed to the method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- If the offset from the edge of the panel is greater than zero, draw a table border,
//--- otherwise, the panel border is used as the table border
   if(border>0)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));

//--- Height of the entire table grid
   int greed_h=y2-y1;
//--- Calculate the row height depending on the table height and the number of rows
   int row_h=(int)::round((double)greed_h/(double)rows);
//--- In the loop based on the number of rows
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate the Y coordinate of the next horizontal grid line (Y coordinate of the next table row)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" line colors is passed and the line is even
      if(alternating_color && i%2==0)
        {
         //--- lighten the table background color and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         this.m_canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw a table grid horizontal line
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create a new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to the list of rows of the tabular data object
      //--- (if adding an object failed, delete the created object)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Set its Y coordinate in the created row object taking into account the offset from the panel title
      row_obj.SetY(row_y-this.m_header_h);
     }
     
//--- Table grid width
   int greed_w=x2-x1;
//--- Calculate the column width depending on the table width and the number of columns
   int col_w=(int)::round((double)greed_w/(double)columns);
//--- In the loop by table columns,
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate the X coordinate of the next vertical grid line (X coordinate of the next table row)
      int col_x=x1+col_w*i;
      //--- If this is not the very first vertical line, draw it
      //--- (the first vertical line is either the table frame or the panel frame)
      if(i>0)
         this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from the table data object 
      int total=this.m_table_data.RowsTotal();
      //--- In the loop by table rows
      for(int j=0;j<total;j++)
        {
         //--- get the next row
         CTableRow *row=this.m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create a new table cell 
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add the created cell to the row
         //--- (if adding an object failed, delete the created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In the created cell object, set its X coordinate and the Y coordinate from the row object 
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
  }

Diese Methode unterscheidet sich von der oben beschriebenen nur durch die automatische Berechnung der Tabellenbreite und -höhe in Abhängigkeit vom Tabelleneinzug vom Rand der Platte, der Zeilenhöhe (Tabellenhöhe/Anzahl der Zeilen) und der Spaltenbreite (Tabellenbreite/Anzahl der Spalten). Sie ist auch direkt im Code vollständig kommentiert.

Wir müssen diese Namensschilder erstellen (auf die Tafel zeichnen), nachdem wir die Tafel erstellt und auf die Karte gezeichnet haben. Andernfalls wird das Namensschild nicht gezeichnet, aber die Tabellendaten werden alle berechnet und können verwendet werden. Mit anderen Worten: Das Zeichnen einer Tabelle nach der Anzeige eines Feldes auf einem Chart ist nur dann erforderlich, wenn eine Tabelle auf dem Feld gezeichnet werden soll.


Um den Hintergrund des Paneels und der Arbeitsfläche schnell wiederherzustellen, verwenden wir die Arrays, die die Pixel aus dem Bild der Arbeitsfläche und des Paneels erhalten, bevor das Paneel zusammengeklappt wird. Beim Erweitern eines Bedienfelds wird nicht alles neu gezeichnet, was zuvor darauf gezeichnet wurde, sondern der Hintergrund des Bedienfelds und der Arbeitsbereich werden einfach aus Pixelanordnungen wiederhergestellt. Das ist viel praktischer, als alles, was auf der Leinwand gezeichnet wurde, zu speichern, um es später neu zu zeichnen.

Es gibt zwei Methoden für das Panel und den Arbeitsbereich - für das Speichern eines Bildes in einem Pixelarray und für das Wiederherstellen eines Bildes aus einem Pixelarray.


Die Methode, die den Arbeitsbereich in der Pixelmatrix speichert:

//+------------------------------------------------------------------+
//| Save the working space to the array of pixels                    |
//+------------------------------------------------------------------+
void CDashboard::SaveWorkspace(void)
  {
//--- Calculate the required size of the array (width * height of the working space)
   uint size=this.m_workspace.Width()*this.m_workspace.Height();
//--- If the size of the array is not equal to the calculated one, change it
   if(this.m_array_wpx.Size()!=size)
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_wpx,size)!=size)
        {
         ::PrintFormat("%s: ArrayResize failed. Error %lu",(string)__FUNCTION__,::GetLastError());
         return;
        }
     }
   uint n=0;
//--- In the loop along the height of the working space (pixel Y coordinate)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- in the loop by the working space width (pixel X coordinate)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- calculate the pixel index in the receiving array
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- copy pixel to the receiving array from the working space X and Y
         this.m_array_wpx[n]=this.m_workspace.PixelGet(x,y);
        }
  }

Wir gehen jedes Pixel jeder Bildzeile in zwei verschachtelten Schleifen durch und kopieren sie in das empfangende Array.


Die Methode, die den Arbeitsbereich aus dem Array von Pixeln wiederherstellt:

//+------------------------------------------------------------------+
//| Restore the working space from the array of pixels               |
//+------------------------------------------------------------------+
void CDashboard::RestoreWorkspace(void)
  {
//--- Exit if the array is empty
   if(this.m_array_wpx.Size()==0)
      return;
   uint n=0;
//--- In the loop along the height of the working space (pixel Y coordinate)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- in the loop by the working space width (pixel X coordinate)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- calculate the pixel index in the array
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- copy the pixel from the array to the X and Y coordinates of the working space
         this.m_workspace.PixelSet(x,y,this.m_array_wpx[n]);
        }
  }

In zwei verschachtelten Schleifen berechnen wir den Index jedes Pixels jeder Bildzeile im Array und kopieren sie aus dem Array in die X- und Y-Koordinaten des Bildes.


Die Methode, die den Panel-Hintergrund in einem Pixel-Array speichert:

//+------------------------------------------------------------------+
//| Save the panel background to the pixel array                     |
//+------------------------------------------------------------------+
void CDashboard::SaveBackground(void)
  {
//--- Calculate the required size of the array (panel width * height)
   uint size=this.m_canvas.Width()*this.m_canvas.Height();
//--- If the size of the array is not equal to the calculated one, change it
   if(this.m_array_ppx.Size()!=size)
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_ppx,size)!=size)
        {
         ::PrintFormat("%s: ArrayResize failed. Error %lu",(string)__FUNCTION__,::GetLastError());
         return;
        }
     }
   uint n=0;
//--- In the loop by the panel height (pixel Y coordinate)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- in the loop by the panel width (pixel X coordinate)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- calculate the pixel index in the receiving array
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- copy pixel to the receiving array from the panel X and Y
         this.m_array_ppx[n]=this.m_canvas.PixelGet(x,y);
        }
  }


Die Methode stellt den Panelhintergrund aus der Pixelmatrix wieder her:

//+------------------------------------------------------------------+
//| Restore the panel background from the array of pixels            |
//+------------------------------------------------------------------+
void CDashboard::RestoreBackground(void)
  {
//--- Exit if the array is empty
   if(this.m_array_ppx.Size()==0)
      return;
   uint n=0;
//--- In the loop by the panel height (pixel Y coordinate)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- in the loop by the panel width (pixel X coordinate)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- calculate the pixel index in the array
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- copy the pixel from the array to the X and Y coordinates of the panel
         this.m_canvas.PixelSet(x,y,this.m_array_ppx[n]);
        }
  }


Um ein Objekt in den Vordergrund zu bringen, müssen wir zwei Operationen hintereinander durchführen: das Objekt ausblenden und sofort einblenden. Jedes grafische Objekt hat die Eigenschaft OBJPROP_TIMEFRAMES, die für seine Sichtbarkeit in jedem Zeitrahmen verantwortlich ist. Um ein Objekt in allen Zeitrahmen auszublenden, müssen wir diese Eigenschaft auf OBJ_NO_PERIODS setzen. Dementsprechend müssen wir, um das Objekt anzuzeigen, die Eigenschaft OBJPROP_TIMEFRAMES auf OBJ_ALL_PERIODS setzen.


Die Methode, die das Panel versteckt:

//+------------------------------------------------------------------+
//| Hide the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Hide(const bool redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   ::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   if(redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Bei Tafel- und Arbeitsflächenobjekten wird die Eigenschaft OBJPROP_TIMEFRAMES auf OBJ_NO_PERIODS gesetzt, und das Chart wird neu gezeichnet, um die Änderungen sofort widerzuspiegeln (wenn das entsprechende Flag gesetzt ist).


Die Methode, die das Panel anzeigt:

//+------------------------------------------------------------------+
//| Display the panel                                                |
//+------------------------------------------------------------------+
void CDashboard::Show(const bool redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
   if(!this.m_minimized)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
   if(redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Für das Panel-Objekt wird die Eigenschaft OBJPROP_TIMEFRAMES sofort auf OBJ_ALL_PERIODS gesetzt. Für das Objekt Arbeitsbereich wird der Wert nur gesetzt, wenn sich das Panel im aufgeklappten Zustand befindet.

Wenn das Flag zum Neuzeichnen aktiviert ist, wird das Chart neu gezeichnet, um die Änderungen sofort anzuzeigen.


Die Methode, die das Panel in den Vordergrund bringt:

//+------------------------------------------------------------------+
//| Bring the panel to the foreground                                |
//+------------------------------------------------------------------+
void CDashboard::BringToTop(void)
  {
   this.Hide(false);
   this.Show(true);
  }

Zunächst werden das Bedienfeld und der Arbeitsbereich ausgeblendet, ohne dass das Chart neu gezeichnet wird, dann werden sie sofort angezeigt und das Chart wird neu gezeichnet.

In einigen Fällen, in denen Pixelarrays nicht im Speicher abgelegt werden können, müssen sie in Dateien gespeichert und dann von dort geladen werden. Die Klasse enthält Methoden zum Speichern von Pixelarrays in einer Datei und zum Laden von Pixelarrays aus der Datei:

//+------------------------------------------------------------------+
//| Save the pixel array of the working space to a file              |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveWorkspace(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- If the saved array is empty, inform of that and return 'false'
   if(this.m_array_wpx.Size()==0)
     {
      ::PrintFormat("%s: Error. The workspace pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- If the array could not be saved to a file, report this and return 'false'
   if(!::FileSave(filename,this.m_array_wpx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Save the pixel array of the panel background to a file           |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveBackground(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- If the saved array is empty, inform of that and return 'false'
   if(this.m_array_ppx.Size()==0)
     {
      ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- If the array could not be saved to a file, report this and return 'false'
   if(!::FileSave(filename,this.m_array_ppx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Upload the array of working space pixels from a file             |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadWorkspace(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- If failed to upload data from the file into the array, report this and return 'false'
   if(::FileLoad(filename,this.m_array_wpx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Upload the array of panel background pixels from a file          |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadBackground(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- If failed to upload data from the file into the array, report this and return 'false'
   if(::FileLoad(filename,this.m_array_ppx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }

Derzeit ist die Handhabung dieser Methoden noch nicht implementiert, da die Notwendigkeit dafür erst zum Zeitpunkt der Erstellung dieses Artikels entdeckt wurde. Künftige Artikel über die Dashboard-Klasse werden diese Methoden wahrscheinlich verwenden.


Indikator mit Dashboard

Um das Dashboard zu testen, erstellen wir einen einfachen Indikator, der einen regelmäßigen gleitenden Durchschnitt zeichnet. Es werden die aktuellen Geld- und Briefkurse sowie die Daten der Kerze angezeigt, über der sich der Mauszeiger gerade befindet.

Wir erstellen im Ordner Indicators einen neuen Ordner TestDashboard mit einem neuen nutzerdefinierten Indikator:


Dann legen wir die Parameter fest:


Wir wählen den ersten OnCalculate-, OnChartEvent- und OnTimer-Typ für den Fall weiterer Verbesserungen:


Wir wählen einen zu zeichnenden Puffer aus und klicken auf Fertig stellen:


Wir erhalten die folgende Vorlage:

//+------------------------------------------------------------------+
//|                                                TestDashboard.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot MA
#property indicator_label1  "MA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      InpPeriodMA=10;
input int      InpMethodMA=0;
input int      InpPriceMA=0;
input int      InpPanelX=20;
input int      InpPanelY=20;
input int      InpUniqID=0;
//--- indicator buffers
double         MABuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,MABuffer,INDICATOR_DATA);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//---
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   
  }
//+------------------------------------------------------------------+

Wir speichern die Panel-Objektklassendatei im erstellten Indikatorordner, sodass sich die enthaltene Panel-Klassendatei im selben Ordner wie der Indikator befindet. Nach der Fertigstellung der Panel-Klasse kann ihre endgültige Version im Include-Ordner der Datei-Sandbox des Handelsterminals abgelegt werden, um in nutzerdefinierten Programmen verwendet zu werden.

Passen wir jetzt noch die erstellte Indikatorvorlage an. Wir binden die Panel-Objektklasse ein, initialisieren die Eingänge mit verständlicheren Anfangswerten, legen den Namen des zu zeichnenden Puffers fest und deklarieren die globalen Variablen:

//+------------------------------------------------------------------+
//|                                                TestDashboard.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot MA
#property indicator_label1  "MA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- includes
#include "Dashboard.mqh"
//--- input variables
input int                  InpPeriodMA =  10;            /* MA Period   */ // Moving average calculation period
input ENUM_MA_METHOD       InpMethodMA =  MODE_SMA;      /* MA Method   */ // Moving average calculation method
input ENUM_APPLIED_PRICE   InpPriceMA  =  PRICE_CLOSE;   /* MA Price    */ // Moving average calculation price
input int                  InpPanelX   =  20;            /* Dashboard X */ // Panel X coordinate
input int                  InpPanelY   =  20;            /* Dashboard Y */ // Panel Y coordinate
input int                  InpUniqID   =  0;             /* Unique ID   */ // Unique ID for the panel object
//--- indicator buffers
double         BufferMA[];
//--- global variables
CDashboard    *dashboard=NULL;
int            handle_ma;        // Moving Average indicator handle
int            period_ma;        // Moving Average calculation period
int            mouse_bar_index;  // Index of the bar the data is taken from
string         plot_label;       // Name of the graphical indicator series displayed in DataWindow
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

In OnInit() erstellen wir das Handle des Standardindikators gleitenden Durchschnitts, setzen Sie die Parameter des Indikators und den zu zeichnenden Puffer. Da der Indikator vom Beginn der Historie bis zu den aktuellen Daten berechnet wird, legen wir die Indizierung des Indikatorpuffers wie bei der Zeitreihe fest. Wir erstellen das Dashboard-Objekt in demselben Handler. Wir zeigen das Objekt sofort nach der Erstellung an und zeichnen das Tabellengitter. Anschließend senden wir die tabellarischen Daten an das Journal:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferMA,INDICATOR_DATA);

//--- Create indicator handle
   period_ma=(InpPeriodMA<1 ? 1 : InpPeriodMA);
   ResetLastError();
   handle_ma=iMA(Symbol(),PERIOD_CURRENT,period_ma,0,InpMethodMA,InpPriceMA);
   if(handle_ma==INVALID_HANDLE)
     {
      PrintFormat("%s Failed to create MA indicator handle. Error %lu",__FUNCTION__,GetLastError());
      return INIT_FAILED;
     }
//--- Set the indicator parameters
   IndicatorSetInteger(INDICATOR_DIGITS,Digits());
   IndicatorSetString(INDICATOR_SHORTNAME,"Test Dashboard");
//--- Set the parameters of the buffer being drawn
   plot_label="MA("+(string)period_ma+","+StringSubstr(EnumToString(Period()),7)+")";
   PlotIndexSetString(0,PLOT_LABEL,plot_label);
   ArraySetAsSeries(BufferMA,true);

//--- Create the panel object
   dashboard=new CDashboard(InpUniqID,InpPanelX,InpPanelY,200,250);
   if(dashboard==NULL)
     {
      Print("Error. Failed to create dashboard object");
      return INIT_FAILED;
     }
//--- Display the panel with the "Symbol, Timeframe description" header text
   dashboard.View(Symbol()+", "+StringSubstr(EnumToString(Period()),7));
//--- Draw the name plate on the panel background
   dashboard.DrawGridAutoFill(2,12,2);
   //dashboard.DrawGrid(2,1,12,2,19,97);
//--- Display tabular data in the journal
   dashboard.GridPrint(2);
//--- Initialize the variable with the index of the mouse cursor bar
   mouse_bar_index=0;
//--- Successful initialization
   return(INIT_SUCCEEDED);
  }

Die gesamte Logik ist im Code kommentiert. Die Tabelle wird mit automatischer Berechnung der Zeilen- und Spaltengrößen erstellt. Die Erstellung einer einfachen Tabelle ist im Code auskommentiert. Sie können das automatische Namensschild auskommentieren, das einfache auskommentieren und den Indikator neu kompilieren. Der Unterschied wird angesichts der aktuellen Tabellenparameter unbedeutend sein.

In OnDeinit() entfernen wir das Panel, geben den Indikator-Handle frei und löschen die Chart-Kommentare:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- If the panel object exists, delete it
   if(dashboard!=NULL)
      delete dashboard;
//--- Release the handle of the MA indicator
   ResetLastError();
   if(!IndicatorRelease(handle_ma))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
//--- Delete all comments
   Comment("");
  }

In OnCalculate() haben alle vordefinierten Zeitreihen-Arrays die gleiche Indexierung wie die Zeitreihen, sodass sie mit der Indexierung des Zeichenpuffers übereinstimmen. Alles Weitere ist in den Kommentaren zum Handler-Code beschrieben:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//--- Set indexing for the arrays as in a timeseries
   ArraySetAsSeries(time,true);
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Check for the minimum number of bars for calculation
   if(rates_total<period_ma) return 0;

//--- Check and calculate the number of calculated bars
   int limit=rates_total-prev_calculated;
//--- If 'limit' is 0, then only the current bar is calculated
//--- If 'limit' is 1 (opening a new bar), then two bars are calculated - the current newly opened one and the previous one
//--- If 'limit' is more than 1, then this is either the first launch of the indicator, or some changes in history - the indicator is completely recalculated
   if(limit>1)
     {
      limit=rates_total-period_ma-1;
      ArrayInitialize(BufferMA,EMPTY_VALUE);
     }
     
//--- Calculate the amount of data copied from the indicator handle to the drawing buffer
   int count=(limit>1 ? rates_total : 1),copied=0;
//--- Prepare data (receive data to the moving average buffer by handle)
   copied=CopyBuffer(handle_ma,0,0,count,BufferMA);
   if(copied!=count) return 0;

//--- Loop of indicator calculation based on the moving average data
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      // Here we calculate a certain indicator based on the standard Moving Average data
     }

//--- Receive price and timeseries data and display it on the panel
//--- At the first launch, we display the data of the current bar on the panel
   static bool first=true;
   if(first)
     {
      DrawData(0,TimeCurrent());
      first=false;
     }

//--- Declare the price structure and get the current prices
   MqlTick  tick={0};
   if(!SymbolInfoTick(Symbol(),tick))
      return 0;

//--- If the cursor is on the current bar, display the data of the current bar on the panel
   if(mouse_bar_index==0)
      DrawData(0,time[0]);
//--- Otherwise, display only the Bid and Ask prices on the panel (we update the prices on the panel at each tick)
   else
     {
      dashboard.DrawText("Bid",dashboard.CellX(0,0)+2,dashboard.CellY(0,0)+2); dashboard.DrawText(DoubleToString(tick.bid,Digits()),dashboard.CellX(0,1)+2,dashboard.CellY(0,1)+2,90);
      dashboard.DrawText("Ask",dashboard.CellX(1,0)+2,dashboard.CellY(1,0)+2); dashboard.DrawText(DoubleToString(tick.ask,Digits()),dashboard.CellX(1,1)+2,dashboard.CellY(1,1)+2,90);
     }
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }

In OnChartEvent() ruft der Indikator zunächst OnChartEvent die Panelbehandlung auf und verarbeitet dann die erforderlichen Ereignisse:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Call the panel event handler
   dashboard.OnChartEvent(id,lparam,dparam,sparam);

//--- If the cursor moves or a click is made on the chart
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
     {
      //--- Declare the variables to record time and price coordinates in them
      datetime time=0;
      double price=0;
      int wnd=0;
      //--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- write the bar index where the cursor is located to a global variable
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         //--- Display the bar data under the cursor on the panel 
         DrawData(mouse_bar_index,time);
        }
     }

//--- If we received a custom event, display the appropriate message in the journal
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id (lparam): %lu, event message (sparam): %s",__FUNCTION__,id,lparam,sparam);
     }
  }

Hier, wenn ein Ereignis vom Panel empfangen wird, wenn die Schaltfläche „close“ angeklickt wird, und wenn es notwendig ist, auf ein solches Ereignis zu reagieren, müssen wir seine Verarbeitung registrieren. Die Entscheidung über das Programmverhalten bei diesem Ereignis liegt beim Programmentwickler.


Die Funktion, die die aktuellen Kurse und Taktdaten nach Index anzeigt:

//+------------------------------------------------------------------+
//| Display data from the specified timeseries index to the panel    |
//+------------------------------------------------------------------+
void DrawData(const int index,const datetime time)
  {
//--- Declare the variables to receive data in them
   MqlTick  tick={0};
   MqlRates rates[1];
//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      return;
//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1)
      return;
//--- Display the current prices and data of the specified bar on the panel
   dashboard.DrawText("Bid",        dashboard.CellX(0,0)+2,   dashboard.CellY(0,0)+2);  dashboard.DrawText(DoubleToString(tick.bid,Digits()),       dashboard.CellX(0,1)+2,   dashboard.CellY(0,1)+2,90);
   dashboard.DrawText("Ask",        dashboard.CellX(1,0)+2,   dashboard.CellY(1,0)+2);  dashboard.DrawText(DoubleToString(tick.ask,Digits()),       dashboard.CellX(1,1)+2,   dashboard.CellY(1,1)+2,90);
   dashboard.DrawText("Date",       dashboard.CellX(2,0)+2,   dashboard.CellY(2,0)+2);  dashboard.DrawText(TimeToString(rates[0].time,TIME_DATE),   dashboard.CellX(2,1)+2,   dashboard.CellY(2,1)+2,90);
   dashboard.DrawText("Time",       dashboard.CellX(3,0)+2,   dashboard.CellY(3,0)+2);  dashboard.DrawText(TimeToString(rates[0].time,TIME_MINUTES),dashboard.CellX(3,1)+2,   dashboard.CellY(3,1)+2,90);
   
   dashboard.DrawText("Open",       dashboard.CellX(4,0)+2,   dashboard.CellY(4,0)+2);  dashboard.DrawText(DoubleToString(rates[0].open,Digits()),  dashboard.CellX(4,1)+2,   dashboard.CellY(4,1)+2,90);
   dashboard.DrawText("High",       dashboard.CellX(5,0)+2,   dashboard.CellY(5,0)+2);  dashboard.DrawText(DoubleToString(rates[0].high,Digits()),  dashboard.CellX(5,1)+2,   dashboard.CellY(5,1)+2,90);
   dashboard.DrawText("Low",        dashboard.CellX(6,0)+2,   dashboard.CellY(6,0)+2);  dashboard.DrawText(DoubleToString(rates[0].low,Digits()),   dashboard.CellX(6,1)+2,   dashboard.CellY(6,1)+2,90);
   dashboard.DrawText("Close",      dashboard.CellX(7,0)+2,   dashboard.CellY(7,0)+2);  dashboard.DrawText(DoubleToString(rates[0].close,Digits()), dashboard.CellX(7,1)+2,   dashboard.CellY(7,1)+2,90);
   
   dashboard.DrawText("Volume",     dashboard.CellX(8,0)+2,   dashboard.CellY(8,0)+2);  dashboard.DrawText((string)rates[0].real_volume,            dashboard.CellX(8,1)+2,   dashboard.CellY(8,1)+2,90);
   dashboard.DrawText("Tick Volume",dashboard.CellX(9,0)+2,   dashboard.CellY(9,0)+2);  dashboard.DrawText((string)rates[0].tick_volume,            dashboard.CellX(9,1)+2,   dashboard.CellY(9,1)+2,90);
   dashboard.DrawText("Spread",     dashboard.CellX(10,0)+2,  dashboard.CellY(10,0)+2); dashboard.DrawText((string)rates[0].spread,                 dashboard.CellX(10,1)+2,  dashboard.CellY(10,1)+2,90);
   dashboard.DrawText(plot_label,   dashboard.CellX(11,0)+2,  dashboard.CellY(11,0)+2); dashboard.DrawText(DoubleToString(BufferMA[index],Digits()),dashboard.CellX(11,1)+2,  dashboard.CellY(11,1)+2,90);
//--- Redraw the chart to immediately display all changes on the panel
   ChartRedraw(ChartID());
  }

Wenn wir auf die Methode DrawText des Klasse des Panels achten:

void CDashboard::DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE)

können wir sehen, dass die X- und Y-Koordinaten nach dem Text an die Methode übergeben werden. Dies ist das, was wir von Tabellendaten erhalten, wobei die Position der Tabellenzelle durch ihre Zeilen- und Spaltennummer angegeben wird.

Zum Beispiel werden Geld- und Briefkurs (Bid und Ask) auf dem Panel an der „Adresse“ der Tabellenzellen für Bid 0,0 (Text „Bid“) und 0,1 (Geldkurs) angezeigt:

dashboard.DrawText("Bid"dashboard.CellX(0,0)+2, dashboard.CellY(0,0)+2);  dashboard.DrawText(DoubleToString(tick.bid,Digits()), dashboard.CellX(0,1)+2, dashboard.CellY(0,1)+2,90);

Die Zellenwerte werden hier übernommen

für den Text „Bid“:

  • CellX(0,0) — Zelle in der Zeile und Spalte Null — X-Koordinatenwert,
  • CellY0,0) — Zelle in der Zeile und Spalte Null — Y-Koordinatenwert.

für den Bid-Preis:

  • CellX(0,1) — Zelle in der Zeile Null und Spalte Eins — X-Koordinatenwert,
  • CellY0,1) — Zelle in der Zeile Null und Spalte Eins — Y-Koordinatenwert.

Der Wert 90 für die angezeigte Textbreite in der zweiten Zelle weist auf den Fall hin, dass der aktuelle Text weniger breit sein kann als der vorherige. Daher wird der vorherige Text nicht vollständig gelöscht, und die beiden Texte überschneiden sich. Deshalb geben wir hier ausdrücklich die Breite der angezeigten Beschriftung an, die garantiert den zuvor gezeichneten Text löscht, aber nicht die angrenzenden Daten, da das Tabellenfeld, in das der Text geschrieben wird, breiter als 90 Pixel ist.

Auf diese Weise können wir für jede Tabellenzelle ihre Koordinaten im Panel abrufen und Text darin anzeigen. Da die Koordinaten für die Schnittpunkte der Tabellengitterlinien angegeben werden, werden zu den Koordinaten in X und Y zwei Pixel hinzugefügt, um den Text innerhalb der Tabellenzellen auszurichten.

Nach der Erstellung des Indikators und dem Start im Chart werden die Daten aus der erstellten und im Panel gezeichneten Tabelle im Journal angezeigt:

Table: Rows: 12, Columns: 2
  Row   0    Column   0      Cell X:  2      Cell Y:  2   
  Row   0    Column   1      Cell X:  100    Cell Y:  2   
  Row   1    Column   0      Cell X:  2      Cell Y:  21  
  Row   1    Column   1      Cell X:  100    Cell Y:  21  
  Row   2    Column   0      Cell X:  2      Cell Y:  40  
  Row   2    Column   1      Cell X:  100    Cell Y:  40  
  Row   3    Column   0      Cell X:  2      Cell Y:  59  
  Row   3    Column   1      Cell X:  100    Cell Y:  59  
  Row   4    Column   0      Cell X:  2      Cell Y:  78  
  Row   4    Column   1      Cell X:  100    Cell Y:  78  
  Row   5    Column   0      Cell X:  2      Cell Y:  97  
  Row   5    Column   1      Cell X:  100    Cell Y:  97  
  Row   6    Column   0      Cell X:  2      Cell Y:  116 
  Row   6    Column   1      Cell X:  100    Cell Y:  116 
  Row   7    Column   0      Cell X:  2      Cell Y:  135 
  Row   7    Column   1      Cell X:  100    Cell Y:  135 
  Row   8    Column   0      Cell X:  2      Cell Y:  154 
  Row   8    Column   1      Cell X:  100    Cell Y:  154 
  Row   9    Column   0      Cell X:  2      Cell Y:  173 
  Row   9    Column   1      Cell X:  100    Cell Y:  173 
  Row   10   Column   0      Cell X:  2      Cell Y:  192 
  Row   10   Column   1      Cell X:  100    Cell Y:  192 
  Row   11   Column   0      Cell X:  2      Cell Y:  211 
  Row   11   Column   1      Cell X:  100    Cell Y:  211 


Wenn wir zwei Indikatoren mit einem Panel auf demselben Chart starten und dabei unterschiedliche Werte für die eindeutige ID des Panels angeben, funktionieren sie unabhängig voneinander:


Hier sehen wir, dass die Felder zwar getrennt funktionieren, aber jeder Indikator sein eigenes Feld hat. Allerdings gibt es auch einen Konflikt: Wenn die Tafeln bewegt werden, versucht auch das Chart, sich zu bewegen. Dies geschieht, weil ein Feld, das wir verschieben, das Verschieben des Charts deaktiviert, während das zweite es aktiviert, wenn wir sehen, dass der Cursor außerhalb des Feldes ist.

Um dieses Verhalten loszuwerden, ist es am einfachsten, eine Semaphore in den globalen Terminalvariablen anzulegen, in die die ID des aktiven Panels geschrieben wird. Die anderen werden sich nicht in die Verwaltung der Karte einmischen, wenn sie dort etwas anderes als ihre ID sehen.

Wenn wir den Indikator im visuellen Modus des Testers laufen lassen und versuchen, das Panel zu verschieben, bewegt es sich mit einigen Schwierigkeiten über den Bildschirm. Gleichzeitig können die Daten aus den Balken des getesteten Charts abgerufen werden - wenn Sie auf einen Balken klicken, werden seine Daten auf dem Panel angezeigt. Auch mit der rechten Maustaste (halten Sie sie gedrückt und bewegen Sie den Cursor entlang des Charts) können Sie das Feld anzeigen, in dem sich der Cursor gerade befindet, und die Daten anzeigen oder das Feld am Kopfbereich anfassen und an die gewünschte Stelle verschieben. Leider müssen wir im visuellen Modus des Testers aufgrund der unvollständigen Implementierung der Ereignisbehandlung auf solche Tricks zurückgreifen.


Schlussfolgerung

Heute haben wir ein kleines Panel erstellt, das bei der Entwicklung nutzerdefinierter Strategien unter Verwendung von Indikatoren helfen kann. In den folgenden Artikeln werden wir uns mit der Einbeziehung von Indikatoren und der Handhabung ihrer Daten in EAs für alle Standardindikatoren befassen.


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

Beigefügte Dateien |
Dashboard.mqh (195.26 KB)
TestDashboard.mq5 (24.81 KB)
Neuronale Netze leicht gemacht (Teil 56): Nuklearnorm als Antrieb für die Erkundung nutzen Neuronale Netze leicht gemacht (Teil 56): Nuklearnorm als Antrieb für die Erkundung nutzen
Die Untersuchung der Umgebung beim Verstärkungslernen ist ein dringendes Problem. Wir haben uns bereits mit einigen Ansätzen beschäftigt. In diesem Artikel werden wir uns eine weitere Methode ansehen, die auf der Maximierung der Nuklearnorm beruht. Es ermöglicht den Agenten, Umgebungszustände mit einem hohen Maß an Neuartigkeit und Vielfalt zu erkennen.
GUI: Tipps und Tricks zur Erstellung Ihrer eigenen Grafikbibliothek in MQL GUI: Tipps und Tricks zur Erstellung Ihrer eigenen Grafikbibliothek in MQL
Wir gehen die Grundlagen von GUI-Bibliotheken durch, damit Sie verstehen, wie sie funktionieren, oder sogar anfangen können, Ihre eigenen zu erstellen.
Entwicklung eines MQTT-Clients für MetaTrader 5: ein TDD-Ansatz — Teil 3 Entwicklung eines MQTT-Clients für MetaTrader 5: ein TDD-Ansatz — Teil 3
Dieser Artikel ist der dritte Teil einer Serie, die unsere Entwicklungsschritte für einen nativen MQL5-Client für das MQTT-Protokoll beschreibt. In diesem Teil wird detailliert beschrieben, wie wir die testgetriebene Entwicklung nutzen, um den Teil des Betriebsverhaltens des CONNECT/CONNACK-Paketaustauschs zu implementieren. Am Ende dieses Schritts MUSS unser Client in der Lage sein, sich angemessen zu verhalten, wenn er mit einem der möglichen Ergebnisse eines Verbindungsversuchs auf dem Server konfrontiert wird.
Kategorientheorie in MQL5 (Teil 22): Ein anderer Blick auf gleitende Durchschnitte Kategorientheorie in MQL5 (Teil 22): Ein anderer Blick auf gleitende Durchschnitte
In diesem Artikel versuchen wir, die in dieser Reihe behandelten Konzepte zu vereinfachen, indem wir uns auf einen einzigen Indikator beschränken, der am häufigsten vorkommt und wahrscheinlich am leichtesten zu verstehen ist. Der gleitende Durchschnitt. Dabei betrachten wir die Bedeutung und die möglichen Anwendungen von vertikalen natürlichen Transformationen.