MetaTrader 5 herunterladen

Die Verwendung von Layout und Containern für GUI Controls: Die CGrid Klasse

18 Mai 2016, 09:41
Enrico Lambino
0
290

Inhaltsverzeichniss


1. Einleitung

Die CGrid Klasse ist ein Layout Manager, welcher für das Design von GUI-Controls für Dialogfenster in Metatrader verwendet wird. Dieses ist eine der benutzerdefinierten Containerklassen, welche für das GUI Design ohne die absolutes Positionierung von Controls verwendet wird.

Es wird empfohlen den Artikel Artikel über die CBox Klasse zu lesen, bevor Sie mit diesem Artikel fortfahren.


2. Ziele

Die Verwendung der CBox Klasse ist für die meisten einfachen Dialogfenster ausreichend. Aber wenn sich die Anzahl der Controls in den Dialogfenster erhöht, ist die Verwendung von multiplen CBox-Containern mit folgenden Nachteilen behaftet:

  • Eine tiefere Verschachtelung von Controls.
  • Es werden mehrere Container-Controls für das Layout benötigt.
  • Es sind sehr viele Code-Zeilen nötig um einfache Dinge zu erstellen.

Die meisten dieser Nachteile mit der CBox-Klasse können umgangen werden, wenn man die Controls in einem Gitter platziert, anstatt in individuellen Containern. Die Ziele dieses Artikels sind die folgenden:

  • Implementierung einer Klasse, um Controls innerhalb eines definierten Gitters zu platzieren.
  • Implementierung einer einfacheren Alternative zu verschachtelten CBox Containern.

Und ähnlich wie bei der CBox Klasse, sollen auch hier die folgenden Ziele erreicht werden:

  • Der Programmcode sollte wiederverwendbar sein.
  • Die Änderung eines Teils des Interface sollte nur minimale Auswirkungen auf andere Komponenten haben.
  • Die Positionierung der Komponenten innerhalb des Interfaces sollte automatisch erfolgen.

In diesem Artikel haben wir das Ziel, einen Layout Manager zu definieren, welcher die oben aufgelisteten Ziele unter Verwendung der CGrid Klasse realisiert.


3. Die CGrid Klasse

Die ABC Klasse generiert einen Container für einen oder mehrere GUI-Controls und präsentiert diese in einem Gitter. In dem nachfolgenden Bild wird ein Beispiel-Layout von einer Instanz der CGrid Klasse gezeigt

CGrid Layout

Abbildung 1. Grid Layout

Die Verwendung dieser Klasse kann sehr nützlich sein, insbesondere dann, wenn wir es mit Controls zu haben zu tun haben, die alle identische Abmessungen haben. Dazu zählen zum Beispiel Buttons oder Edit-Boxen.

Das oben aufgeführte Beispiel ist ein Gitter mit 4 x 4 Zellen (4 Spalten und 4 Zeilen). Allerdings wollen wir eine Klasse entwickeln, die eine beliebige Anzahl von Zeilen und Spalten in einem Raster unterbringen kann.

Wir definieren die CGrid Klasse als ein Nachkomme der CBox Klasse. Damit ist es uns möglich, die virtuellen Funktionen der übergeordneten Klasse zu überschreiben. Zudem gibt uns das die Möglichkeit, die Instanzen dieser Klasse wie die Instanzen der CBox zu manipulieren:

#include "Box.mqh"
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
class CGrid : public CBox
  {
protected:
   int               m_cols;
   int               m_rows;
   int               m_hgap;
   int               m_vgap;
   CSize             m_cell_size;
public:
                     CGrid();
                     CGrid(int rows,int cols,int hgap=0,int vgap=0);
                    ~CGrid();
   virtual int       Type() const {return CLASS_LAYOUT;}
   virtual bool      Init(int rows,int cols,int hgap=0,int vgap=0);
   virtual bool      Create(const long chart,const string name,const int subwin,
                            const int x1,const int y1,const int x2,const int y2);
   virtual int       Columns(){return(m_cols);}
   virtual void      Columns(int cols){m_cols=cols;}
   virtual int       Rows(){return(m_rows);}
   virtual void      Rows(int rows){m_rows=rows;}
   virtual int       HGap(){return(m_hgap);}
   virtual void      HGap(int gap){m_hgap=gap;}
   virtual int       VGap(){return(m_vgap);}
   virtual void      VGap(int gap){m_vgap=gap;}
   virtual bool      Pack();
protected:
   virtual void      CheckControlSize(CWnd *control);
  };
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGrid::CGrid()
  {
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGrid::CGrid(int rows,int cols,int hgap=0,int vgap=0)
  {
   Init(rows,cols,hgap,vgap);
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGrid::~CGrid()
  {
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CGrid::Init(int rows,int cols,int hgap=0,int vgap=0)
  {
   Columns(cols);
   Rows(rows);
   HGap(hgap);
   VGap(vgap);
   return(true);
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CGrid::Create(const long chart,const string name,const int subwin,
                   const int x1,const int y1,const int x2,const int y2)
  {
   return(CBox::Create(chart,name,subwin,x1,y1,x2,y2));
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CGrid::Pack()
  {
   CSize size=Size();
   m_cell_size.cx = (size.cx-((m_cols+1)*m_hgap))/m_cols;
   m_cell_size.cy = (size.cy-((m_rows+1)*m_vgap))/m_rows;
   int x=Left(),y=Top();
   int cnt=0;
   for(int i=0;i<ControlsTotal();i++)
     {
      CWnd *control=Control(i);
      if(control==NULL)
         continue;
      if(control==GetPointer(m_background))
         continue;
      if(cnt==0 || Right()-(x+m_cell_size.cx)<m_cell_size.cx+m_hgap)
        {
         if(cnt==0)
            y+=m_vgap;            
         else y+=m_vgap+m_cell_size.cy;
         x=Left()+m_hgap;
        }
      else x+=m_cell_size.cx+m_hgap;    
      CheckControlSize(control);
      control.Move(x,y);
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }
      cnt++;
     }   
   return(true);
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGrid::CheckControlSize(CWnd *control)
  {
   control.Size(m_cell_size.cx,m_cell_size.cy);
  }
//+----------------------------------------------------------------


3.1. Initialisierung

Genauso wie bei den anderen Containern und controls, erzeugen wir das Gerät über den Aufruf der Methode Create() dieser Klasse. Aber ähnlich wie bei jeder anderen Instanz der CBox Klasse, ist die Angabe der Position des Controls an diesem Punkt optional. Wir können einfach die Höhe und die Breite des Controls deklarieren, unter Verwendung der Eigenschaften x2 und y2. Falls dieses Gitter der einzige Container ist, der zu der Client Area hinzugefügt werden soll, dann kann der folgende Code verwendet werden (mit m_main als eine Instanz von CGrid):

if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);

Direkt nach der Erzeugung, müssen wir das Gitter über den Aufruf von Init()initialisieren. Bei der Initialisierung einer Instanz eines Gitters, müssen wir die Anzahl an Spalten und Zeilen angeben, in die die Client-Area aufgeteilt werden soll, sowie der Abstand zwischen jeder Zelle in dem Gitter (Horizontal uns vertikal). Um das Gitter zu initialisieren, müssen wir die Init()-Methode in dem Quellcode aufrufen. Der folgende Programmcode erzeugt ein 4x4 Gitter mit horizontalen und vertikalen Zwischenräumen zwischen jeder Zelle von zwei Pixeln:

m_main.Init(4,4,2,2);

Die Init() Methode hat vier Parameter:

  1. Anzahl der Zeilen
  2. Anzahl der Spalten;
  3. Horizontale Lücke (in pixels);
  4. Vertikale Lücke (in pixels).

Die Angabe der horizontalen und vertikalen Lücken zwischen den Zellen sind optionale Parameter. Standardmäßig sind diese Werte Null, sofern sie nicht mit benutzerdefinierten Werten initialisiert werden.


3.2. Abstand zwischen Controls

Die hgap (Horizontale Lücke) und vgap (Vertikale Lücke) Parameters definieren den Abstand zwischen jeder Zelle in dem Gitter. Da das Gitter die Fläche der Client Area maximal ausnutzt, ist der total verbleibende Raum für Controls in der horizontalen und vertikalen Richtung über die folgende Formel zu errechnen:

Verbleibende totale Größe für Controls = Gesamter Platz - (Lücke * (Anzahl von Zellen+1))

Die oben dargestellte Formel wird innerhalb der Pack() Funktion der Klasse verwendet.


3.3. Größenveränderung von Controls

In der CGrid Klasse wird die Größe jedes Controls so eingestellt, dass sie den gesamten Raum der Zelle ausfüllt. Aus diesem Grund kann bei der Verwendung von solch einem Layout die initiale Größe eines Controls mit 0 angegeben werden. Das Control wird dann zu einem späteren Zeitpunkt, während das Haupt-Dialogfenster erzeugt wird, in seiner Größe verändert (CDialog oder CAppDialog) sobald die Pack() Methode der Klasse aufgerufen wird.

Die verbleibende Größe (horizontal oder vertikal), die mit der oben angegebenen Formel berechnet wird, bestimmt die X- oder Y- Größe von jeder einzelnen Zelle in dem Gitter. Für die Größe jeder Zelle innerhalb des Gitters, wird die folgende Formel verwendet:

xsize = total size left for controls / total number of columns

ysize = total size left for controls / total number of rows

Die aktuelle Veränderung der Größe wird innerhalb der Methode CheckControlSize() dieser Klasse durchgeführt.


4. Beispiel #1: Ein einfaches Gitter mit Buttons

Um ein einfaches Beispiel für die Verwendung der ABC Klasse zu geben, präsentieren wir hier ein einfaches Gitter mit Buttons: Hier ist ein Screenshot einer GUI:

Ein einfaches Gitter mit Buttons

Figure 2. Ein einfaches Gitter mit Buttons

Wie wir sehen können, haben wir hier ein Gitter aus 3x3 Zellen, wobei jede Zelle einen Button enthält. Jeder Button wird gleichmäßig in dem Gitter verteilt, wobei das das Gitter die komplette Client-Area füllt.

Um dieses Grid erzeugen zu können, müssen wir einen EA oder einen Indikator erstellen, welcher den in dem Artikel über die CBox-klasse vorgegebenen Strukturen folgt, was im Wesentlichen den Beispielen über Controls im Metatrader ähnelt. Wir deklarieren also ein Source-Code-File, welches die Deklaration einer Instanz eines benutzerdefinierten CAppDialog-Fensters enthält (Zusammen mit anderen Eventhandler), und verbinden (link) es mit dem Header-File, welches die aktuelle Deklaration der verwendeten Klasse beinhaltet.

Für das 3 x 3 Gitter benötigen wir eine Instanz der ABC Klasse als ein Element der Klasse, zusammen mit einer Gruppe von neun Buttons (einer für jede Zelle des Gitters):

class CGridSampleDialog : public CAppDialog
  {
protected:
   CGrid             m_main;
   CButton           m_button1;
   CButton           m_button2;
   CButton           m_button3;
   CButton           m_button4;
   CButton           m_button5;
   CButton           m_button6;
   CButton           m_button7;
   CButton           m_button8;
   CButton           m_button9;
public:
                     CGridSampleDialog();
                    ~CGridSampleDialog();
  };

In den nächsten Schritt über schreiben wir die öffentlichen virtuellen Funktionen der CAppDialog Klasse

public:
                     CGridSampleDialog();
                    ~CGridSampleDialog();
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
bool CGridSampleDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateMain(chart,name,subwin))
      return(false);   
   for(int i=1;i<=9;i++)
     {
      if(!CreateButton(i,chart,"button",subwin))
         return(false);
     }   
   if(!m_main.Pack())
      return(false);
   if(!Add(m_main))
      return(false);
   return(true);
  }
EVENT_MAP_BEGIN(CGridSampleDialog)
EVENT_MAP_END(CAppDialog)

Die event map bleibt in diesem Beispiel leer, da wir den Buttons keine Events zuweisen wollen.

Im letzten Schritt müssen wir die geschützten Funktionen der Klasse deklarieren, welche diejenigen sind, die für die Konstruktion des Gitters und der Controls verwendet werden:

protected:
   virtual bool      CreateMain(const long chart,const string name,const int subwin);
   virtual bool      CreateButton(const int button_id,const long chart,const string name,const int subwin);

Bei der Verwendung dieses Beispiels, können wir schon einige Vorteile der CGrid Klasse gegenüber der CBox Klasse sehen. Um das gleiche Layout mit der CBox Klasse zu konstruieren, würden wir vier unterschiedliche Container benötigen. Das liegt daran, dass die CBox nur eine einzige Spalte oder eine einzige Zeile handhaben kann. Mit der Verwendung von CGrid, haben wir die Anzahl der Container von 4 auf 1 reduziert, was natürlich auch zu weniger Deklarationen und Programmcode führt.

bool CGridSampleDialog::CreateMain(const long chart,const string name,const int subwin)
  {
   if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);
   m_main.Init(3,3,5,5);
   return(true);
  }

Die CreateMain() Klassenmethode ist für die Konstruktion des Gitter-Controls selbst verantwortlich. Sie arbeitet ähnlich wie die Erzeugung eines Controls mit der CBox. Der einzige Unterschied liegt darin, dass die CGrid eine weitere Methode benötigt, die Init()-Methode; Auf der anderen Seite, braucht die CBox dieses nicht.

Die Implementation für das Klassen-Element CreateButton(), wird in dem nachfolgenden Code gezeigt

bool CGridSampleDialog::CreateButton(const int button_id,const long chart,const string name,const int subwin)
  {
   CButton *button;
   switch(button_id)
     {
      case 1: button = GetPointer(m_button1); break;
      case 2: button = GetPointer(m_button2); break;
      case 3: button = GetPointer(m_button3); break;
      case 4: button = GetPointer(m_button4); break;
      case 5: button = GetPointer(m_button5); break;
      case 6: button = GetPointer(m_button6); break;
      case 7: button = GetPointer(m_button7); break;
      case 8: button = GetPointer(m_button8); break;
      case 9: button = GetPointer(m_button9); break;
      default: return(false);
     }
   if (!button.Create(chart,name+IntegerToString(button_id),subwin,0,0,100,100))
      return(false);
   if (!button.Text(name+IntegerToString(button_id)))
      return(false);
   if (!m_main.Add(button))
      return(false);
   return(true);
  }

Da die Erzeugung jeder der Buttons sich sehr ähnelt, verwenden wir eine einzige Methode für die Erzeugung der Buttons, anstelle einer Funktion für jeden einzelnen Button. Dieses wird über die implementierte CreateButton()-Methode erreicht. Wir rufen diese Methode innerhalb der virtuellen Create() Klassenmethode, direkt nachdem wir das Dialogfenster und das Gitter erzeugt haben, auf. Wie in dem Code-Ausschnitt für die virtuelle Create()-Methode dargestellt, haben wir eine for -Schleife dafür verwendet. Da die Buttons schon statisch innerhalb der Klasse deklariert sind, sind die Buttons schon aufgrund der Deklaration erzeugt worden. Daher ist die Verwendung des new-Operators nicht mehr notwendig. Wir erhalten daher den pointer (automatisch) zu jedem Button und können damit die Create()-Methode jedes Buttons aufrufen.


5. Beispiel #2: Schiebe-Puzzle

Unser zweites Beispiel beinhaltet ein Spiel, welches Schiebepuzzle genannt wird. In diesem Spiel bekommt der User eine Gruppe von Zahlen, die von 1 bis 15 reicht und in einem 4 x 4 Gitter enthalten sind. Das Ziel für den Anwender ist die Neuordnung der Zahlen, so, dass sie eine aufsteigende oder absteigende Reihenfolge ergeben. Sobald der Anwender eine sortierte Reihenfolge gebildet hat, ist das Ziel erreicht, so wie es in dem folgenden Screenshot gezeigt wird:

Sliding Puzzle

Abbildung 3. Sliding Puzzle

Zu den Klassen-Methoden, die für eine Konstruktion eines Dialogfensters notwendig sind, benötigen diese Anwendung die folgenden zusätzlichen Eigenschaften:

  • Eine Methode für die Erzeugung der Buttons;
  • Eine Methode für die zufällige Anordnung der Kacheln;
  • Eine Methode welche überprüft ob sich eine bestimmte Zelle neben einer Leeren Zelle befindet.
  • Eine Methode für die Überprüfung, ob das Puzzle bereits gelöst worden ist.
  • Eine Methode für die click Events jedes einzelnen Buttons in dem Gitter.


5.1. Erzeugung eines Dialogfensters

Wir deklarieren die Klasse als eine Erweiterung der CAppDialog Klasse, mit ihren geschützten oder privaten Elementen, Konstruktoren und Destruktoren:

class CSlidingPuzzleDialog : public CAppDialog
  {
protected:
   CGrid             m_main;
   CButton           m_button1;
   CButton           m_button2;
   CButton           m_button3;
   CButton           m_button4;
   CButton           m_button5;
   CButton           m_button6;
   CButton           m_button7;
   CButton           m_button8;
   CButton           m_button9;
   CButton           m_button10;
   CButton           m_button11;
   CButton           m_button12;
   CButton           m_button13;
   CButton           m_button14;
   CButton           m_button15;
   CButton           m_button16;
   CButton          *m_empty_cell;
public:
                     CSlidingPuzzleDialog();
                    ~CSlidingPuzzleDialog();   
  };

Der folgende Code zeigt die Create()-Methode dieser Klasse.

Deklaration (Unterhalb der Klassendefinition, Öffentliche Funktionen):

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

Implementation:

bool CSlidingPuzzleDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateMain(chart,name,subwin))
      return(false);
   for(int i=1;i<=16;i++)
     {
      if(!CreateButton(i,chart,"button",subwin))
         return(false);
     }
   m_empty_cell=GetPointer(m_button16);
   if(!m_main.Pack())
      return(false);
   if(!Add(m_main))
      return(false);
   Shuffle();
   return(true);
  }

Hier können wir sehen, dass der Dialog die Funktion CreateMain() besitzt, welche für die Konstruktion des Gitters sorgt, und CreateButton() in einer for Schleife ausführt, welche für die Erzeugung der Buttons in dem Gitter verwendet wird. Wir sehen hier zudem die Pack()-Methode der CGrid Instanz, welche für die Neupositionierung der Controls und das Hinzufügen des Gitters zu der Client Area unter Verwendung der Add-Methode verantwortlich ist. Die Initialisierung des Spiels ist auch schon über die Methode Shuffle() anwesend.


5.2. Buttons

Der folgende Code-Ausschnitt zeigt die Methode CreateButton():

bool CSlidingPuzzleDialog::CreateButton(const int button_id,const long chart,const string name,const int subwin)
  {
   CButton *button;
   switch(button_id)
     {
      case 1: button = GetPointer(m_button1); break;
      case 2: button = GetPointer(m_button2); break;
      case 3: button = GetPointer(m_button3); break;
      case 4: button = GetPointer(m_button4); break;
      case 5: button = GetPointer(m_button5); break;
      case 6: button = GetPointer(m_button6); break;
      case 7: button = GetPointer(m_button7); break;
      case 8: button = GetPointer(m_button8); break;
      case 9: button = GetPointer(m_button9); break;
      case 10: button = GetPointer(m_button10); break;
      case 11: button = GetPointer(m_button11); break;
      case 12: button = GetPointer(m_button12); break;
      case 13: button = GetPointer(m_button13); break;
      case 14: button = GetPointer(m_button14); break;
      case 15: button = GetPointer(m_button15); break;
      case 16: button = GetPointer(m_button16); break;
      default: return(false);
     }
   if(!button.Create(chart,name+IntegerToString(button_id),subwin,0,0,100,100))
      return(false);
   if(button_id<16)
     {
      if(!button.Text(IntegerToString(button_id)))
         return(false);
     }
   else if(button_id==16)
     {
      button.Hide();
     }
   if(!m_main.Add(button))
      return(false);
   return(true);
  }

Hier können wir sehen, dass diese Methode ähnlich der aus dem vorangegangenen Beispiel ist. Innerhalb dieser Methode weisen wir jeder Zelle einen anfänglichen Wert zwischen 1 und 16 zu. Zu Dienst verstecken wir die 16 Zelle, da sie als eine leere Zelle fungiert.


5.3. Überprüfung auf benachbarte Kacheln

Es ist nur notwendig zu überprüfen, ob es in eine bestimmte Richtung eine leere benachbarte Zelle gibt. Andernfalls wird die leere Zelle einen Wert mit einem Button tauschen, der nicht existiert. Die Überprüfung, ob es eine benachbarte Zelle gibt, wird mit den Funktionen HasNorth(), HasSouth(), HasEast(), and HasSouth() überprüft. Der folgende Code-Ausschnitt zeigt dieHasNorth()-Methode:

bool CSlidingPuzzleDialog::HasNorth(CButton *button,int id,bool shuffle=false)
  {
   if(id==1 || id==2 || id==3 || id==4)
      return(false);
   CButton *button_adj=m_main.Control(id-4);
   if(!CheckPointer(button_adj))
      return(false);
   if(!shuffle)
     {
      if(button_adj.IsVisible())
         return(false);
     }
   return(true);
  }

Diese Funktion prüft, ob sich ein Button oder eine leere Zelle in eine Richtung bewegen darf, welches auch die Richtungen sind, in die sich eine leere Zelle bewegen kann. Wenn ein bestimmter Button in dem Mittelpunkt des Gitters gefunden wird, dann kann er sich in alle Richtungen bewegen. Aber wenn er sich an einem Rand befindet, dann gibt es ein paar Bewegungsrichtungen die nicht existieren. Zum Beispiel, unter Nichtbeachtung von leeren Zellen, kann sich die erste Zelle des Gitters nur nach rechts oder unten sich bewegen, aber sie kann sich nicht nach links oder oben bewegen wobei die sechste Zelle sich in alle Richtungen bewegen kann.


5.4. Neuanordnung der Kacheln

Der folgende Code Ausschnitt zeigt die Shuffle() Methode dieser Klasse:

void CSlidingPuzzleDialog::Shuffle(void)
  {
   m_empty_cell=m_main.Control(16);
   for(int i=1;i<m_main.ControlsTotal()-1;i++)
     {
      CButton *button=m_main.Control(i);
      button.Text((string)i);
     }
   MathSrand((int)TimeLocal());
   CButton *target=NULL;
   for(int i=0;i<30;i++)
     {
      int empty_cell_id=(int)StringToInteger(StringSubstr(m_empty_cell.Name(),6));
      int random=MathRand()%4+1;
      if(random==1 && HasNorth(m_empty_cell,empty_cell_id,true))
         target= m_main.Control(empty_cell_id-4);
      else if(random==2 && HasEast(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id+1);
      else if(random==3 && HasSouth(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id+4);
      else if(random==4 && HasWest(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id-1);
      if(CheckPointer(target))
         Swap(target);
     }
  }

Wenn die Kacheln verschoben werden, solltet dieses mit einem Zufallsprinzip geschehen. Andernfalls würden die Kacheln immer die gleiche Reihenfolge behalten. Dazu verwenden wir die FunktionenMathSrand und MathRand. Für die Initialisierung der Zufallsfunktionen verwenden wir die lokale Uhrzeit.

Bevor eine Verschiebung stattfindet, initialisieren wir die Werte der Buttons zu ihren Default-Werten. Dieses schützt uns davor, dass das Puzzle unlösbar wird, oder es zu schwierig wird dieses zu lösen. Wir erledigen dieses, indem wir allen Zellen der Reihe nach einen Wert von 1-15 zuweisen und auch der leeren Zelle den 16 Wert zuweisen. Wir weisen auch der 16. Zelle einen leeren Zellen-Pointer zu (Klassenmember), den wir vorher deklariert haben.

Am Ende dieser Methode findet das Sortieren der Kacheln statt. Hier werden nicht wirklich die Buttons verschoben. Lediglich deren Werte werden getauscht, was die Illusion einer Bewegung verursacht. Und wie wir feststellen werden, ist dieses auch der leichtere Ansatz. Jeder Schleife überprüft, ob es eine benachbarte Kachel gibt und falls diese benachbarte Kachel eine leere Kachel ist, dann werden die Werte der leeren Kachel mit der zufällig selektierten getauscht.

Wir geben noch einen Standardwert für die maximale Anzahl von Tauschversuchen an. Der voreingestellte Wert ist 30, aber dieser Wert kann natürlich geändert werden um den Schwierigkeitsgrad zu erhöhen oder zu verringern. Die Permutation kann mehr oder weniger schwieriger sein, als der eingestellte Schwierigkeitsgrad, was davon abhängt, ob der Ziel-Button einen gültigen Pointer für jede Iteration bekommen hat, oder nicht.


5.5. Button Click Event

Um ein Click-Event eines Buttons behandeln zu können, müssen wir einen Eventhandler deklarieren. Wir verwenden hierfür eine einzige Methode, die alle Button-Click-Events verarbeitet:

CSlidingPuzzleDialog::OnClickButton(CButton *button)
  {
   if(IsMovable(button))
     {
      Swap(button);
      Check();
     }
  }

die IsMovable() Funktion überprüft mit den Funktionen (e.g. HasNorth(), HasSouth()) lediglich, ob eine bestimmte Kachel eine leere Kachel neben sich hat. Wenn der Button eine leere Kachel neben sich hat, dann ist er bewegbar und daher wird die Swap()-Funktion aufgerufen, welche den Wert des Buttons mit der leeren Zelle austauscht. Nach jedem erfolgreichen Austausch wird zudem die Funktion Check() aufgerufen.

Anschließend erzeugen wir separate Eventhandler für jeden Button. Hier ist das Beispiel für den Eventhandler des ersten Buttons:

CSlidingPuzzleDialog::OnClickButton1(void)
  {
   OnClickButton(GetPointer(m_button1));
  }

Jeder dieser Eventhandler wird die Funktion OnClickButton() aufrufen. Wir müssen diese Methoden noch in der Eventmap deklarieren:

EVENT_MAP_BEGIN(CSlidingPuzzleDialog)
   ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
   ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
   ON_EVENT(ON_CLICK,m_button3,OnClickButton3)
   ON_EVENT(ON_CLICK,m_button4,OnClickButton4)
   ON_EVENT(ON_CLICK,m_button5,OnClickButton5)
   ON_EVENT(ON_CLICK,m_button6,OnClickButton6)
   ON_EVENT(ON_CLICK,m_button7,OnClickButton7)
   ON_EVENT(ON_CLICK,m_button8,OnClickButton8)
   ON_EVENT(ON_CLICK,m_button9,OnClickButton9)
   ON_EVENT(ON_CLICK,m_button10,OnClickButton10)
   ON_EVENT(ON_CLICK,m_button11,OnClickButton11)
   ON_EVENT(ON_CLICK,m_button12,OnClickButton12)
   ON_EVENT(ON_CLICK,m_button13,OnClickButton13)
   ON_EVENT(ON_CLICK,m_button14,OnClickButton14)
   ON_EVENT(ON_CLICK,m_button15,OnClickButton15)
   ON_EVENT(ON_CLICK,m_button16,OnClickButton16)
EVENT_MAP_END(CAppDialog)

Alternativ ist es auch möglich die Eventhandler direkt aus der eventmap selbst aufzurufen, um eine Deklaration von separaten Eventhandlern für jeden Button zu vermeiden.

Zum Schluss müssen wir noch die öffentliche OnEvent()-Funktion zu der Deklaration hinzufügen:

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


5.6. Überprüfung

Nach jedem Klick muss noch geprüft werden, ob das Puzzle bereits gelöst worden ist. Dieses wird mit der Check()-Function durchgeführt:

bool CSlidingPuzzleDialog::Check(void)
  {
   for(int i=1;i<m_main.ControlsTotal()-1;i++)
     {
      CButton *button=m_main.Control(i);
      if(CheckPointer(button))
        {
         if(button.Text()!=IntegerToString(i))
           {
            Print("status: not solved: "+button.Text()+" "+IntegerToString(i));
            return(false);
           }
        }
     }
   Print("status: solved");
   return(true);
  }

Die Überprüfung wird vom zweiten Control bis zum vorletzten Control durchgeführt. das erste Control ist immer der Hintergrund, was natürlich kein Button ist, während das letzte Control immer die leere Zelle ist, welche nicht überprüft werden muss.


6. Die CGridTk Klasse


6.1. Probleme mit der CGrid Klasse

Wir stellen folgende Probleme bei der Verwendung der CGrid Klasse fest:

  • Leerer Raum — CGrid platziert das nächste Control in der nächsten Spalte der aktuellen Zeile, und wird nur zu der nächsten Zeile wechseln, wenn die aktuelle Zeile voll ist.
  • Benutzerdefiniertes Positionieren und die Größe der Controls — Das Layout kann in vielen Fällen sehr hilfreich sein, aber manchmal ist es auch zu starr. Das liegt daran, dass jedes Control innerhalb einer Zelle den gesamten Platz der Zelle ausfüllt.

Es gibt sicher einige Fälle, wo wir ein spezielles Control weiter weg von seinen Schwestern platzieren wollen. Horizontal oder vertikal. Und das mit einem Abstand, der größer ist als die vorhandene Lücke in dem Gitter. Als Beispiel könnte man hier aufführen, zwei Gruppen von Buttons voneinander zu trennen, oder die Positionierung von einem Button auf der linken Seite der Client Area und einem anderen Button auf der rechten Seite der Client-Area. Diese Arten von GUI Designs kann man sehr oft im Internet finden.

Das erstgenannte Problem könnten wir mit leere Controls lösen. Es könnte sich hierbei um einen Control mit wenigen kosmetischen Komponenten handeln, wie zum Beispiel einen Button oder ein Label. Zudem könnten wir solche unsichtbaren Controls über die Funktion Hide() rendern, so wie wir es auch mit der 16. Zelle in unserem ersten Beispiel gemacht haben. Anschließen könnten wir dann diese Controls in leere Zellen platzieren, wo wir ein wenig Raum erzeugen wollen. Dieses erzeugt dann die Illusion von Raum in einer solchen Zelle. Aber in Wirklichkeit ist diese Zelle natürlich durch ein unsichtbares Control belegt.

Diese Lösung kann natürlich sinnvoll sein für ein Dialogfenster, aber in komplexeren Dialogen, ist es uneffizient und unpraktisch. Je mehr leere Zellen benötigt werden, um so unnötig länger wird der Programmcode. Zudem wird dann das Verwalten des Codes immer schwieriger (Zum Beispiel wenn wir eine ganze Reihe oder / und Spalte von leeren Zellen haben).

Das zweite Problem hat etwas mit der Position und der Größe der Controls zu tun. In Bezug auf die Positionierung der einzelnen Controls in einer Zelle, haben wir kein Problem, solange alle Steuerelemente die gleiche Größe und den gleichen Abstand zueinander haben. Aber falls das nicht der Fall ist, dann müssen wir einen weiteren Ansatz implementieren. Die beste Lösung wäre wahrscheinlich die asymmetrische Positionierung der Controls außerhalb des Gitters durch absolute Positionierung. Eine weitere Alternative wäre die Verwendung eines weiteren Containers wie zum Beispiel einer CBox oder einer anderen Instanz von CGrid.


6.2. CGridTk: Ein verbessertes CGrid

Die Standard CGrid Klasse kann eine große Bandbreite von Applikationen abdecken. Aber ihre Möglichkeiten als ein Gitter-Container sind sehr begrenzt. Basierend auf den beiden Problemen, die wir in dem vorigen Abschnitt besprochen haben, können wir eine stark verbesserte Version mit den folgenden Eigenschaften ableiten: (zusätzlich zu CGrid):

  • Erstellen von leeren Zellen ohne GUI Control.
  • Die Platzierung von Controls mit einer benutzerdefinierten Breite und Höhe, ausgedrückt als ein Vielfaches von den Pixeln einer Zelle.

Mit diesen Eigenschaften wären wir in der Lage, die oben angesprochenen Probleme zu lösen. Zudem wird uns dieses mehr Freiheiten mit der Platzierung und der Positionierung von Zellen bieten, ähnlich wie bei der absoluten Positionierung. Aber anders als bei der absoluten Positionierung, verwenden wir hier in die Zellengröße als die Basiseinheit für die Positionierung und nicht 1 Pixel. Auch wenn wir die Präzision aus Gründen der Bequemlichkeit opfern, so ist es einfacher eine Größe von einer Zelle in einem Raster sichtbar zu machen als, sagen wir mal, 100 Pixel auf dem Bildschirm.

Wir werden die Klasse in GridTk umbenennen. Ihr Code wird hier aufgelistet:

#include "Grid.mqh"
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
class CGridTk : public CGrid
  {
protected:
   CArrayObj         m_constraints;
public:
                     CGridTk();
                    ~CGridTk();
   bool              Grid(CWnd *control,int row,int column,int rowspan,int colspan);
   bool              Pack();
   CGridConstraints     *GetGridConstraints(CWnd *control);
  };
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGridTk::CGridTk(void)
  {
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGridTk::~CGridTk(void)
  {
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CGridTk::Grid(CWnd *control,int row,int column,int rowspan=1,int colspan=1)
  {
   CGridConstraints *constraints=new CGridConstraints(control,row,column,rowspan,colspan);
   if(!CheckPointer(constraints))
      return(false);
   if(!m_constraints.Add(constraints))
      return(false);
   return(Add(control));
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CGridTk::Pack()
  {
   CGrid::Pack();
   CSize size=Size();
   m_cell_size.cx = (size.cx-(m_cols+1)*m_hgap)/m_cols;
   m_cell_size.cy = (size.cy-(m_rows+1)*m_vgap)/m_rows;   
   for(int i=0;i<ControlsTotal();i++)
     {
      int x=0,y=0,sizex=0,sizey=0;
      CWnd *control=Control(i);
      if(control==NULL)
         continue;
      if(control==GetPointer(m_background))
         continue;
      CGridConstraints *constraints = GetGridConstraints(control);
      if (constraints==NULL)
         continue;   
      int column = constraints.Column();
      int row = constraints.Row();
      x = (column*m_cell_size.cx)+((column+1)*m_hgap);
      y = (row*m_cell_size.cy)+((row+1)*m_vgap);
      int colspan = constraints.ColSpan();
      int rowspan = constraints.RowSpan();
      control.Size(colspan*m_cell_size.cx+((colspan-1)*m_hgap),rowspan*m_cell_size.cy+((rowspan-1)*m_vgap));
      control.Move(x,y);
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }
     }
   return(true);
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGridConstraints *CGridTk::GetGridConstraints(CWnd *control)
  {
   for(int i=0;i<m_constraints.Total();i++)
     {
      CGridConstraints *constraints=m_constraints.At(i);
      CWnd *ctrl=constraints.Control();
      if(ctrl==NULL)
         continue;
      if(ctrl==control)
         return(constraints);
     }
   return (NULL);
  }

Zu der Add() Methode, stellen wir eine neue Methode für das Hinzufügen von Controls zu dem Gitter vor, die Grid()-Methode. Wenn diese Methode verwendet wird, dann kann ein Control eine benutzerdefinierte Position und Größe zugewiesen werden, basierend auf einem Vielfachen der Größe von einer Zelle.

Wir sehen, dass die Klasse ein Element der CConstraints Klasse besitzt, welches wir weiter unten besprechen.


6.2.1. Dimensionen in Zeilen und Spalten

Mit den Zeilen und Spalten-Spannen, können wir nun angeben wie lang oder breit das Control sein soll. Dies ist eine Verbesserung gegenüber der Standardgröße von einer Zellengröße, aber es ist natürlich weniger präzise als die absolute Positionierung. Es ist natürlich nicht notwendig darauf hinzuweisen, dass die CGridTk Klasse nicht länger die Methode CheckControlSize() der Klassen CBox und CGrid verwendet. Vielmehr führt sie das Ändern der Größe der Controls innerhalb der Pack()-Methode selbst aus.


6.2.2. Die CConstraints Klasse

Für jedes Control müssen wir eine Reihe von Einschränkungen definieren, die festlegen, wie jedes Control im Raster positioniert wird, welche Zellen es belegt und wie die Größe verändert werden soll. Wir können die Controls direkt neu positionieren und in ihrer Größe ändern, sobald sie mit der Methode Grid() der Klasse CGridTk hinzugefügt wurden. Jedoch werden wir aus Gründen der Konsistenz die Redimensionierung und Neupositionierung der Controls erst mit dem Aufruf der Pack()-Methode durchführen. (Ähnlich wie es auch in der CBox Klasse geschieht) Und dieses zu tun, müssen wir die Einschränkungen im Speicher ablegen, was auch der eigentliche Zweck der CConstraints Klasse ist:

class CGridConstraints : public CObject
  {
protected:
   CWnd             *m_control;
   int               m_row;
   int               m_col;
   int               m_rowspan;
   int               m_colspan;
public:
                     CGridConstraints(CWnd *control,int row,int column,int rowspan=1,int colspan=1);
                    ~CGridConstraints();
   CWnd             *Control(){return(m_control);}
   int               Row(){return(m_row);}
   int               Column(){return(m_col);}
   int               RowSpan(){return(m_rowspan);}
   int               ColSpan(){return(m_colspan);}
  };
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGridConstraints::CGridConstraints(CWnd *control,int row,int column,int rowspan=1,int colspan=1)
  {
   m_control = control;
   m_row = MathMax(0,row);
   m_col = MathMax(0,column);
   m_rowspan = MathMax(1,rowspan);
   m_colspan = MathMax(1,colspan);
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
CGridConstraints::~CGridConstraints()
  {
  }

Dem Konstruktor der Klasse können wir entnehmen, dass die CConstraints-Klasse die Zeilen, Spalten, Zeilen Spanne und Spalten Spanne für jedes Control speichert Aber dieses ist nur möglich, wenn die Grid()-Methode aufgerufen wurde, was man in der Implementation der CGridTk Klasse sehen kann. Darüber hinaus speichert diese Klasse nur Informationen ab. Wie diese Information verwendet wird, ist in der CGridTk Klasse implementiert.


6.3.3. Die Standardposition

Wenn ein Control nicht über die Methode Grid() zu dem Gitter hinzugefügt worden ist, dann wird die standardmäßige Positionierung verwendet. Ein solches Control, welches mit der Methode Add() hinzugefügt worden ist, hat keine Einschränkungen (es werden keine CGridConstraints Objekte in der Grid-Klasseninstanz gespeichert). In diesem Fall können die aktualisierten Methoden in der CG Klasse mit diesen Control(s) nichts anfangen, sofern eine Positionierung oder eine Änderung der Größe gewünscht ist. Die Platzierung dieser Methode wäre gleichzusetzen mit der CGrid Klassenmethode. Es wäre zu Verwenden, wie ein eine Ausweich- oder Standardmethode der Positionierung. Das bedeutet, die Controls würden wie die Ziegel in einer Wand gestapelt, jedoch ausgehend von der linken oberen Ecke des Clientbereichs, wie in dem ersten Beispiel gezeigt wurde.


7. Beispiel #3: Schiebe Puzzle (Verbessert)

Um das Schiebepuzzle weiter zu optimieren, müssen wir noch einige Änderungen an dem zweiten Beispiel vornehmen:

  1. Erzeugung eines "New Game" Buttons, damit der Expert Advisor nicht jedes Mal neu gestartet werden muss.
  2. Die Erzeugung eines Controls in dem Dialogfenster, welches den Status des Spiels anzeigt, was das bisher notwendige Öffnen des Journal-Tabs im Terminal-Fenster unnötig macht.
  3. Implementierung einer unterschiedlichen Größe der neuen Controls.
  4. Zudem noch ein paar kosmetische Veränderungen, wie zum Beispiel die Einfärbung der Kacheln und das Anzeigen aller Kacheln in dem Gitter (optional).

Diese optimierte Version des Schiebe-Puzzles ist in dem folgenden Screenshot dargestellt:

Sliding Puzzle (Verbessert)

Figure 4. Sliding Puzzle (Verbessert)

Wie man in dem Screenshot sehen kann, haben wir neue Komponenten dem Dialogfenster hinzugefügt. Es gibt jetzt einen Button, welcher es uns erlaubt ein neues Spiel zu starten, sowie eine Textbox, welche uns den aktuellen Status des Spieles anzeigt. Jetzt wollen wir aber nicht, dass diese Buttons so in ihrer Größe verändert werden, dass sie einer Zellengröße entsprechen, so wie es bei den anderen 16 Buttons der Fall ist. Dieses kann zu Verwirrungen bei den Anwendern führen, weil sie die Beschreibung oder den Text dieser Controls nicht sehen können.

Um diesen Dialog zu konstruieren, müssen wir die Klasse des zweiten Beispiels erweitern, oder kopieren Sie die Klasse einfach und modifizieren sie sie anschließend. In diesem Fall wählen wir die kopiermethode anstelle die Klasse des 2. Beispiels zu erweitern.

In diesem neuen Dialog, wurden zwei zusätzliche Controls hinzugefügt. Wir müssen nun Funktionen deklarieren, welche die Erzeugung dieser Controls vornehmen, namentlich CreateButtonNew() und CreateLabel(). Zunächst müssen wir sie als Elemente der Klasse definieren:

protected:
   //protected member methods start

   virtual bool      CreateButtonNew(const long chart,const string name,const int subwin);
   virtual bool      CreateLabel(const long chart,const string name,const int subwin);

   //Weitere geschützte Methoden weiter unten..

Die aktuelle implementation der Funktionen wird hier gezeigt:

//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CSlidingPuzzleDialog::CreateButtonNew(const long chart,const string name,const int subwin)
  {
   if(!m_button_new.Create(chart,name+"buttonnew",m_subwin,0,0,101,101))
      return(false);
   m_button_new.Text("New");
   m_button_new.ColorBackground(clrYellow);
   if(!m_main.Grid(GetPointer(m_button_new),4,0,1,2))
      return(false);
   return(true);
  }
//+----------------------------------------------------------------
//|                                                                  |
//+----------------------------------------------------------------
bool CSlidingPuzzleDialog::CreateLabel(const long chart,const string name,const int subwin)
  {
   if(!m_label.Create(chart,name+"labelnew",m_subwin,0,0,102,102))
      return(false);
   m_label.Text("click new");
   m_label.ReadOnly(true);
   m_label.TextAlign(ALIGN_CENTER);
   if(!m_main.Grid(GetPointer(m_label),4,2,1,2))
      return(false);
   return(true);
  }

Es ist zudem noch notwendig einige Modifikationen an einige Funktionen vorzunehmen. Da nun neue controls dem Gitter hinzugefügt wurden, müssen die Funktionen Check(), HasNorth(), HasSouth(), HasWest(), and HasEast() Modifiziert werden. Dieses ist notwendig um sicherzustellen, dass die aktuelle Kacheln nicht Werte von anderen Kacheln verändert, die keine Zahlen darstellen. Zunächst werden wir den Kacheln, die eine Nummer darstellen, den prefix "Block" als Argument bei dem Aufruf von Create() mitgeben und dann mit Hilfe dieses Präfix ermitteln, ob das gewählte Control tatsächlich eine Nummern-Kachel darstellt. Der nachfolgende Code zeigt die aktualisierte Funktion, Check():

bool CSlidingPuzzleDialog::Check(void)
  {
   for(int i=0;i<m_main.ControlsTotal();i++)
     {
      CWnd *control=m_main.Control(i);
      if(StringFind(control.Name(),"block")>=0)
        {
         CButton *button=control;
         if(CheckPointer(button))
           {
            if(button.Text()!=IntegerToString(i))
              {
               m_label.Text("not solved");
               return(false);
              }
           }
        }
     }
   m_label.Text("solved");
   m_solved=true;
   return(true);
  }

Hier verwenden wir die StringFind Funktion, um sicherzustellen, dass das selektierte Control ein Button ist und auch eine Nummern-Kachel darstellt. Dieses ist notwendig, andernfalls würden wir Fehlermeldung wie zum Beispiel "incorrect casting of pointers" erhalten, wenn wir versuchen, dem Control eine Instanz des Typs CButton zuweisen wollen, was in den nachfolgenden Code-Zeilen passiert. In diesem Code sehen wir auch, dass wir anstelle der Print-Funktion das CEdit-Control für die Darstellung des Spielestatus verwenden.


8. Container Verschachtelung

Es ist auch möglich das Gitter innerhalb eines weiteren Containers wie zum Beispiel einem Box Container oder einem größeren Gitter zu platzieren. Wenn es innerhalb eines CBox Containers platziert wird, dann folgt das Gitter dem Layout und den Anordnungen seines übergeordneten Containers. Aber wie bei jedem anderen Control oder Container, welches innerhalb einer Instanz von CBox platziert wird, sollte bei dem Gitter auch eine bevorzugte Höhe und Breite festgelegt werden. Auf der anderen Seite, wenn es in einem anderen Gitter platziert wird, dann wird die Größe des Gitters automatisch berechnet.


9. Vor- und Nachteile

Vorteile:

  • Es kann die Anzahl an Containern für die Dialogfenster stark reduzieren, speziell wenn identische Controls vorhanden sind.
  • Es ist einfacher zu managen und zu warten als die CBox.
  • Es ist einfacher zu verwenden als die absolute Positionierung.

Nachteile:

  • Es ist weniger präzise als das absolute positionieren.
  • Die Ausrichtung kann ein wenig von der rechten und unteren Seite abweichen, falls die Größe der Controls nicht mit den Proportionen der Client-Area übereinstimmt. Dieses kann auftreten, wenn die Größe der Client-Area minus dem Raum für jede Zelle keine ganzzahliges Ergebnis liefert, wenn sie durch die Anzahl der Zellen oder spalten geteilt wird. Die Ursache dafür ist, dass ein Pixel in sich nicht weiter geteilt werden kann und sich somit Fehler aufsummieren können. Dieses kann jedoch durch eine Änderung der Größe des Haupt-Dialogfeldes gelöst werden.


10. Schlussfolgerung

In diesem Artikel haben wir die Möglichkeiten der Verwendung von einem Gitter-Layout bei der Konstruktion und dem Design von grafischen Panels betrachtet. Diese zusaätzliche Layout-Klasse bietet ein weiteres Tool für die einfache Konstruktion von GUI Controls in MetaTrader. Wir haben Fälle betrachtet, bei denen es vorteilhaft ist, diese Klasse anstelle der Standard Box Layout Klasse zu verwenden.

Wir haben zwei Klassen für die Erstellung von Gittern präsentiert: Die CGrid und die CGridTk class. Die CGrid Klasse ist ein externes Control, welches als ein Container für notwendige Controls in einem GUI Panel agiert. Es fügt wichtige Controls als Nachkommen (Childs) in Gittern ein und organisiert sie dort neu. Die CGridTk Klasse ist eine Erweiterung der CGrid Klasse, und bietet weitere Eigenschaften für die benutzerdefinierte Positionierung und Größenveränderung von Controls. Diese Klassen können als grundlegende Bausteine für die einfachere Erstellung von grafischen Steuerelementen in Metatrader dienen.

Übersetzt aus dem Englischen von MetaQuotes Software Corp.
Originalartikel: https://www.mql5.com/en/articles/1998

Beigefügte Dateien |
Grid.zip (12.35 KB)
Verwendung von Layouts und Containern für GUI Controls: Die CBox Klasse Verwendung von Layouts und Containern für GUI Controls: Die CBox Klasse

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 CBox Klasse ist ein externes Control, welches als ein Container für besondere Controls in einem GUI-Panel agiert. Sie vereinfacht das Designen von grafischen Panels und in einigen Fällen reduziert sie auch den Programmieraufwand.

MQL5 für Anfänger: Antivandalismusschutz der grafischen Objekten MQL5 für Anfänger: Antivandalismusschutz der grafischen Objekten

Was soll Ihr Programm machen, wenn die grafischen Bedienfelder von jemandem geändert oder gelöscht wurden? In diesem Artikel zeigen wir Ihnen, wie Sie nach der Löschung der Anwendung auf dem Chart keine "herrenlose" Objekte mehr haben können, und wie sie die Kontrolle über sie halten können, falls sie umbenennt werden oder wenn vom Programm erstellte Objekte gelöscht werden.

Universeller Expert Advisor: Traden mit Gruppen von Strategien und deren Verwaltung (Part 4) Universeller Expert Advisor: Traden mit Gruppen von Strategien und deren Verwaltung (Part 4)

In dem letzten Abschnitt der Serie über die CStrategy Trading Engine, werden wir die parallele Ausführung von verschiedenen Handels-Algorithmen betrachten, das Laden von Strategien über ein XML-File kennenlernen und wir werden ein einfaches Bedienfeld erstellen, mit welchem es möglich ist, die Strategie(n) auszuwählen und auch die Handelsmodi.

Grundlagen der Programmierung in MQL5: Listen Grundlagen der Programmierung in MQL5: Listen

Die neue Version der Programmiersprache für die Entwicklung von Handelsstrategien, MQL [MQL5], liefert im Vergleich zur Vorgängerversion [MQL4] leistungsstärkere und effektivere Features. Der Vorteil besteht im Wesentlichen aus den Merkmalen der objektorientierten Programmierung. In diesem Beitrag wird die Möglichkeit betrachtet, komplexe benutzerdefinierte Datentypen wie Knoten und Listen zu verwenden. Außerdem liefert der Beitrag ein Anwendungsbeispiel für die Verwendung von Listen in der praktischen Programmierung in MQL5.