Verwendung von Layouts und Containern für GUI Controls: Die CBox Klasse

Enrico Lambino | 18 Mai, 2016

Inhaltsverzeichniss


1. Einleitung

Die absolute Positionierung von Controls innerhalb eines Dialogfensters, ist der direkteste Weg für die Erzeugung von einem grafischen Userinterface einer Applikation. Aber in einigen Fällen kann diese Herangehensweise an die Erzeugung eines grafischen User-Interfaces (GUI) unvorteilhaft und auch unpraktisch sein. Dieser Artikel präsentiert eine alternative Methode für die Erzeugung von GUI-Controls, basierend auf Layouts und Containern und der Verwendung eines Layoutmanagers, der CBox Klasse.

Die hier verwendete Layout Manager Klasse ist sehr ähnlich denen, wie sie auch in anderen Programmiersprachen gefunden wird, wie z.B. BoxLayout (Java) und Pack geometry manager (Python/Tkinter).


2. Ziele

Wenn wir uns das SimplePanel und die Controls-Beispiele, die in MetaTrader 5 vorhanden sind ansehen, dann sehen wir, dass diese Controls innerhalb der Panels in Pixel by Pixel positioniert werden (absolute Positionierung). Jedem erzeugten Control wird in der Client-Area eine bestimmte Position zugewiesen und jede Position eines Controls hängt von der Position des vorherigen Controls ab, mit zusätzlichen Offset-Angaben. Auch wenn dieses in vielen Fällen die natürliche Herangehensweise ist, ist eine solche Präzision in den meisten Fällen nicht notwendig und die Verwendung dieser Methode kann in vielen Fällen von Nachteil sein.

Jeder Programmierer mit ein wenig Erfahrung ist in der Lage, unter der Verwendung von genauen Pixel Positionsangaben, ein grafisches User-Interface zu erstellen. Diese Vorgehensweise hat aber die folgenden Nachteile:

  • Es ist nahezu unmöglich, dass ein Control nicht durch die Größenänderung eines anderen Controls beeinflusst wird.
  • Zudem ist der erzeugte Programmcode nicht wiederverwendbar. Das bedeutet, dass kleinere Änderungen in dem Interface in einigen Fällen zu vielen Veränderungen in dem Programmcode führen.
  • Dieses kann sehr zeitraubend sein, insbesondere dann, wenn man ein sehr komplexes Interface zusammenstellt.

Dieses veranlasste uns ein Layout System mit den folgenden Zielen zu erstellen:

  • Der Code sollte wiederverwendbar sein.
  • Die Veränderung eines Teils des Interfaces, sollte nur minimale Änderungen am Programmcode zur Folge haben.
  • Die Positionierung der Komponenten innerhalb des Interfaces sollte automatisch berechnet werden.

Eine Implementierung eines solchen Systems wird in diesem Artikel mit dem Container - die XBox-Klasse realisiert.


3. Die CBox-Klasse

Eine Instanz der CBox Klasse agiert als Container oder Box — Controls können einer Box hinzugefügt werden und die CBox Klasse berechnet dann automatisch die Positionierung dieser Controls über den vorhandenen freien Platz. Ein typisches Beispiel einer Instanz der CBox Klasse könnte die folgende Layout Struktur haben:

CBox Layout

Abbildung 1. CBox Layout

Die äußeren Box repräsentiert die Größe des gesamten Containers, wobei die gepunktete Box die inneren Abstands-Grenzen darstellt. Die blaue Fläche repräsentiert den freien Abstand zu den Außengrenzen des gesamten Containers. Die verbleibende weiße Fläche stellt die gesamte Fläche für die Positionierung von weiteren Controls innerhalb des Containers dar.

Je nach Komplexität des Panels, kann die CBox Klasse auf verschiedene Weise eingesetzt werden: Zum Beispiel ist es auch möglich dass ein CBox-Container weitere Container mit einer Anzahl von Controls enthält. Es ist auch möglich, dass ein Container ein Control und noch einen Container enthält. Es wird jedoch empfohlen Container nur als Schwestern innerhalb eines übergeordneten Containers zu verwenden.

Wir konstruieren eine CBox durch die Erweiterung von CWndClient (ohne Scrollbalken), wie es im folgenden Ausschnitt gezeigt wird:

#include <Controls\WndClient.mqh>
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
class CBox : public CWndClient
  {
public:
                     CBox();
                    ~CBox();   
   virtual bool      Create(const long chart,const string name,const int subwin,
                           const int x1,const int y1,const int x2,const int y2);
  };
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CBox::CBox() 
  {
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CBox::~CBox()
  {
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CBox::Create(const long chart,const string name,const int subwin,
                  const int x1,const int y1,const int x2,const int y2)
  {
   if(!CWndContainer::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateBack())
      return(false);
   if(!ColorBackground(CONTROLS_DIALOG_COLOR_CLIENT_BG))
      return(false);
   if(!ColorBorder(clrNONE))
      return(false);
   return(true);
  }
//+----------------------------------------------------------------

Es ist auch möglich, dass die CBox Klasse direkt von der CWndContainer erbt. Auf diese Weise würde jedoch die Klasse einige hilfreiche Eigenschaften, wie zum Beispiel Hintergrundfarbe und Ränder(Borders), verlieren. Alternativ kann man eine wesentlich einfachere Version durch das Erweitern des Objektes CWndObj erhalten, aber Sie müssen dazu noch eine Instanz des CArrayObj Objektes als einer von seinen privaten geschützten Elementen hinzufügen und alle Klassen-Methoden der Objekte, die in dieser Instanz gespeichert sind, neu erstellen.


3.1. Layout Styles

CBox hat zwei Layout-Styles: Vertikal Style und Horizontal Style.

Der horizontale Layout-Style hat das folgende Basislayout:

Horizontal Style für CBox

Abbildung 2. Horizontal Style (Centered)

Ein vertikaler Style hat das folgende Basislayout:

Vertical Style für CBox

Abbildung 3. Vertical Style (Zentriert)

Die Cbox Klasse verwendet standardmäßig in den Horizontal Style.

Mit einer Kombination aus diesen beiden Layout (unter Verwendung mehrerer Container) ist es möglich, praktisch jede Art von einem GUI-Panel-Design zu erstellen. Zudem erlaubt das Platzieren von Controls in einem Container ein segmentiertes Programm-Design. Das bedeutet, dass zum Beispiel eine Größenänderung eines Controls in einem Container die Controls eines anderen Containers nicht beeinflusst.

Um den horizontalen oder vertikalen Stil in CBox zu implementieren, benötigen wir die Deklaration einer Enumeration, welche dann wiederum als ein Element der Klasse gespeichert wird.

enum LAYOUT_STYLE
  {
   LAYOUT_STYLE_VERTICAL,
   LAYOUT_STYLE_HORIZONTAL
  };

3.2. Berechnung des Abstandes zwischen den Controls

Die Cbox Klasse maximiert den zugewiesenen verfügbaren Platz und verwendet diesen um darin die Controls gleichmäßig zu verteilen, so wie es in den vorherigen Abbildung gezeigt ist.

Mit Blick auf die oben genannten Abbildungen, können wir dann die Formel zur Berechnung des Raumes zwischen den Controls in einem bestimmten CBox-Container ableiten. Mit dem nachfolgenden Pseudo-Code:

Für horizontale Layouts:
x space = ((available space x)-(total x size of all controls))/(total number of controls + 1)
y space = ((available space y)-(y size of control))/2

Für vertikale Layouts:
x space = ((available space x)-(x size of control))/2
y space = ((available space y)-(total y size of all controls))/(total number of controls + 1)

3.3. Ausrichtung

Die Berechnung des Raumes zwischen den Controls, wie es in dem vorherigen Abschnitt erwähnt wurde, gilt nur für eine zentrierte Ausrichtung. Wir würden aber gerne noch weitere Möglichkeiten der Ausrichtungen haben, und daher müssen wir noch ein paar kleine Veränderungen an der Berechnung vornehmen.

Für die horizontale Ausrichtung, sind die zur Verfügung stehenden Optionen, abgesehen von einem zentrierten Container, links, rechts und in der Mitte (keine Seiten), wie in den folgenden Abbildungen dargestellt:

Horizontal box - Ausrichtung links (align left)

Figure 4. Horizontal Style Ausrichtung links (Align Left)

Horizontal box - Ausrichtung rechts (align right)

Abbildung 5. Horizontal Style - Ausrichtung Rechts (Align Right)

Horizontal box - Zentriert (align center, no sides)

Abbildung 6. Horizontal Style (Zentriert, No Sides)


Für die vertikale Ausrichtung, sind die zur Verfügung stehenden Optionen, abgesehen von der zentrierten Ausrichtung, oben, unten, in der Mitte und in der Mitte (keine Seiten), wie unten dargestellt:

Vertical box - Ausrichtung oben (align top) Vertical box - Ausrichtung Zentriert (Align center no sides) Vertical box - Ausrichtung unten (align bottom)

Abbildung 7. Vertikale Ausrichtungs-Stile: (Links) Ausrichtung oben, (Mitte) Zentriert - keine Ränder, (Rechts) Ausrichtung unten

Beachten Sie, dass die CBox Klasse die x- und y-Abstände zwischen den Controls auf der Grundlage dieser Ausrichtungen automatisch berechnen soll. Somit muss man anstelle eines Teilers, wie

(Gesamte Anzahl an Controls + 1)

um den Zwischenraum zwischen den Controls zu erhalten, die gesamte Anzahl der Controls für die asymmetrische Ausrichtung (recht, linke , odere und unteren Ausrichtung) als Teiler und (die gesamte Anzahl an Controls - 1) für einen zentrierten Container ohne Ränder an den Seiten verwendet werden.

Ähnlich wie die beiden Layout Styles, benötigt die Implementierung der Ausrichtung in der Cbox Klasse Enumerationen. Wir deklarieren eine Enumeration für jeden Ausrichtung-Stil wie folgt:

enum VERTICAL_ALIGN
  {
   VERTICAL_ALIGN_CENTER,
   VERTICAL_ALIGN_CENTER_NOSIDES,
   VERTICAL_ALIGN_TOP,
   VERTICAL_ALIGN_BOTTOM
  };
enum HORIZONTAL_ALIGN
  {
   HORIZONTAL_ALIGN_CENTER,
   HORIZONTAL_ALIGN_CENTER_NOSIDES,
   HORIZONTAL_ALIGN_LEFT,
   HORIZONTAL_ALIGN_RIGHT
  };

3.4. Rendern der Komponenten

Normalerweise erstellen wir Vontrols indem wir die Koordinaten X1, X1, X2 und Y2 angeben, so wie es in dem folgen Code für einen Button passiert:

CButton m_button;
int x1 = currentX;
int y1 = currentY;
int x2 = currentX+BUTTON_WIDTH; 
int y2 = currentY+BUTTON_HEIGHT
if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,x1,y1,x2,y2))
      return(false);

Wobei X2 - X1 und Y2 - Y1 der Breite und der Höhe des Controls entsprechen. Ähnlich wie in diesem Beispiel, können wir aber auch einen Button erzeugen indem wir die viel einfachere Methode der Cbox Klasse verwenden, so wie es in dem folgenden Ausschnitt dargestellt ist:

if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,0,0,BUTTON_WIDTH,BUTTON_HEIGHT))
      return(false);

Die Cbox Klasse wird dann diese Komponenten in dem Fenster automatisch neu positionieren. Für die neue Positionierung von allen Controls und Containern, muss die Methode Pack() aufgerufen werden, welche auch die Methode Render() aufruft.

bool CBox::Pack(void)
  {
   GetTotalControlsSize();
   return(Render());
  }

Die Pack() Methode erhält die kombinierte Größe der Container, und ruft anschließend die Render() Methode auf, in welcher der größte Teil der Berechnung stattfindet. Der nachfolgende Codeabschnitt zeigt das Rendering der Controls innerhalb des Containers mit Hilfe der Methode Render().

bool CBox::Render(void)
  {
   int x_space=0,y_space=0;
   if(!GetSpace(x_space,y_space))
      return(false);
   int x=Left()+m_padding_left+
      ((m_horizontal_align==HORIZONTAL_ALIGN_LEFT||m_horizontal_align==HORIZONTAL_ALIGN_CENTER_NOSIDES)?0:x_space);
   int y=Top()+m_padding_top+
      ((m_vertical_align==VERTICAL_ALIGN_TOP||m_vertical_align==VERTICAL_ALIGN_CENTER_NOSIDES)?0:y_space);
   for(int j=0;j<ControlsTotal();j++)
     {
      CWnd *control=Control(j);
      if(control==NULL) 
         continue;
      if(control==GetPointer(m_background)) 
         continue;
      control.Move(x,y);     
      if (j<ControlsTotal()-1)
         Shift(GetPointer(control),x,y,x_space,y_space);      
     }
   return(true);
  }

3.5. Größenanpassung der Komponenten

Wenn ein Control größer ist als der zur Verfügung stehende Platz, dann sollte dieses Control in seiner Größe verändert werden, damit es passend ist. Andernfalls würde natürlich das Control über die Ränder des Containers hinausragen, was zu Problemen mit dem gesamten Panel führt. Dieser Ansatz ist auch nützlich dafür, wenn man ein Control auf die maximale Größe der zur verfügung stehnden Fläche der Client-Area oder des Containers bringen will. Wenn die Breite und/oder Höhe eines gegebenen Controls die maximale Breite und/oder Höhe des Containers abzüglich des Mindestabstandes an den Seiten überschreitet, dann wird das Control in seiner Größe auf die maximalen Breite und/oder Höhe begrenzt.

Beachten Sie, dass die Cbox Klasse einen Container nicht in seiner Größe verändert, wenn die gesamte Größe aller enthaltenen Controls den maximalen Freiraum überschreitet. In so einem Fall muss entweder die Größe des Hauptfensters (CDialog oder CAppDialog), oder die des individuellen Controls manuell justiert werden.


3.6. Recursives Rendering

Für die einfache Verwendung der Cbox Klasse, sollte ein einziger Aufruf der Pack()-Methode ausreichend sein. In verschachtelten Containern muss natürlich die selbe Methode ebenfalls aufgerufen werden, damit dort die Positionen der individuellen Controls korrigiert werden können. Dieses kann durch Hinzufügen einer Funktion in dem Verfahren vermieden werden, wobei das gleiche Verfahren in den eigenen Controls durchgeführt wird, aber nur, wenn das Control eine Instanz einer von CBox oder einer anderen Layoutklasse ist. Um dieses zu tun, definieren wir zunächst einMakro und geben ihm einen eindeutigen Wert:

#define CLASS_LAYOUT 999

Dann überschreiben wir die Methode Type() der CObject Klasse, damit Sie den Wert des gerade vorbereiteten Makros zurückgibt:

virtual int       Type() const {return CLASS_LAYOUT;}

Zuletzt führen wir innerhalb der Pack() Methode der Cbox Klasse die Rendering Methoden der Child-Container, welche Instanzen der Layout Klasse sind, aus:

for(int j=0;j<ControlsTotal();j++)
     {
      CWnd *control=Control(j);
      if(control==NULL) 
         continue;
      if(control==GetPointer(m_background)) 
         continue;
      control.Move(x,y);

      //Aufruf der Pack() Methode, falls es eine Layout-Klasse ist
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }     
   
      if (j<ControlsTotal()-1)
         Shift(GetPointer(control),x,y,x_space,y_space);      
     }

Die Rendering Methode beginnt mit der Berechnung des zur Verfügung stehenden Platzes für die Controls innerhalb des Containers. Diese Werte werden in den Variablen m_total_x und m_total_y, gespeichert. Im nächsten Schritt berechnen wir den Abstand zwischen den Controls, basierend auf dem Layout-Style und der Ausrichtung (Alignment). Im letzten Schritt implementieren wir die Repositionierung der Controls innerhalb des Containers.

CBox zählt auch die Anzahl der Controls, welche neu positioniert werden, da es auch Objekte innerhalb des Containers gibt, welche keine neue Positionierung benötigen, wie z.B. CWndClient das native Hintergrund-Objekt oder vielleicht auch einige andere Controls um die die Cbox-Klasse erweitert wurde.

CBox beinhaltet auch die minimale control Größe innerhalb des Containers (Mit Ausnahme des Hintergrundes), definiert durch m_min_size (struct CSize). Sein Zweck ist es, die Controls einheitlich innerhalb des Containers gestapelt zu halten, egal ob horizontal oder vertikal. Seine Definition ist eher kontraintuitiv, da dies tatsächlich die Größe des größten Controls ist. Aber wir definieren ihn hier als das Minimum, da Cbox annehmen wird, dass dieses die minimale Größe ist und den zur Verfügung stehenden Raum basierend auf dieser Größenangabe berechnet.

Beachten Sie, dass die Shift()-Methode nach einem ähnlichen Verfahren für die Positionierung von Controls verfährt (Absolute Positionierung). Die Rendering-Methoden beinhalten Referenzen zu den X und Y-Koordinaten, welche nach jeder Neupositionierung eines Controls aktualisiert und gespeichert werden. Die Cbox-Klasse führt das automatisch aus, sodass der Entwickler des Panels nur die aktuelle Größe der einzelnen Controls angeben muss.


4. Die Implementation in einem Dialogfenster

Wenn wir die Klasse CBox benutzen, Dann ersetzen wir die Funktionalitäten der native client area von CDialog oder CAppDialog, m_client_area, Welche eine Instanz von CWndClient ist. Somit haben wir in diesem Fall drei Optionen:

  1. Erweitern/Überschreiben von CAppDialog oder CDialog damit CBox die 'Client Area' ersetzt.
  2. Die Verwendung von Containern und das Hinzufügen der Container zu der 'Client Area'
  3. Die Verwendung von einem übergeordneten CBox Container in welchen weitere kleinere Container verwaltet werden.

Die Verwendung der ersten Option wird eine Menge Arbeit bedeuten, da wir die Dialog Objekte neu schreiben müssen, damit sie die neue Client Area verwenden. Alternativ können die Dialog Objekte um die containerklasse erweitert werden, aber wir behalten dennoch eine unbenutzte Instanz von CWndClient (m_client_area), welche unnötig Speicher belegt.

Die zweite Option ist ebenso machbar. Wir können einfach Controls in einen Container platzieren und dann über die Pixel Positionierung der 'Client Area' hinzufügen. Aber diese Methode würde nicht das vollständige Potenzial der Cbox Klasse ausnutzen, welche dafür entwickelt worden ist, das einfache Erzeugen von Panels zu ermöglichen, ohne sich dabei großartige Gedanken über die Positionierung von individuellen Controls und Containern machen zu müssen.

Daher wird die dritte Option empfohlen. Wir erzeugen eine Haupt-CBox-Container, welcher alle anderen kleineren Container und Controls enthält. Dieser übergeordneten Container belegt die Größe der gesamten 'nativen Client Area' und wird als ihr einziger Nachkomme (Child) hinzugefügt. Dies macht den 'native Client-Bereich' ein wenig überflüssig, aber zumindest wird er immer noch verwendet. Darüber hinaus können wir mit dieser Option ein hohes Maß an Codierung / Decodierung vermeiden.


5. Beispiele


5.1. Beispiel #1: Ein einfacher PIP-Wert Rechner

Jetzt verwenden wir die c-box Klasse um ein einfaches Panel zu implementieren: einen Pip-Value Rechner. Der Pip-Value Rechner-Dialog beinhaltet drei Felder mit dem Typen CEdit, namentlich:

  • name des Symbols oder Finanzinstruments;
  • Größe von 1 pip für das angegebene Symbol;
  • Wert von 1 pip für das angegebene Symbol.

Somit haben wir sechs unterschiedliche Controls, inklusive der Labels (CLabel) für jedes Feld, und ein Button (CButton) für das Ausführen der Berechnung. Nachfolgend sehen Sie ein Screenshot des Rechners:

Pip value Rechner - screenshot

Abbildung 8. Pip Value Rechner

Wenn wir uns das Panel anschauen, können wir sehen, dass wir fünf unterschiedliche Cbox Container verwenden. Es gibt drei horizontale Container für jedes der Felder, und einen weiteren horizontalen Container, rechts ausgerichtet, für die Buttons. Alle diese Container sind in einem Haupt-Container mit einem vertikalen Stil zusammengefasst. Und schließlich wird dieser Haupt-Container der 'Client-Area' derCAppDialog Instanz hinzugefügt. Die folgende Abbildung zeigt das Layout der Container. Die violetten Boxen repräsentieren die horizontalen Reihen. Die weißen Boxen stellen die wesentlichen Controls dar, und die große graue Box das Hauptfenster.

Pip Value Rechner - Dialog layout

Abbildung 9. Pip Value Rechner Layout

Beachten Sie, dass wir mit der Verwendung der cBox-Container keine Makros für Abstände und Lücken in definieren. Vielmehr definieren wir Makros für die Größe der Controls, wir konfigurieren jede Instanz einer cBox, und lassen diese sich entsprechend anordnen.

Um dieses Panel zu konstruieren, beginnen wir zunächst mit der Herstellung eines Header-Files, 'PipValueCalculator.mqh', welches sich in dem gleichen Verzeichnis wie der Hauptquellcode befinden sollte, welchen wir später behandeln werden. (PipValueCalculator.mq5). In diesem File schließen wir das CBox Klassen Header-File ein, sowie auch andere Include-Files, welche wir für dieses Panel benötigen. Wir benötigen zudem die CSymbolInfo Klasse, welche wir für die Berechnung des Pip-Wertes benötigen:

#include <Trade\SymbolInfo.mqh>
#include <Layouts\Box.mqh>
#include <Controls\Dialog.mqh>
#include <Controls\Label.mqh>
#include <Controls\Button.mqh>

Im nächsten Schritt geben wir die Breite und Höhe der verwendetetn Controls an. Es ist möglich eine bestimmte Größe für jedes Control zu anzugeben, aber für dieses Panel werden wir eine allgemeine Größe verwenden. Das bedeutet, dass jetzt alle wesentlichen Controls die gleiche Breite und Höhe aufweisen

#define CONTROL_WIDTH   (100)
#define CONTROL_HEIGHT  (20)

Nun werden wir das aktuelle Panel-Klassen-Objekt erzeugen. Dieses wird standardmäßig durch die Ableitung von der Klasse CAppDialog durchgeführt:

class CPipValueCalculatorDialog : public CAppDialog

Die initiale Struktur dieser Klasse sieht ähnlich wie die Folgende aus:

class CPipValueCalculatorDialog : public CAppDialog
  {
protected:
//protected class members here
public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();

protected:
//protected class methods here
  };
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CPipValueCalculatorDialog::CPipValueCalculatorDialog(void)
  {
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CPipValueCalculatorDialog::~CPipValueCalculatorDialog(void)
  {
  }

Mit dem oben abgebildeten Code-Abschnitt haben wir nun die Vorlage für unseren Pip-Rechner-Panel-Klasse (Diese kann natürlich auch für andere Panels verwendet werden). Nun fahren wir mit dem Element der Haupt-Container-Klasse fort, welcher als übergeordneter Container von allen anderen cBox Containern auf diesem Panel fungieren soll:

class CPipValueCalculatorDialog : public CAppDialog
  {
protected:
   CBox              m_main;
// weiterer code hier...

Wir haben nun den Haupt-Container des Panels definiert, aber nicht die aktuelle Funktion für seine Erzeugung. Dazu fügen wir eine weitere Klassen-Methode der Panel-Klasse hinzu:

// start der Klassen-definition
// ...
public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
protected:
   virtual bool      CreateMain(const long chart,const string name,const int subwin);
// the rest of the definition
// ...

Anschließend definieren wir den aktuellen Körper der Klassen-Methode außerhalb der Klasse (ähnlich wie Wie die Körper der Klassen Konstruktor und Destruktor definiert werden):

bool CPipValueCalculatorDialog::CreateMain(const long chart,const string name,const int subwin)
  {   
   //Erzeugung des Haupt-CBox Containers
   if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);   

   //Hinzufügen des vertikalen Layout
   m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
   
   //Abstand setzen (padding) 10 px auf allen Seiten
   m_main.Padding(10);
   return(true);
  }

Wir verwenden CDialog::m_client_area.Width() und CDialog::m_client_area.Height() um die Höhe und Breite des Containers anzugeben. Damit nimmt dieser den gesamten Platz der 'Client Area' in Anspruch. Wir führen zudem noch ein paar Modifikationen des Containers durch: Die Anwendung des vertikalen Styles, und das Setzen des Abstandes (Padding) auf 10 pixels zu jeder Seite. Diese Funktionen werden von der CBox Klasse zur Verfügung gestellt.

Nachdem wir nun die Elemente der Container-Klasse definiert haben, erzeugen wir nun die Elemente für die Reihen, so wie es in Abbildung 9 dargestellt ist. Für die oberste Zeile, die die Zeile für das Symbol ist, deklarieren wir zunächst den Container und dann die darin enthaltenen wesentlichen Controls.

CBox              m_main;
CBox              m_symbol_row;   //row container
CLabel            m_symbol_label; //label control
CEdit             m_symbol_edit;  //edit control

Sie auch mit dem Hauptcontainer, definieren wir eine Funktion für die Erzeugung des aktuellen Reihen-Containers

bool CPipValueCalculatorDialog::CreateSymbolRow(const long chart,const string name,const int subwin)
  {
   //Erzeugung des CBox-Containers für diese Reihe (Symbol Reihe)
   if(!m_symbol_row.Create(chart,name+"symbol_row",subwin,0,0,CDialog::m_client_area.Width(),CONTROL_HEIGHT*1.5))
      return(false);

   //Erzeugung des Controls label
   if(!m_symbol_label.Create(chart,name+"symbol_label",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))
      return(false);
   m_symbol_label.Text("Symbol");
   
   //Erzeugung des Edit-Controlsl
   if(!m_symbol_edit.Create(chart,name+"symbol_edit",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))
      return(false);
   m_symbol_edit.Text(m_symbol.Name());

   //Hinzufügen der Controls zu dem übergeordneten Container (Reihe)
   if(!m_symbol_row.Add(m_symbol_label))
      return(false);
   if(!m_symbol_row.Add(m_symbol_edit))
      return(false);
   return(true);
  }

In dieser Funktion erstellen wir zuerst den Symbolreihen-Container Beachten Sie, dass wir die gesamte Breite der 'Client Area' als Breite verwenden, während wir seine Höhe 50% größer als die Control Größe, die wir vorher definiert haben, machen.

Nach der Erzeugung der Reihe, erzeugen wir die individuellen Controls. In diesem Fall verwenden sie die Control-Breite- und Höhe-Makros, wie sie vorher definiert wurden. Beachten Sie auch, wie wir diese Controls erzeugen:

Create(chart,name+"symbol_edit",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))

Die Werte in rot sind die X1 und Y1 Koordinaten. Das bedeutet, dass während der Erzeugung alle Controls an der oberen linken Seite des Charts platziert werden. Die Neuanordnung findet statt, sobald wir die Methode Pack() der Klasse CBox aufrufen.

Wir haben nun den Reihen-Container erzeugt. Zudem haben wir die wichtigen Controls innerhalb dieses Containers erzeugt. Im nächsten Schritt werden wir die gerade erzeugten Controls dem Reihen-Container hinzufügen:

if(!m_symbol_row.Add(m_symbol_label))
   return(false);
if(!m_symbol_row.Add(m_symbol_edit))
   return(false);

Für die anderen Reihen (pip size, pip value, und Button-Reihen), implementieren wir in etwa die gleichen Methoden, wie wir es für die Symbolreihe gemacht haben.

Die Erzeugung des Haupt-Containers und andere Reihen-Nachkommen (Child-rows) werden benötigt, wenn die CBox Klasse verwendet wird. Jetzt bewegen wir uns auf vertrautem Boden, nämlich die Schaffung des Panel-Objektes selbst. Dieses wird durch das Überschreiben der virtuellen Methode Create() der CAppDialog Klasse durchgeführt. In dieser Methode machen unsere zwei vorher definierten Methoden Sinn, da sie innerhalb dieser Methode aufgerufen werden:

bool CPipValueCalculatorDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   //create CAppDialog panel
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   
   //Die Erzeugung des Haupt CBox Containers unter Verwendung der Funktionen, die wir vorher definiert haben  
   if(!CreateMain(chart,name,subwin))
      return(false);  

   //Die Erzeugung des Reihen CBox Containers, unter der Verwendung der Funktion, die wir vorher definiert haben  
   if(!CreateSymbolRow(chart,name,subwin))
      return(false);

   //Hinzufügen des Reihen-Containers als Nachkomme (Child) des Hauptcontainers
   if(!m_main.Add(m_symbol_row))
      return(false);

   //Rendern des Haupt-CBox Containers und allen seinen Nachkommen (child) Containers (rekursiv)
   if (!m_main.Pack())
      return(false);
   
   //Hinzufügen des Haupt CBox Containers als das einzige 'Kind' (Child) der Panel-Client-Area
   if (!Add(m_main))
      return(false);
   return(true);
  }

Vergessen Sie nicht die überschriebene Create()-Methode in der CPipValueCalculatorDialog Klasse zu deklarieren:

public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);

Wie hier dargestellt, muss es sich um eine public-Klassenmethode handeln, da wir sie außerhalb der Klasse aufrufen. Um genauer zu sein, dieses wird in dem Haupt-Sourcecode-File benötigt: PipValueCalculator.mq5:

#include "PipValueCalculator.mqh"
CPipValueCalculatorDialog ExtDialog;
//+----------------------------------------------------------------
//| Expert Initialisierungs-Function                                   |
//+----------------------------------------------------------------
int OnInit()
  {
//---
//--- Erzeugung des Anwendungs-Dialogs
   if(!ExtDialog.Create(0,"Pip Value Calculator",0,50,50,279,250))
      return(INIT_FAILED);
//--- Start der Applikation
   if(!ExtDialog.Run())
      return(INIT_FAILED);
//--- ok
   return(INIT_SUCCEEDED);
  }
//+----------------------------------------------------------------
//| Expert deinitialization Funktion                                 |
//+----------------------------------------------------------------
void OnDeinit(const int reason)
  {
//---
   ExtDialog.Destroy(reason);
  }
//+----------------------------------------------------------------
//| Expert tick Funktion                                             |
//+----------------------------------------------------------------
void OnTick()
  {
//---
  }
//+----------------------------------------------------------------
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
    //Zurzeit auskommentiert, es wird später besprochen  
    //ExtDialog.ChartEvent(id,lparam,dparam,sparam);
  }
//+----------------------------------------------------------------

Dieser Programmcode ist ähnlich zu dem, was wir normalerweise bei Panel-Source-Code-Files sehen, mit der Ausnahme von drei Dingen:

  1. Wir fügen 'PipValueCalculator.mqh' als Header-File hinzu, anstatt der include-Datei für CAppDialog. 'PipValueCalculator.mqh' beinhaltet schon die Header-Datei, daher ist es nicht nötig diese in dem Haupt-Source-File einzuschließen. 'PipValueCalculator.mqh' ist ebenso verantwortlich für das Einschließen (include)der CBox-Klassen-Header-Datei.
  2. Wir definieren ExtDialog als eine Instanz der Klasse, die wir zuvor in 'PipValueCalculator.mqh' definiert haben. (PipValueCalculator class).
  3. Wir definieren eine benutzerdefinierte Größe des Panels, welche passender ist als wie in der Methode ExtDialog.Create() definiert wird.

Wenn wir diesen Code nur mit der Symbolreihe kompilieren, dann sollte das Panel wie folgt aussehen:

Pip Wert-Rechner mit einer Reihe

Abbildung 10. Pip Wert-Rechner mit einer Reihe

Der Hauptcontainer hat ein vertikales Layout und ist zentriert, wobei die Symbolreihe ein horizontales Layout besitzt (Und ist ebenso horizontal und vertikal zentriert). Damit das Panel so aussieht, wie in der Abbildung 8, müssen wir noch die anderen Reihen hinzufügen. Hierzu können wir die gleichen Methoden verwenden, wie wir sie bei den Symbolreihen verwendet haben. Die einzige Ausnahme ist die Button-Reihe, welche lediglich ein einziges Control besitzt (Button) und rechts angeordnet werden muss.

m_button_row.HorizontalAlign(HORIZONTAL_ALIGN_RIGHT);

Die Behandlung von Events geht eigentlich über den Rahmen dieses Artikels hinaus aber aus Gründen der Vollständigkeit werden wir hier ein wenig darauf eingehen. Wie beginnen mit der Deklaration eines neuen Elementes der Klasse PipValueCalculator, m_symbol. Wir fügen noch zwei weitere Elemente, m_digits_adjust und m_points_adjust hinzu, welche später für die Konvertierung der Größe von Punkten zu Pips verwendet wird.

CSymbolInfo      *m_symbol;
int               m_digits_adjust;
double            m_points_adjust;

Wir initialisieren m_symbol In dem Konstruktor oder in der Create()-Methode, mit dem folgenden Code:

if (m_symbol==NULL)
      m_symbol=new CSymbolInfo();
if(m_symbol!=NULL)
{
   if (!m_symbol.Name(_Symbol))
      return(false);
}   

Wenn der Symbol-Pointer Null ist, dann erzeugen wir eine neue Instanz von CSymbolInfo. Wenn er nicht Null ist dann weisen wir dem Symbolnamen das Chart Symbol zu.

Im nächsten Schritt definieren wir einen Eventhandler für das Click-Event eines Buttons. Dieses geschieht durch die Implementation der OnClickButton() Klassen-Methode. Der Körper der Methode sieht wie folgt aus:

void CPipValueCalculatorDialog::OnClickButton()
  {
   string symbol=m_symbol_edit.Text();
   StringToUpper(symbol);
   if(m_symbol.Name(symbol))
     {
      m_symbol.RefreshRates();
      m_digits_adjust=(m_symbol.Digits()==3 || m_symbol.Digits()==5)?10:1;
      m_points_adjust=m_symbol.Point()*m_digits_adjust;
      m_pip_size_edit.Text((string)m_points_adjust);      
      m_pip_value_edit.Text(DoubleToString(m_symbol.TickValue()*(StringToDouble(m_pip_size_edit.Text()))/m_symbol.TickSize(),2));
     }
   else Print("invalid input");
  }

Die Methode berechnet den Pip-Wert, indem Sie zunächst den Wert des Controls m_symbol_edit abfragt. Als nächstes gibt sie den Namen des Symbols an die Instanz der CSymbolInfo Klasse weiter. Diese Klasse ermittelt den Tick-Wert des angegebenen Symbols, welches dann mit einem Multiplizierer angepasst wird, damit wir den Wert für einen Pip erhalten.

Der finale Schritt um das Eventhandling der Klasse aktivieren zu können, ist die Definition eines Eventhandler (auch innerhalb der PipValueCalculator-Klasse). In dem Bereich der öffentlichen Methoden der Klasse fügen Sie folgenden Code hinzu:

virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

Anschließend definieren wir den Körper der Klassenmethode außerhalb der Klasse unter Verwendung des folgenden Code-Ausschnitts:

EVENT_MAP_BEGIN(CPipValueCalculatorDialog)
   ON_EVENT(ON_CLICK,m_button,OnClickButton)
EVENT_MAP_END(CAppDialog)


5.2. Beispiel #2: Rekonstruierung des Controls-Beispiels

Das Controls-Panel-Beispiel wird automatisch bei einer frischen Neuinstallation des Metatraders installiert. Im Navigator Fenster finden Sie es unter Expert Advisors\Examples\Controls. Ein Screenshot des Panels finden Sie hier:

Controls - dialog

Abbildung 11. Controls Dialog (Original)

Das Layout des Dialogfensters, welches oben abgebildet ist, wird detailiert in der folgenden Abbildung dargestellt. Um dieses Panel mit CBox-Instanzen rekonstruieren zu können, benötigen wir vier horizontale Reihen (in violett) für die folgenden Controls:

  1. Das Edit Control;
  2. Die drei Button Controls;
  3. Das SpinEdit und das DatePicker Control;
  4. Die ComboBox, RadioGroup und CheckGroup (Spalte 1) und die ListView (Spalte 2) Controls.

Der letzte horizontale Container ist ein spezieller Fall, da er ein verschachtelter Container ist, welcher zwei weitere Container enthält (Spalte 1 und 2, in Grün). Diese Container haben vertikale Layouts.

Abbildung 12. Controls Dialog Layout

Abbildung 12. Controls Dialog Layout

Wenn der Controls Dialog rekonstruiert wird, müssen alle Zeilen, welche einen Aufruf Add() besitzen, entfernt werden, ausgenommen bei dem Haupt-Container, der als einziger Nachfahre (Child) der Dialog Client-Area handelt. Nun sollten die anderen Controls und Container zu den dafür vorgesehenen übergeordneten Containern von der tiefsten Ebene aus bis zum Hauptcontainer hinzugefügt werden und letztlich dann der nativen 'Client Area' hinzugefügt werden.

Wenn Sie dieses installiert und kompiliert haben, führen Sie es aus. Alles sollte nun einwandfrei arbeiten, bis auf den DateTimePicker, dessen Inkrement, Dekrement und List-Buttons nicht funktionieren. Dieses basiert auf dem Fakt, dass die Dropdown-Liste des CDatePicker auf den Hintergrund der anderen Container festgelegt ist. Um dieses Problem zu lösen, suchen Sie in dem Verzeichnis %Data Folder%\MQL5\Include\Controls\DatePicker.mqh nach der Datei der CDatepicker Klasse. Suchen Sie in der Datei nach der Methode ListShow() und fügen Sie am Anfang der Methode den folgenden Code hinzu:

BringToTop();

Kompilieren und testen Sie bitte erneut. Jetzt sollte die Dropliste des Datepickers im Vordergrund sein und auch Click-Events verarbeiten. Hier ist der Code-Ausschnitt der gesamten Funktion:

bool CDatePicker::ListShow(void)
  {
   BringToTop();
//--- set value   
   m_list.Value(m_value);
//--- show the list
   return(m_list.Show());
  }

Ein Screenshot des neu konstruierten Control Dialogs:

Controls - Rekonstruierter Dialog

Abbildung 13. Controls Dialog (Unter Verwendung von CBox)

Mit dem Blick auf das große Bild könnte man denken, dass es dem Original fast identisch ist. Und doch gibt es einen überraschenden Unterschied, welcher durch Zufall entstanden ist: Spalte 1 ist perfekt an Spalte 2 ausgerichtet. In der ursprünglichen Version können wir sehen, dass die CheckGroup und die ListView an ihre Unterkante gleich ausgerichtet sind. Aber am oberen Rand stimmt die ComboBox mit dem Rand von der ListView nicht überein. Natürlich könnte man die Koordinaten neu positionieren aber dieses würde eine neue Justage nicht nur von den Koordinaten der ComboBox bedeuten, sondern auch die Koordinaten von der RadioGroup und die Abstände der 3 Controls betreffen. Wenn man jedoch den CBox Container verwendet, dann braucht man lediglich den oberen und unteren Abstand auf Null zu setzen und man muss die richtige Ausrichtung verwenden.

Aber dieses bedeutet nicht, dass die Verwendung von CBox und der Anordnung unter dem Aspekt der Genauigkeit ideal ist. Obwohl die Methode etwas weniger präzise ist als das genaue Eingabe von Koordinaten für die Controls, ist die Verwendung von Containern und Layouts trotzdem in der Lage eine ausreichende Genauigkeit zu bieten, während zudem die Zeit für die Entwicklung einer GUI verkürzt wird.


6. Vor- und Nachteile

Vorteile:

  • Der Code ist wiederverwendbar — Sie können die CBox Klasse oder andere Layout-Klassen über verschiedene Applikationen und Dialoge hinweg verwenden.
  • Sie ist skalierbar — Obwohl der Quellcode in kleineren Anwendung länger ausfallen kann, sind die Vorteile dieses Verfahrens bei komplexen Panels und Dialogen deutlich zu sehen.
  • Segmentation der Controls — Dieses erlaubt es Ihnen, Controls zu verändern ohne dabei die Position anderer Controls zu beeinflussen.
  • Automatische Positionierung — Ränder, Lücken und Abstände werden innerhalb dieser Klasse automatisch berechnet und müssen nicht manuell programmiert werden.

Nachteile:

  • Sie müssen für die Container zusätzliche Steuerelemente erzeugen, was die Erzeugung von zusätzlichen Funktionen für die Benutzung dieser Container erfordert.
  • Weniger präzise — Die Positionierung ist abhängig von den zur Verfügung stehenden Optionen des Layouts und der Ausrichtung.
  • Es können Probleme oder Schwierigkeiten entstehen, wenn ein Container Steuerelemente mit unterschiedlicher Größe enthält, wobei entweder der Unterschied in der Größe auf ein Minimum gelegt werden muss oder weitere Container verwendet werden müssen.


7. Schlussfolgerung

In diesem Artikel haben wir die Möglichkeit der Verwendung von Layouts und Containern für das Design von grafischen Panels betrachtet. Dieser Ansatz erlaubt es uns, den Prozess der Positionierung von Controls mit Hilfe von Layout- und auf Ausrichtungs-Stilen zu automatisieren. Sie vereinfacht das Designen von grafischen Panels und in einigen Fällen reduziert sie auch den Programmieraufwand.

Die CBox Klasse ist ein externes Control, welches als ein Container für besondere Controls in einem GUI-Panel agiert. In diesem Artikel haben wir die Arbeitsweise, und wie es in einer wirklichen Anwendung angewendet werden kann, demonstriert Obwohl dieses Verfahren weniger genau ist als die absolute Angabe der Position, stellt sie immer noch ein Grad an Genauigkeit dar, die für eine Vielzahl von Anwendungen geeignet ist.