Grafische Interfaces I: Vorbereitung der Bibliotheksstruktur (Kapitel 1)

Anatoli Kazharski | 26 Mai, 2016

Inhalt

 

Einleitung

Dieser Artikel ist der Anfang einer weiteren Serie über die Entwicklung von grafischen Interfaces. Zur Zeit gibt es keine einzige Bibliothek, die es einem ermöglichen würde, einfach und schnell qualitative graphische Interfaces innerhalb einer MQL- Anwendung zu erzeugen. Damit meine ich graphische Interfaces, wie wir sie auch von anderen Betriebssystemen her kennen.

Das Projektziel besteht darin, dem End-Benutzer diese Möglichkeit zu bieten und ihm zu zeigen, wie man es mithilfe dieser Bibliothek nutzen kann. Mein Ziel war es, diese Studie so einfach wie möglich zu halten, damit noch viel Freiraum für weitere Entwicklungen bleibt.

Es lohnt sich die von Dmitry Fedoseev geteilte Bibliothek zu erwähnen, die er in seiner Serie von Artikeln veröffentlicht hat:

Beide Bibliotheken, die hier vorgeschlagene und die Standardbibliothek, haben Vorteile, aber auch Nachteile. Dimitry hat eine detaillierte Beschreibung in Form einer Bedienungsanleitung und ein breiteres Spektrum von Elementen der Benutzeroberfläche zur Verfügung gestellt, als es in der Standardbibliothek zu Verfügung steht. Allerdings stimme ich nicht mit einigen Teilen der Funktionalität und der implementierten Kontrollmechanismen überein. Einige Benutzer haben ihre Vorschläge in den Kommentaren aufgelistet und ich habe diese für diesen Artikel in Betracht gezogen.

Die Standardbibliothek besitzt eine detaillierte Beschreibung und meiner Meinung nach, ist die Struktur besser gelungen, aber immer noch nicht bis zum Ende durchdacht. Zudem fehlen der Standardbibliothek einige Controls, die für die Erzeugung von grafischen Interfaces notwendig sind. Beide Implementationen haben den Nachteil, dass wenn eine Modernisierung erfolgen muss, die Bibliothek von Grund neu aufgebaut werden muss. In meinem Artikel habe ich den Versuch gestartet, alle Vorteile zu sammeln und alle Nachteile nach Möglichkeit zu eliminieren.

Zuvor habe ich schon zwei einfache Artikel über die Verwendung von Controls geschrieben:

Diese sind prozedural geschrieben und deren Zweck war es ein wenig Know-how über MQL zu erlangen. Nun ist es an der Zeit eine komplexere Struktur mit einem Beispielprojekt zu beschreiben, welches mithilfe der objektorientierten Programmierung implementiert wurde.

Wie profitieren die Leser durch das Lesen dieses Artikels?

Ich verwende die Methode der Darstellung, wie sie in der Serie von den folgenden Artikeln verwendet wird: "Ein Versuch eine perfekte Sequenz zu simulieren" Oft ist die Reihenfolge der Aktionen und die Linie des Denkens bei der Entwicklung von großen Projekten mehr chaotisch und besteht aus vielen Experimenten, aus Versuchen und auch aus Irrtümern. Hier versuchen wir alle diese Komplikationen zu umgehen. Diejenigen, die sich zum ersten Mal mit Projekten in dieser Größe auseinandersetzen, wird empfohlen, alle Schritte öfters zu wiederholen, um den hier dargelegten Stoff und die Prozesse in der entwicklung besser verstehen zu können. Dieser Artikel gibt die Möglichkeit, alle Gedankengänge in der idealen Reihenfolge durchzugehen, da alle Antworten auf die meisten Fragen bereits vorhanden sind und jeder weitere Teil des Projektes nur nach Bedarf erstellt wird.

 

Liste der Controls

Also was soll diese Bibliotheken nun darstellen? Wie soll die objektorientierte Struktur dieser Bibliothek aussehen? Womit sollen wir starten? Tatsächlich gibt es zu Anfang immer eine ganze Menge Fragen. Lassen Sie uns zunächst definieren, welche Controls und Interface-Elemente für die Entwicklung einer MQL Anwendung notwendig sind. Jeder stellt seine eigenen Anforderungen und die Bandbreite von Ideen variiert von Person zu Person. Für den Einen sind eine bestimmte Menge von Buttons und Checkboxen ausreichend. Andere wiederum denken über Multi-Fenster-Interfaces nach, welche unterschiedliche Arten von Datenauswahlmöglichkeiten und Controls besitzen sollten.

Ich möchte an dieser Stelle darauf hinweisen dass die hier beschriebene Implementation letztendlich als Endprodukt in der Handelsplattform MetaTrader 4 und MetaTrader 5genutzt werden kann. Wenn wir das Ergebnis mit einigen Idealvorstellungen vergleichen, dann gibt es natürlich auch noch Luft nach oben. Nachdem alle Artikel dieser Serie veröffentlicht worden sind, werde ich meine Gedanken, wie die ideale Implementation einer Bibliothek für die Erstellung von Interfaces in MQL aussehen könnte, mit Ihnen teilen.

Die erste Version dieser Bibliothek wird Interface-Elemente enthalten, wie sie in der nachfolgenden Liste zu sehen sind. Einige von denen werden in unterschiedlichen Variationen und separaten Klassen implementiert, wobei jedes dieser Elemete für einen bestimmten Zweck entwickelt wurde. Das bedeutet, dass einige besser für diesen Fall und die anderen besser für jenen Fall geeignet sind.

Die oben dargestellte Liste enthält Steuerelemente, die für ihre Funktion weitere Elemente aus dieser Liste benötigen. Zum Beispiel benötigt das in der oben aufgeführten Aufzählung verwendete Control des Typs combobox das List View Control, und List View wiederum beinhaltet einen vertikalen Scroll. Die horizontalen und vertikalen Scrollbars werden auch in allen Arten der Tabellen verwendet. Der Drop-down Kalender beinhaltet ebenso denKalender, welcher als ein separates Control verwendet werden kann. Wie jedes Element erzeugt wird, wird eingehend betrachtet, nachdem wir die Projektstruktur festgelegt haben.

 

Basisklasse der Standardbibliothek als einfache Objekte

Wir werden auch einige Klassen der Standardbibliothek verwenden. Diese besitzen die primitiven grafischen Elemente, die wiederum Bestandteile aller weiteren Bedienelemente sind. Jede dieser Klassen erlaubt eine schnelle Erstellung oder Löschung von grafischen Objekten, sowie den Zugriff auf deren Eigenschaften.

Die Dateien von den Klassen mit den einfachen grafischen Objekten finden Sie hier:

Der Artikel Marktbeobachtung mithilfe vorgefertigter Klassen gibt einen umfangreichen Einblick in die Verwendung von Klassen. Daher werden wir in diesem Artikel darauf nicht weiter eingehen. Ich möchte lediglich in Erinnerung rufen, dass die Basisklasse dieser Gruppe von Klassen CObject ist. Die Klasse CChartObject wird von dieser abgeleitet. Sie beinhaltet Methoden, die von allen grafischen Objekten verwendet werden. Alle anderen Klassen werden von der CChartObject Klasse abgeleitet und beinhalten Methoden für die Verwaltung von eindeutigen Eigenschaften von jedem einzelnen grafischen Objekt.

Die gemeinsame Struktur der Beziehungen zwischen der Standardbibliothek-Klasse und den grafischen Objekten kann wie folgt dargestellt werden: Lassen Sie uns festlegen, dass ein blauer Pfeil auf die Verbindung zwischen einer Basisklasse und seiner Ableitung hindeutet.

Abbildung  1. Die gemeinsame Struktur der Verbindungen innerhalb der Klasse der Standardbibliothek.

Abbildung 1. Die gemeinsame Struktur der Verbindungen innerhalb der Klasse der Standardbibliothek.

Während wir unsere Bibliothek entwickeln, werden wir die Struktur mit ähnlichen Diagrammen darstellen. Für eine vereinfachte Darstellung werden wir eine verkürzte Version, wie folgt gezeigt wird, verwenden. Das bedeutet, dass das letzte Element in dem dargestellten Diagramm, jedes (...) grafische Objekt aus dem obigen Diagramm sein kann.

Abbildung  2. Gekürzte Version der Struktur der Standardbibliothek für grafische Objekte.

Abbildung 2. Gekürzte Version der Struktur der Standardbibliothek für grafische Objekte.

Um Grafikschnittstellen bauen zu können, benötigen wir nur ein paar dieser Klassen:

Sie alle teilen sich die gemeinsame Eigenschaft, dass sie sich nicht bewegen, wenn der Chart gescrollt wird. Dann benötigt jedes dieser einfachen Objekte eine abgeleitete Klasse, welche einige Eigenschaften abspeichert, die sehr oft gebraucht werden:

In diesen Klassen müssen auch die entsprechenden Methoden für die Erlangung und Aufrechterhaltung der Werte dieser Eigenschaften enthalten sein. In der Tat können die Werte dieser Eigenschaften auch unter Verwendung von Methoden der Basisklasse zugewiesen werden. Aber dieser Ansatz ist sehr verschwenderisch in Bezug auf den Verbrauch von Ressourcen.

 

Abgeleitete Klasse von primitiven Objekten mit zusätzlichen Methoden

Erzeugen Sie in dem Verzeichnis <data folder>\MQL5\Include (für MetaTrader 4: <data folder>\MQL4\Include) einen Ordner mit dem Namen EasyAndFastGUI. Wir werden alle Dateien für unsere Bibliothek in diesem Ordner ablegen. Sie finden das Datenverzeichnis in dem Hauptmenü des Metatrader oder des MetaEditors in dem Sie File > Open Data Folder wählen. In dem EasyAndFastGUI Verzeichnis, werden alle Dateien der Bibliothek für die Erzeugung von grafischen Interfaces in dem UnterverzeichnisControls abgelegt. Erzeugen sie nun in dem Controls Unterverzeichnis eine Datei mit dem Namen Objects.mqh. Sie wird die zuvor genannten abgeleiteten Klassen beinhalten.

In dem Kopf der DateiObjects.mqh müssen wir die Dateien der Standardbibliothek miteinbeziehen:

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <ChartObjects\ChartObjectsBmpControls.mqh>
#include <ChartObjects\ChartObjectsTxtControls.mqh>

Alle Klassen für die oben gelisteten Objekte sind vom selben Typ und daher zeige ich hier nur den Code für eine dieser Klassen. Tatsächlich passt jede dieser Methoden in eine einzige Zeile. Um Platz zu sparen und Information in einer komprimierten Form darzustellen, werden diese Methoden in dem Klassen-Körper (body) untergebracht.

Die Initialisierung aller Variablen findet in dem Konstruktor statt. Ein Teil von denen steht in der Liste der Initialisierung eines Konstruktors (Vor dem Klassen-Körper). In Wirklichkeit ist es nicht so wichtig, da wir bei der Entwicklung der gesamten Bibliothek keinen einzigen Fall hatten, bei dem die Initialisierungs-Sequenz von Feldern für abgeleitete und Basisklassen erforderlich war. Daher können in den Klassen-Körpern alle Variablen initialisiert werden oder lediglich diejenigen, die ein paar Kommentare benötigen.

//+-----------------------------------------------------------------------+
//| Klasse mit zusätzlichen Eigenschaften für das Objekt Rectangle Label  |
//+-----------------------------------------------------------------------+
class CRectLabel : public CChartObjectRectLabel
  {
protected:
   int               m_x;
   int               m_y;
   int               m_x2;
   int               m_y2;
   int               m_x_gap;
   int               m_y_gap;
   int               m_x_size;
   int               m_y_size;
   bool              m_mouse_focus;
public:
                     CRectLabel(void);
                    ~CRectLabel(void);
   //--- Coordinates
   int               X(void)                      { return(m_x);           }
   void              X(const int x)               { m_x=x;                 }
   int               Y(void)                      { return(m_y);           }
   void              Y(const int y)               { m_y=y;                 }
   int               X2(void)                     { return(m_x+m_x_size);  }
   int               Y2(void)                     { return(m_y+m_y_size);  }
   //--- Indents from the edge point (xy)
   int               XGap(void)                   { return(m_x_gap);       }
   void              XGap(const int x_gap)        { m_x_gap=x_gap;         }
   int               YGap(void)                   { return(m_y_gap);       }
   void              YGap(const int y_gap)        { m_y_gap=y_gap;         }
   //--- Sizes
   int               XSize(void)                  { return(m_x_size);      }
   void              XSize(const int x_size)      { m_x_size=x_size;       }
   int               YSize(void)                  { return(m_y_size);      }
   void              YSize(const int y_size)      { m_y_size=y_size;       }
   //--- Focus
   bool              MouseFocus(void)             { return(m_mouse_focus); }
   void              MouseFocus(const bool focus) { m_mouse_focus=focus;   }
  };
//+------------------------------------------------------------------+
//| Konstruktor                                                      |
//+------------------------------------------------------------------+
CRectLabel::CRectLabel(void) : m_x(0),
                               m_y(0),
                               m_x2(0),
                               m_y2(0),
                               m_x_gap(0),
                               m_y_gap(0),
                               m_x_size(0),
                               m_y_size(0),
                               m_mouse_focus(false)
  {
  }
//+----------------------------------------------------------------
//| Destruktor                                                       |
//+----------------------------------------------------------------
CRectLabel::~CRectLabel(void)
  {
  }

Eine Datei enthält mehrere Klassen. Für eine schnelle Navigation zwischen diesen, können Sie an den Anfang der Datei, nach dem Einbeziehen der Dateien für die Standardbibliothek, eine Datei-Übersicht schreiben, welche aus einer Liste der Klassen besteht:

//--- Die Liste der Klassen für eine schnelle Navigation (Alt+G)
class CRectLabel;
class CEdit;
class CLabel;
class CBmpLabel;
class CButton;

Nun können Sie, genau wie bei dem Wechsel zwischen Funktionen und Methoden, über das Platzieren des Cursors in den Namen der Klasse und durch Drücken der Tastenkombination Alt+G sofort zu der gewünschten Klasse navigieren.

In der jetzigen Phase kann das Schema wie folgt dargestellt werden: Hier ist das Rechteck mit dem dicken blauen Rahmen die Objects.mqh Datei mit den darin enthaltenen Klassen (Rechtecke mit einem dünnen blauen Rahmen), welche wir vorhin beschrieben haben. Die blauen Rahmen bedeuten, dass alle diese Klassen in dieser Datei von einer der Klassen, welche durch das Rechteck CChartObject… repräsentiert werden, abgeleitet worden sind, wobei der Pfeil auf den Ursprung hindeutet.

Abbildung 3. Der Ausbau der Struktur durch das Erzeugen von abgeleiteten Klassen eines einfachen Objektes.

Abbildung 3. Der Ausbau der Struktur durch das Erzeugen von abgeleiteten Klassen eines einfachen Objektes.

Die Frage nach den grafischen primitiven Objekten ist gelöst. Dann müssen wir entscheiden, wie wir diese Objekte verwalten, wenn sie zu einer Gruppe gehören, da fast jedes Control aus mehreren einfachen Objekten besteht. Jedes Control ist einzigartig, aber sie haben alle ein paar gemeinsame Eigenschaften. Lassen Sie uns eine Basisklasse mit dem Namen CElementerzeugen, welche die gemeinsamen Eigenschaften für jedes Control enthält.

 

Die Basisklasse für alle Controls

Die CElement Klasse befindet sich in der Element.mqh Datei, und die Datei Objects.mqh wird über den Befehl #include miteinbezogen:

//+------------------------------------------------------------------+
//|                                                      Element.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Objects.mqh"
//+------------------------------------------------------------------+
//| Base control class                                               |
//+------------------------------------------------------------------+
class CElement#
  {
public:
                     CElement(void);
                    ~CElement(void);
  };

Um Platz zu sparen, werde ich die Methoden dieser Klasse in separaten Gruppen innerhalb des Klassen-Körpers darstellen. Am Ende dieses Artikels, können Sie die vollständige Version dieser Klasse mit allen ihren Methoden, die wir noch beschreiben werden, herunterladen. Dieses trifft auch auf alle weiteren Klassen zu.

Wir weisen darauf hin, dass keine der Klassen in der Objects.mqh Datei Basisklassen für die CElement Klasse sind und keine von denen unter den mit einbezogenen Objekten dieser Klasse sein werden. Später werden sie als Objekte in den abgeleiteten Klassen von derCElement Klasse sein und in der Basisklasse nur als ein Array von Objekt-Pointern abgespeichert werden. Wenn die Objects.mqh Datei mit in der Element.mqh Datei miteinbezogen wird, dann brauchen Sie sie in Zukunft in anderen Dateien nie wieder miteinzubeziehen. Daraus ergibt sich, dass wir anstelle der Einbeziehung von zwei Dateien (Objects.mqh und Element.mqh), wir nur noch eine mit einbeziehen müssen, und dieses ist die Element.mqh Datei.

In dem Controls Verzeichnis erzeugen wir eine weitere Datei, in welcher wir ein paar gemeinsame Eigenschaften des gesamten Programms in den #define Directiven abspeichern wollen:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Class name
#define CLASS_NAME ::StringSubstr(__FUNCTION__,0,::StringFind(__FUNCTION__,"::"))
//--- Program name
#define PROGRAM_NAME ::MQLInfoString(MQL_PROGRAM_NAME)
//--- Program type
#define PROGRAM_TYPE (ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)
//--- Prevention of exceeding the array size
#define PREVENTING_OUT_OF_RANGE __FUNCTION__," > Prevention of exceeding the array size."

//--- Font
#define FONT      ("Calibri")
#define FONT_SIZE (8)

Bitte beachten Sie, dass den Funktionen in dem oben dargestellten Code immer ein doppelter Doppelpunkt vorausgeht. Diese sind nicht zwingend vorgeschrieben und alles würde auch ohne sie funktionieren. Bei der Programmierung gilt es jedoch als gute Manier, wenn man einen doppelten Doppelpunkt vor eine Systemfunktion der Programmiersprache setzt. Dadurch kann man eindeutig eine Systemfunktion erkennen.

Binden Sie die Datei Defines.mqh mit in der Objects.mqh Datei mit ein. Somit ist sie in der gesamten Kette der Dateien verfügbar (Defines.mqh -> Objects.mqh -> Element.mqh):

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Defines.mqh"
#include <ChartObjects\ChartObjectsBmpControls.mqh>
#include <ChartObjects\ChartObjectsTxtControls.mqh>

Nun müssen wir definieren, welche gemeinsamen Eigenschaften die Controls haben sollen. Jedes Control ist ein eigenständiges Programm-Modul, welches vollkommen unabhängig von anderen ähnlichen Modulen arbeitet. Da aber einige dieser Controls in Gruppen zusammengefasst werden, wie es zum Beispiel bei komplexeren Controls der Fall ist, wird es die Situationen geben, wo Controls Nachrichten an das Hauptprogramm und an andere Controls senden. Daher müssen wir bestimmen können, ob die erhaltene Nachricht von einem Control unseres Programms stammt, oder ob sie von einem anderen Programm stammt, welches auf dem gleichen Chart läuft.

Zudem müssen wir noch folgendes definieren:

class CElement#
  {
protected:
   //--- (1) Name of class and (2) program, (3) program type
   string            m_class_name;
   string            m_program_name;
   ENUM_PROGRAM_TYPE m_program_type;
   //--- Control state
   bool              m_is_visible;
   bool              m_is_dropdown;
   int               m_is_object_tabs;
   //--- Focus
   bool              m_mouse_focus;
   //---
public:
                     CElement(void);
                    ~CElement(void);
   //--- (1) Erhalten und Festlegung des Klassennamens, (2) Erhalten das Programmnamens, 
   //    (3) Erhalten des Programmtyps
   string            ClassName(void)                    const { return(m_class_name);           }
   void              ClassName(const string class_name)       { m_class_name=class_name;        }
   string            ProgramName(void)                  const { return(m_program_name);         }
   ENUM_PROGRAM_TYPE ProgramType(void)                  const { return(m_program_type);         }
   //--- Control state
   void              IsVisible(const bool flag)               { m_is_visible=flag;              }
   bool              IsVisible(void)                    const { return(m_is_visible);           }
   void              IsDropdown(const bool flag)              { m_is_dropdown=flag;             }
   bool              IsDropdown(void)                   const { return(m_is_dropdown);          }
   void              IsObjectTabs(const int index)            { m_is_object_tabs=index;         }
   int               IsObjectTabs(void)                 const { return(m_is_object_tabs);       }
   //--- Focus
   bool              MouseFocus(void)                   const { return(m_mouse_focus);          }
   void              MouseFocus(const bool focus)             { m_mouse_focus=focus;            }
  };

Da es auf einer Programmoberfläche mehrere Controls des selben Typs geben kann (z.B. mehrere Buttons), sollte jeder von ihnen seine eigene eindeutige Nummer oder seinen Identifizierer (id) haben. Zur gleichen Zeit kann ein Bedienelement auch aus einer ganzen Reihe von anderen Steuerelementen (Controls) bestehen, wobei jedes seine eigene Indexnummer benötigt.

class CElement#
  {
protected:
   //--- Identifier und Index von Control
   int               m_id;
   int               m_index;
   //---
public:
   //--- Festlegen und erhalten des Identifiers
   void              Id(const int id)                         { m_id=id;                      }
   int               Id(void)                           const { return(m_id);                 }
   //--- Festlegen und erhalten des Control Index
   void              Index(const int index)                   { m_index=index;                }
   int               Index(void)                        const { return(m_index);              }
  };

Wie zuvor schon erwähnt, werden alle Objekte von grafischen Controls in einem Array des Typs CChartObject als Pointer zu diesen Objekten gespeichert. Hierfür benötigen wir eine Methode, welche das Einfügen von Objekt-Pointern in ein Array, nachdem sie erfolgreich erzeugt wurden, erlaubt. Zudem müssen wir noch einen Pointer von einem Array erhalten (1), welcher über den Index (2) spezifiziert wird, um die Größe Arrays mit den Objekten erhalten zu können und den Speicher des Arrays (3) leeren zu können.

class CElement#
  {
protected:
   //--- Gemeinsames Array von Pointern zu allen Objekten in diesem Control
   CChartObject     *m_objects[];
   //---
public:
   //--- Erhalt des the Object-Pointers über einen angegebenen Index
   CChartObject     *Object(const int index);
   //--- (1) Erhalten der Anzahl der Control-Objekte, (2) Leeren des Objekt-Arrays
   int               ObjectsElementTotal(void)          const { return(::ArraySize(m_objects)); }
   void              FreeObjectsArray(void)                   { ::ArrayFree(m_objects);         }
   //---
protected:
   //--- Methode um Pointers zu einfachen Objekten dem Array hinzuzufügen.
   void              AddToArray(CChartObject &object);
  };
//+-----------------------------------------------------------------------+
//| Gibt den Pointer des Control-Objektes über den Index zurück           |
//+-----------------------------------------------------------------------+
CChartObject *CElement::Object(const int index)
  {
   int array_size=::ArraySize(m_objects);
//--- Überprüfung der Größe des Objekt-Arrays
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > No ("+m_class_name+") objects in this element!");
      return(NULL);
     }
//--- Korrektur falls die Größe überschritten wird
   int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index;
//--- Rückgabe des Objekt-Pointers
   return(m_objects[i]);
  }
//+----------------------------------------------------------------------+
//| Fügt ein Objekt-Pointer dem Array hinzu                              |
//+----------------------------------------------------------------------+
void CElement::AddToArray(CChartObject &object)
  {
   int size=ObjectsElementTotal();
   ::ArrayResize(m_objects,size+1);
   m_objects[size]=::GetPointer(object);
  }

Jedes Control hat seinen eigenen Eventhandler für Chart-Events und seinen eigenen Timer In der CElement Klasse sind diese Methoden virtuell, da sie nicht universell gemacht werden können, da jedes Control einzigartig ist. Wir werden dieses später noch ausführlicher besprechen, wenn wir eine Klasse entwickeln, welche als Container für alle Objekte fungiert (Controls). Die folgenden Methoden werden auch virtuell sein:

class CElement#
  {
public:
   //--- Chart Eventhandler
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {}
   //--- Timer
   virtual void      OnEventTimer(void) {}
   //--- Verschieben eines Controls
   virtual void      Moving(const int x,const int y) {}
   //--- (1) Anzeigen, (2) verstecken, (3) zurücksetzen, (4) entfernen
   virtual void      Show(void) {}
   virtual void      Hide(void) {}
   virtual void      Reset(void) {}
   virtual void      Delete(void) {}
   //--- (1) Setzen, (2) Zurücksetzen Der prioritäten für die linke Maustaste
   virtual void      SetZorders(void) {}
   virtual void      ResetZorders(void) {}
  };

Sie haben mittlerweile schon gesehen, dass sich in beiden Klassen der grafischen einfachen Objekte, in der Datei Objects.mqh und in der Datei CElement, Eigenschaften und Methoden befinden, die es uns erlauben die Grenzen des Objektes zu erhalten. Folglich gibt uns dieses die Möglichkeit herauszufinden, ob sich der Cursor der Maus über der Fläche eines Controls befindet und ob er sich über einem anderen zugehörigen einfachen grafischen Objekt befindet. Wofür brauchen wir das? Dieses erlaubt es uns, das graphische Interface für den Anwender sehr intuitiv gestalten zu können.

Zum Beispiel kann sich die Farbe des Hintergrundes oder des Rahmens verändern, sobald der Maus-Cursor sich darüber befindet, was darauf hindeutet dass dieses Element von dem Anwender angeklickt werden kann. Um diese Funktion in der CElement Klasse implementieren zu können, benötigen wir Methoden, die mit der Farbe arbeiten. In einer wird die Farbpalette definiert und dafür werden der Methode zwei Farben übergeben. Über diese wird ein Gradient berechnet. Diese Berechnung findet nur einmal statt, wenn das Objekt dem Chart hinzugefügt wird. In der zweiten Methode wird die Berechnung mit einem fertigen Farb-Array durchgeführt, was erheblich Ressourcen einspart.

Wir könnten jetzt eine Methode für die Gradientenberechnung entwickeln, aber wir verwenden eine schon fertige Klasse, welche man in der Codebase herunterladen kann. Wir werden in diesem Projekt einige Methoden aus dem Codebase verwenden. Dmitry Fedoseev hat seine Klasse für das Arbeiten mit Farbe (IncColors) der Code Base hinzugefügt, aber ich würde an dieser Stelle vorschlagen eine Version zu verwenden, die ich leicht korrigiert habe. Sie kann am Ende des Artikels heruntergeladen werden (Colors.mqh).

Die Klasse (CColors) besitzt viele Methoden für alle Gelegenheiten. Die einzige Änderung, die ich durchgeführt habe, ist die Möglichkeit einer schnellen Navigation, wenn die Namen der Methoden sich in den Klassenkörpern und die Methoden selbst ausserhalb der Klassenkörper befinden. Dieses macht es möglich, dass man wesentlich schneller und effizienter zwischen der Methode und dem Inhalt und wieder zurück mit Hilfe der Alt+G Tasten wechseln kann. Diese Datei sollte sich in dem EasyAndFastGUI Verzeichnis befinden. Wir binden diese Datei in unserer Interface Bibliothek über die Element.mqh Datei in dem Verzeichnis ..\EasyAndFastGUI\Controls ein. Da sich diese Datei eine Ebene oberhalb unseres Verzeichnis befindet, muss sie wie folgt eingebunden werden:

//+------------------------------------------------------------------+
//|                                                      Element.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Objects.mqh"
#include "..\Colors.mqh"

Beziehen Sie das Objekt der CColors Klasse mit in der CElement Klasse mit ein, und dann (1) fügen Sie eine Variable und eine Methode für die Angabe der Anzahl der Farben in den Gradienten hinzu und (2) Methoden für die Initialisierung des Gradienten-Arrays und wechseln Sie die Farbe des angegebenen Objektes:

class CElement#
  {
protected:
   //--- Eine Klasseninstanz für das Arbeiten mit Farbe
   CColors           m_clr;
   //--- Die Anzahl der Farben in den Gradienten
   int               m_gradient_colors_total;
   //---
public:
   //--- Bestimmung der Größe des Gradienten
   void              GradientColorsTotal(const int total)     { m_gradient_colors_total=total;  }
   //---
protected:
   //--- Initialisierung des Arrays des Gradienten
   void              InitColorArray(const color outer_color,const color hover_color,color &color_array[]);
   //--- Das Wechseln der Farbe des Objektes
   void              ChangeObjectColor(const string name,const bool mouse_focus,const ENUM_OBJECT_PROPERTY_INTEGER property,
                                       const color outer_color,const color hover_color,const color &color_array[]);
  };

Um das Gradientenarray zu initialisieren, verwenden Sie die Gradient() Methode der CColors Klasse. Die folgenden Dinge müssen der Methode als Parameter übergeben werden: (1) Array der Farben, welches für die Berechnung des Gradienten verwendet wird, (2) das Array, welches die Farbsequenzen der Gradienten-Farben enthält und (3) die angeforderte Größe des Arrays, die der angenommene Anzahl von Schritten in dem Gradienten entspricht.

//+--------------------------------------------------------------------+
//| Initialisierung des Gradient-Array                                 |
//+--------------------------------------------------------------------+
void CElement::InitColorArray(const color outer_color,const color hover_color,color &color_array[])
  {
//--- Array mit den Gradient-Farben
   color colors[2];
   colors[0]=outer_color;
   colors[1]=hover_color;
// --- Die Bildung des Farb-Arrays
   m_clr.Gradient(colors,color_array,m_gradient_colors_total);
  }

In der Methode für das Wechseln der Farbe des Objektes, gibt es folgende Parameter:

Bitte sehen Sie sich auch die Kommentare in den Code von der ChangeObjectColor() Methode an:

//+-------------------------------------------------------------------------------------+
//| Wechseln der Farbe eines Objektes, sobald sich der Maus-Cursor darüber befindet     |
//+-------------------------------------------------------------------------------------+
void CElement::ChangeObjectColor(const string name,const bool mouse_focus,const ENUM_OBJECT_PROPERTY_INTEGER property,
                                 const color outer_color,const color hover_color,const color &color_array[])
  {
   if(::ArraySize(color_array)<1)
      return;
//--- Erhalten der aktuellen Objektfarbe
   color current_color=(color)::ObjectGetInteger(m_chart_id,name,property);
//--- Falls sich der Maus-Cursor über dem Objekt befindet
   if(mouse_focus)
     {
      //--- Verlassen, falls die angegebene Farbe erreicht wurde
      if(current_color==hover_color)
         return;
      //--- Das Farb-Array durchgehen
      for(int i=0; i<m_gradient_colors_total; i++)
        {
         //--- Falls die Farbe nicht zutrifft, dann wechsle zur nächsten
         if(color_array[i]!=current_color)
            continue;
         //---
         color new_color=(i+1==m_gradient_colors_total)? color_array[i] : color_array[i+1];
         //--- Ändern der Farbe
         ::ObjectSetInteger(m_chart_id,name,property,new_color);
         break;
        }
     }
//--- Falls sich der Maus-Cursor nicht über dem Objekt befindet
   else
     {
      //--- Verlassen, falls die angegebene Farbe erreicht wurde
      if(current_color==outer_color)
         return;
      //--- Das Farb-Array durchgehen
      for(int i=m_gradient_colors_total-1; i>=0; i--)
        {
         //--- Falls die Farbe nicht zutrifft, dann wechsle zur nächsten
         if(color_array[i]!=current_color)
            continue;
         //---
         color new_color=(i-1<0)? color_array[i] : color_array[i-1];
         //--- Ändern der Farbe
         ::ObjectSetInteger(m_chart_id,name,property,new_color);
         break;
        }
     }
  }

Andere gemeinsame Eigenschaften aller Controls, sind der Objektanker und die Ecke des Diagramms.

class CElement#
  {
protected:
   //--- Die Ecke des Charts und der Ankerpunkt der Objekte
   ENUM_BASE_CORNER  m_corner;
   ENUM_ANCHOR_POINT m_anchor;
  }

Die Erzeugung der CElement Klasse ist nun vollständig. Am Ende dieses Artikels können Sie die komplette Version dieser Klasse herunterladen. Zurzeit sieht die Struktur der Bibliothek wie folgt aus: Die gelben Pfeile zeigen an, dass die Datei mit einbezogen worden ist. Wenn Sie eine Klasse enthält, ist dieses keine Basisklasse für die Klassen, die in der einbezogenen Datei enthalten sind. Sie wird wie ein einbezogenes Objekt in einer Klasse verwendet, genauso wie es oberhalb zwischen den Klassen CElement und CColors gezeigt wurde.

Abbildung 4. Einbeziehen der CColors Klasse um mit Farben zu arbeiten.

Abbildung 4. Basisklasse für die CElement Controls

 

Die Basisklasse für eine Anwendung mit grafischem Interface

Bevor wir damit beginnen, Interface-Elemente zu erzeugen, müssen wir festlegen, wie die Zusammenhänge zwischen den Objekten umgesetzt werden. Die Gestaltung sollte auf die Art und Weise erfolgen, dass auf jedes Objekt von nur einer Klasse aus zugegriffen werden kann, in welcher die Objekte nicht nur gespeichert, sondern auch kategorisiert werden. Dieses gibt uns die Gelegenheit, die Anzahl der Objekte in diesem Container herauszufinden und diese auch sinnvoll verwalten zu können.

Wenn man nun alle notwendigen Funktionen in einer Klasse einbaut, ist dieses nicht sehr praktisch, da die Klasse dadurch überfüllt wirken könnte. Solche Klassen (Objekte) werden auch God-Objekte genannt, welche dem Sinn der objektorientierten Programmierung entgegenstehen, da sie zu viele Aufgaben besitzen. Wenn die Struktur des Projektes wächst, wird es immer schwieriger werden Änderungen oder Ergänzungen vorzunehmen. Daher werden die Objekte getrennt von der Klasse gespeichert, in welcher Ereignisse behandelt werden. Objekte befinden sich in der Basisklasse CWndContainer und die Events werden in der daraus abgeleiteten CWndEvents Klasse behandelt.

Lassen Sie uns nun die Klassen CWndContainer und CWndEvents erstellen. Da wir nun alle Controls, die wir am Anfang des Artikels aufgelistet haben, erzeugen, ergänzen wir diese Klassen noch mit der dazu notwendigen Funktionalität. Jetzt werden wir die generelle Struktur unseres Projektes bestimmen.

Erzeugen Sie in dem Controls Verzeichnis die Dateien WndContainer.mqh und WndEvents.mqh. Die CWndContainer Klasse ist noch leer, da wir noch keine Controls erzeugt haben.

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Klasse für das Abspeichern aller Objekte des Interfaces          |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
                     CWndContainer(void);
                    ~CWndContainer(void);
  };
//+------------------------------------------------------------------+
//| Konstruktor                                                      |
//+------------------------------------------------------------------+
CWndContainer::CWndContainer(void)
  {
  }
//+------------------------------------------------------------------+
//| Destruktor                                                       |
//+------------------------------------------------------------------+
CWndContainer::~CWndContainer(void)
  {
  }
//+------------------------------------------------------------------+

Die WndContainer.mqh Datei muss in der WndEvents.mqh Datei mit einbezogen werden, da die CWndEvents Klasse eine Ableitung der CWndContainer Klasse ist:

//+------------------------------------------------------------------+
//|                                                    WndEvents.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "WndContainer.mqh"
//+------------------------------------------------------------------+
//| Klasse für das Behandeln von Events                              |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
protected:
                     CWndEvents(void);
                    ~CWndEvents(void);
  };
//+------------------------------------------------------------------+
//| Konstruktor                                                      |
//+------------------------------------------------------------------+
CWndEvents::CWndEvents(void)
  {
  }
//+------------------------------------------------------------------+
//| Destruktor                                                       |
//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(void)
  {
  }
//+------------------------------------------------------------------+

Die Klassen CWndContainer und CWndEvents stellen nun die Basisklassen für jede MQL Anwendung dar, welche grafische Interfaces benötigen.

Für alle zukünftigen Test dieser Bibliotheken, entwickeln wir einen EA. Dieser muss in einem getrennten Verzeichnis erzeugt werden, da es neben der Hauptprogramm-Datei, noch eine einbezogene Datei Program.mqh mit der Klasse unseres Programms (CProgram) gibt. Diese Klasse wird von derCWndEvents Klasse abgeleitet.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <EasyAndFastGUI\Controls\WndEvents.mqh>
//+------------------------------------------------------------------+
//| Klasse für das Erzeugen eines Trading-Panels                     |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
public:
                     CProgram(void);
                    ~CProgram(void);
  };
//+------------------------------------------------------------------+
//| Konstruktor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
  }
//+------------------------------------------------------------------+
//| Destruktor                                                       |
//+------------------------------------------------------------------+
CProgram::~CProgram(void)
  {
  }
//+------------------------------------------------------------------+

Wir benötigen nun Methoden für das Behandeln von Events, welche später über die Eventhandler der Haupt-Programm-Datei aufgerufen werden.

class CProgram : public CWndEvents
  {
public:
   //--- Initialization/uninitialization
   void              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   //--- Timer
   void              OnTimerEvent(void);
   //---
protected:
   //--- Virtueller Eventhandler des Charts
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
  };

Die Timer- und Eventhandler des Charts müssen eine Ebene höher in der Basisklasse CWndEvents erzeugt werden:

class CWndEvents : public CWndContainer
  {
protected:
   //--- Timer
   void              OnTimerEvent(void);
   //--- Virtueller Eventhandler des Charts
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {}
  };

Bitte beachten Sie, dass in dem oben dargestellten Programmcode, in den beiden Klassen Basis-Klasse CWndEvents und in der abgeleiteten Klasse CProgram die OnEvent Eventhandler des Charts als virtual dekalriert werden. Das heißt, dass diese Methode in der CWndEvents Klasse, ein Dummy “{}” ist. Dieses erlaubt es uns, den Fluss der Ereignisse von der Basisklasse in die daraus abgeleitete Klasse weiterzuleiten, sobald dieses notwendig ist. Die in diesen Klassen enthaltenen virtuellen Methoden OnEvent() sind für interne Zwecke gedacht. Für den Aufruf aus der Haupt-Programmdatei heraus, wird eine andere Methode der CWndEvents Klasse verwendet. Lassen Sie sie uns ChartEvent() nennen. Wir werden auch Hilfsmethoden für jede Art von Hauptereignissen erstellen, die den Code klar und lesbar machen.

Zu diesen Hilfsmethoden, welche auch das Überprüfen von benutzerdefinierten Events vornehmen, wird auch eine Methode für die Überprüfung von Events in den Controls benötigt. Lassen Sie sie uns CheckElementsEvents() nennen. Sie wird hier in Grün hervorgehoben:

class CWndEvents : public CWndContainer
  {
public:
   //--- Eventhandler des Charts
   void              ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //---
private:
   void              ChartEventCustom(void);
   void              ChartEventClick(void);
   void              ChartEventMouseMove(void);
   void              ChartEventObjectClick(void);
   void              ChartEventEndEdit(void);
   void              ChartEventChartChange(void);
   //--- Überprüfen von Events in den Controls
   void              CheckElementsEvents(void);
  };

Die Hilfsmethoden werden innerhalb der ChartEvent() Methode und nur in der CWndEvents Klasse verwendet. Um zu vermeiden, dass wir ihr die gleichen Parameter übergeben, lassen Sie uns ähnliche Variablen in der Form von Klassen-Members erzeugen und zudem eine Methode für deren Initialisierung, welche ganz am Anfang der ChartEvent() Methode verwendet wird. Die Variablen sollten in dem privaten Bereich (private) stehen, da sie nur in dieser Klasse verwendet werden.

class CWndEvents : public CWndContainer
  {
private:
   //--- Event Parameter
   int               m_id;
   long              m_lparam;
   double            m_dparam;
   string            m_sparam;
   //--- Initialisierung der Eventparameter
   void              InitChartEventsParams(const int id,const long lparam,const double dparam,const string sparam);
  };
//+-------------------------------------------------------------------+
//| Initialisierung der Event-Variablen                               |
//+-------------------------------------------------------------------+
void CWndEvents::InitChartEventsParams(const int id,const long lparam,const double dparam,const string sparam)
  {
   m_id     =id;
   m_lparam =lparam;
   m_dparam =dparam;
   m_sparam =sparam;
  }

Jetzt können wir in der Haupt-Programmdatei, (1) die Datei, welche die CProgram Klasse enthält mit einbeziehen, (2) eine Intanz erzeugen und (3) sie mit den Hauptfunktionen verbinden:

//+------------------------------------------------------------------+
//|                                                  TestLibrary.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2015, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
//--- Einbeziehen der Trading Panel Klasse
#include "Program.mqh"
CProgram program;
//+------------------------------------------------------------------+
//| Expert Initialisierungs-Function                                 |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   program.OnInitEvent();
//--- Initialisierung erfolgreich
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization Funktion                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   program.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//| Expert tick Funktion                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
  }
//+------------------------------------------------------------------+
//| Timer Funktion                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   program.OnTimerEvent();
  }
//+------------------------------------------------------------------+
//| Trade Funktion                                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
  }
//+------------------------------------------------------------------+
//| ChartEvent Funktion                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   program.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

Falls benötigt, können auch noch andere Methoden für das Eventhandling, wie z.B. OnTick(), OnTrade(), etc. innerhalb der Klasse CProgram erzeugt werden.

 

Test der Eventhandler der Bibliothek und Programm-Klassen

Es wurde bereits erwähnt, dass die virtuelle Methode OnEvent() der CProgram Klasse von der Basisklasse CWndEvents in der Methode ChartEvent() aufgerufen werden kann. Wir wollen nun sicherstellen dass dieses funktioniert und testen daher diesen Mechanismus. Dafür rufen wir in der CWndEvents::ChartEvent() Methode die CProgram::OnEvent() Methode auf:

//+------------------------------------------------------------------+
//| Behandeln von Programm-Events                                    |
//+------------------------------------------------------------------+
void CWndEvents::ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   OnEvent(id,lparam,dparam,sparam);
  }

Anschließen schreiben wir in der CProgram::OnEvent() Methode den folgenden Programmcode:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CLICK)
     {
      ::Comment("x: ",lparam,"; y: ",(int)dparam);
     }
  }

Kompilieren Sie die Dateien und laden Sie diesen EA in einen Chart. Wenn Sie mit der linken Maustaste in den Chart klicken, dann werden die Koordinaten des Mauszeigers in dem Moment, wo Sie die Maustaste loslassen, in der oberen linken Ecke dargestellt. Nachdem wir diesen Test erfolgreich abgeschlossen haben, kann der hervorgehobene Programmcode aus den nächsten zwei Listings entfernt werden CWndEvents::ChartEvent() und CProgram::OnEvent() Methoden.

 

Schlussfolgerung

Um zusammenzufassen, wo wir uns nun befinden, lassen Sie uns alles, worüber wir gesprochen haben, in einem Diagramm darstellen:

Abbildung 5. Wir haben Klassen für das Speichern von Pointern und das Eventhandling in unser Projekt mit aufgenommen.

Abbildung 5. Wir haben Klassen für das Speichern von Pointern und das Eventhandling in unser Projekt mit aufgenommen.

Derzeit besteht das System aus zwei nicht miteinander verbundenen Teilen. Um diese miteinander verbinden zu können, ist es erforderlich das Hauptelement der Benutzeroberfläche zu erstellen. Das Hauptelement ist eine Form oder ein Fenster zu welchem alle weiteren Controls hinzugefügt werden. Daher werden wir eine solche Klasse schreiben und sie CWindow nennen. Wir verbinden die Datei mit der Element.mqh Klasse mit der Datei Window.mqh wobei die CElement Klasse die Basisklasse für die CWindow Klasse darstellt.

Sie finden hier das gesamte Material des ersten Kapitels und können dieses herunterladen und ausprobieren. Wenn Sie fragen zur Verwendung dieses Materials haben, dann können Sie zunächst auf die detaillierte Beschreibung in der Bibliothek zurückgreifen oder Sie stellen Ihre Frage(n) in den Kommentaren zu diesem Artikel.

Liste der Artikel (Kapitel) des ersten Teils: