GUI: Tipps und Tricks zur Erstellung Ihrer eigenen Grafikbibliothek in MQL
Einführung
Die Entwicklung einer GUI-Bibliothek ist eines der größten unspezifischen Projekte, die man sich im Zusammenhang mit MetaTrader 5 vorstellen kann, abgesehen von sehr fortgeschrittenen Dingen wie KI, (guten) neuronalen Netzen und... dem flüssigen Umgang mit einer GUI-Bibliothek, die man nicht selbst entwickelt hat.
Der letzte Punkt war nur ein halber Scherz, natürlich ist es einfacher zu lernen, wie man eine bereits erstellte Bibliothek nutzt (auch wenn es hier wirklich große GUI-Bibliotheken gibt)! Aber warum sollte ich mir die Mühe machen, eine Bibliothek von Grund auf neu zu erstellen, wenn ich lernen kann, sie zu nutzen, die bereits besser ist als etwas, das ich selbst erstellen könnte?
Nun, dafür gibt es einige gute Gründe. Vielleicht halten Sie sie für Ihr spezielles Projekt für zu langsam, vielleicht müssen Sie sie erweitern, wenn Sie etwas ganz Bestimmtes brauchen, das nicht in der Bibliothek enthalten ist (manche Bibliotheken sind schwieriger zu erweitern als andere), oder eine Funktionalität, die mit dieser Implementierung nicht möglich ist, sie könnte einen Fehler haben (mit Ausnahme derer, die durch Missbrauch der Bibliothek entstehen)... oder Sie wollen einfach nur etwas darüber lernen. Die meisten dieser Probleme können von den Autoren einer bestimmten Bibliothek gelöst werden, aber Sie würden sich darauf verlassen, dass sie es bemerken oder bereit sind, es zu tun (im Falle der Erweiterung der Funktionalität).
In diesem Artikel geht es nicht darum, Ihnen beizubringen, wie man eine Schnittstelle erstellt, oder die Schritte zur Entwicklung einer voll funktionsfähigen Bibliothek aufzuzeigen. Stattdessen werden wir Beispiele dafür geben, wie einige spezifische Teile von GUI-Bibliotheken erstellt werden können, sodass sie als Ausgangspunkt für die Erstellung einer solchen dienen können, um ein bestimmtes Problem zu lösen, das Sie vielleicht gefunden haben, oder um ein erstes Verständnis dafür zu bekommen, was überhaupt in einer riesigen Codebasis für eine bereits vollständige GUI-Bibliothek passiert.
Zum Schluss... machen wir uns an die Erstellung einer „GUI-Bibliothek“.
Programmstruktur und Objekthierarchie
Bevor wir mit der Entwicklung einer GUI-Bibliothek beginnen, sollten wir uns diese Frage stellen: Was ist eine GUI-Bibliothek? Kurz gesagt, es handelt sich um eine glorifizierte Hierarchie von Objekten, die andere (Chart-) Objekte verfolgen und ihre Eigenschaften ändern, um verschiedene Effekte zu erzeugen und Ereignisse auszulösen, z. B. durch Bewegen, Klicken oder einen Farbwechsel. Wie diese Hierarchie organisiert ist, kann von Implementierung zu Implementierung variieren, die gängigste (und von mir bevorzugte) ist jedoch eine Baumstruktur von Elementen, wobei ein Element weitere Unterelemente haben kann.
Um dies zu erreichen, beginnen wir mit einer grundlegenden Implementierung eines Elements:
class CElement { private: //Variable to generate names static int m_element_count; void AddChild(CElement* child); protected: //Chart object name string m_name; //Element relations CElement* m_parent; CElement* m_children[]; int m_child_count; //Position and size int m_x; int m_y; int m_size_x; int m_size_y; public: CElement(); ~CElement(); void SetPosition(int x, int y); void SetSize(int x, int y); void SetParent(CElement* parent); int GetGlobalX(); int GetGlobalY(); void CreateChildren(); virtual void Create(){} };
Die Basiselementklasse enthält vorerst nur Informationen über Position, Größe und Beziehungen zu anderen Elementen.
Die Positionsvariablen m_x und m_y sind lokale Positionen im Kontext des übergeordneten Objekts. Dies macht eine globale Positionsfunktion erforderlich, um die tatsächliche Position des Objekts auf dem Bildschirm zu bestimmen. Unten sehen Sie, wie wir die globale Position rekursiv ermitteln können (in diesem Fall für X):
int CElement::GetGlobalX(void) { if (CheckPointer(m_parent)==POINTER_INVALID) return m_x; return m_x + m_parent.GetGlobalX(); }
Im Konstruktor müssen wir für jedes Objekt einen eindeutigen Namen festlegen. Hierfür können wir eine statische Variable verwenden. Aus Gründen, die wir in diesem Artikel nicht erörtern werden, ziehe ich es vor, diese Variable in der Programmklasse zu haben, die wir später sehen werden, aber der Einfachheit halber werden wir sie innerhalb der Elementklasse haben.
Es ist sehr wichtig, dass Sie daran denken, die Kinderelemente im Destruktor zu löschen, um Speicherlecks zu vermeiden!
int CElement::m_element_count = 0; //+------------------------------------------------------------------+ //| Base Element class constructor | //+------------------------------------------------------------------+ CElement::CElement(void) : m_child_count(0), m_x(0), m_y(0), m_size_x(100), m_size_y(100) { m_name = "element_"+IntegerToString(m_element_count++); } //+------------------------------------------------------------------+ //| Base Element class destructor (delete child objects) | //+------------------------------------------------------------------+ CElement::~CElement(void) { for (int i=0; i<m_child_count; i++) delete m_children[i]; }
Schließlich definieren wir die Beziehungsfunktionen AddChild und SetParent, da wir beide Referenzen benötigen, um zwischen Elementen zu kommunizieren: Um beispielsweise die globale Position zu erhalten, muss ein Kind die Position des Elternteils kennen, aber wenn es seine Position ändert, muss das Elternteil die Kinder darüber informieren (diesen letzten Teil werden wir später implementieren). Um Redundanzen zu vermeiden, haben wir AddChild als privat gekennzeichnet.
In den Erstellungsfunktionen werden wir die Chart-Objekte selbst erstellen (und andere Eigenschaften ändern). Es ist wichtig, dass wir sicherstellen, dass die Kinder nach dem Elternteil erstellt werden, deshalb wird eine separate Funktion für diesen Zweck verwendet (da create überschrieben werden kann, und das könnte die Reihenfolge der Ausführung ändern). In der Basiselementklasse ist „Create“ leer.
//+------------------------------------------------------------------+ //| Set parent object (in element hierarchy) | //+------------------------------------------------------------------+ void CElement::SetParent(CElement *parent) { m_parent = parent; parent.AddChild(GetPointer(this)); } //+------------------------------------------------------------------+ //| Add child object reference (function not exposed) | //+------------------------------------------------------------------+ void CElement::AddChild(CElement *child) { if (CheckPointer(child)==POINTER_INVALID) return; ArrayResize(m_children, m_child_count+1); m_children[m_child_count] = child; m_child_count++; }
//+------------------------------------------------------------------+ //| First creation of elements (children after) | //+------------------------------------------------------------------+ void CElement::CreateChildren(void) { for (int i=0; i<m_child_count; i++) { m_children[i].Create(); m_children[i].CreateChildren(); } }
Wir werden nun eine Programmklasse erstellen. Im Moment ist es nur ein Platzhalter, um indirekt mit einem leeren Element, das als Halter verwendet wird, zu interagieren, aber in der Zukunft wird es andere zentralisierte Operationen durchführen, die alle Elemente betreffen (und die Sie nicht mehrfach ausführen lassen wollen). Durch die Verwendung eines leeren Elementhalters zum Speichern anderer Elemente wird vermieden, dass wir eine Funktion neu erstellen, die eine rekursive Iteration der Kinder erfordert. Im Moment brauchen wir keinen Konstruktor/Destruktor für diese Klasse, da wir den Halter nicht als Zeiger speichern.
class CProgram { protected: CElement m_element_holder; public: void CreateGUI() { m_element_holder.CreateChildren(); } void AddMainElement(CElement* element) { element.SetParent(GetPointer(m_element_holder)); } };
Bevor wir unseren ersten Test machen, müssen wir noch die Klasse Element erweitern, da sie jetzt leer ist. Auf diese Weise können wir verschiedene Arten von Objekten unterschiedlich verwalten. Zunächst erstellen wir ein Canvas-Element mit CCanvas (das eigentlich ein Bitmap-Label ist). Canvas-Objekte sind die vielseitigsten Objekte für die Erstellung nutzerdefinierter GUIs, und wir könnten fast alles komplett aus Canvas-Objekten erstellen:
#include <Canvas/Canvas.mqh> //+------------------------------------------------------------------+ //| Generic Bitmap label element (Canvas) | //+------------------------------------------------------------------+ class CCanvasElement : public CElement { protected: CCanvas m_canvas; public: ~CCanvasElement(); virtual void Create(); }; //+------------------------------------------------------------------+ //| Canvas Element destructor (destroy canvas) | //+------------------------------------------------------------------+ CCanvasElement::~CCanvasElement(void) { m_canvas.Destroy(); } //+------------------------------------------------------------------+ //| Create bitmap label (override) | //+------------------------------------------------------------------+ void CCanvasElement::Create() { CElement::Create(); m_canvas.CreateBitmapLabel(0, 0, m_name, GetGlobalX(), GetGlobalY(), m_size_x, m_size_y, COLOR_FORMAT_ARGB_NORMALIZE); }
Wir werden diese 2 Zeilen am Ende von Create hinzufügen, um die Leinwand mit einer zufälligen Farbe zu füllen. Ein korrekterer Weg wäre es, die Canvas-Klasse zu erweitern, um diese spezielle Art von Objekt zu erstellen, aber es ist nicht nötig, so viele Komplikationen hinzuzufügen.
m_canvas.Erase(ARGB(255, MathRand()%256, MathRand()%256, MathRand()%256)); m_canvas.Update(false);
Wir werden auch eine Klasse für Bearbeitungsobjekte erstellen. Aber warum gerade dieses? Warum machen wir nicht unsere eigenen Bearbeitungen, indem wir direkt auf der Leinwand zeichnen und Tastaturereignisse verfolgen, um darin zu schreiben? Nun, es gibt eine Sache, die nicht mit Canvas gemacht werden kann, und das ist das Kopieren und Einfügen von Text (zumindest außerhalb der Anwendung selbst, ohne DLLs). Wenn Sie diese Funktionalität für Ihre Bibliothek nicht benötigen, können Sie den Canvas direkt zur Klasse Element hinzufügen und ihn für jeden Objekttyp verwenden. Wie Sie feststellen werden, werden einige Dinge in Bezug auf die Leinwand anders gemacht...
class CEditElement : public CElement { public: ~CEditElement(); virtual void Create(); string GetEditText() { return ObjectGetString(0, m_name, OBJPROP_TEXT); } void SetEditText(string text) { ObjectSetString(0, m_name, OBJPROP_TEXT, text); } }; //+------------------------------------------------------------------+ //| Edit element destructor (remove object from chart) | //+------------------------------------------------------------------+ CEditElement::~CEditElement(void) { ObjectDelete(0, m_name); } //+------------------------------------------------------------------+ //| Create edit element (override) and set size/position | //+------------------------------------------------------------------+ void CEditElement::Create() { CElement::Create(); ObjectCreate(0, m_name, OBJ_EDIT, 0, 0, 0); ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX()); ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY()); ObjectSetInteger(0, m_name, OBJPROP_XSIZE, m_size_x); ObjectSetInteger(0, m_name, OBJPROP_YSIZE, m_size_y); }
In diesem Fall müssen wir die Eigenschaften Position und Größe explizit festlegen (in Canvas werden sie innerhalb von CreateBitmapLabel vorgenommen).
Mit all diesen Änderungen können wir endlich einen ersten Test durchführen:
#include "Basis.mqh" #include "CanvasElement.mqh" #include "EditElement.mqh" input int squares = 5; //Amount of squares input bool add_edits = true; //Add edits to half of the squares CProgram program; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { MathSrand((uint)TimeLocal()); //100 is element size by default int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100; int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100; for (int i=0; i<squares; i++) { CCanvasElement* drawing = new CCanvasElement(); drawing.SetPosition(MathRand()%max_x, MathRand()%max_y); program.AddMainElement(drawing); if (add_edits && i<=squares/2) { CEditElement* edit = new CEditElement(); edit.SetParent(drawing); edit.SetPosition(10, 10); edit.SetSize(80, 20); } } program.CreateGUI(); ChartRedraw(0); return(INIT_SUCCEEDED); }
Dieses Programm erzeugt ein paar Quadrate und löscht sie, wenn es aus dem Chart entfernt wird. Beachten Sie, dass jedes Bearbeitungselement relativ zu seinem übergeordneten Element an der gleichen Stelle positioniert ist.
Im Moment machen diese Quadrate nicht viel, sie sind einfach „nur da“. In den folgenden Abschnitten werden wir darüber sprechen, wie wir ihnen einige Verhaltensweisen hinzufügen können.
Maus-Eingaben
Wenn Sie schon einmal mit Chart-Ereignissen gearbeitet haben, wissen Sie, dass die Verwendung von Klickereignissen nicht ausreicht, um ein komplexes Verhalten zu erzeugen. Der Trick, um bessere Schnittstellen zu schaffen, ist die Verwendung der Ereignisse der Mausbewegungen. Diese Ereignisse müssen beim Start des Expert Advisors aktiviert werden, also aktivieren wir sie, bevor wir die grafische Benutzeroberfläche erstellen:
void CProgram::CreateGUI(void) { ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); m_element_holder.CreateChildren(); }
Sie liefern die Mausposition (x,y) und den Zustand der Maustasten, Strg und Shift. Jedes Mal, wenn sich mindestens einer der Zustände oder die Position ändert, wird ein Ereignis ausgelöst.
Zunächst werden wir die Phasen definieren, die eine Schaltfläche durchlaufen kann: inactive, wenn sie nicht verwendet wird, und active, wenn sie angeklickt wird. Wir fügen auch down und up hinzu, die den ersten aktiven bzw. inaktiven Zustand darstellen (die Änderungen des Klickzustands). Da wir jede Phase nur mit Mausereignissen erkennen können, sollten wir nicht einmal Klickereignisse verwenden müssen.
enum EInputState { INPUT_STATE_INACTIVE=0, INPUT_STATE_UP=1, INPUT_STATE_DOWN=2, INPUT_STATE_ACTIVE=3 };
Um die Dinge zu vereinfachen, werden wir die Verarbeitung von Mausereignissen zentralisieren, anstatt sie innerhalb jedes Objekts durchzuführen. Auf diese Weise können wir leichter auf Mausdaten zugreifen und sie nachverfolgen, was es erlauben würde, sie auch für andere Arten von Ereignissen zu verwenden und wiederholte Berechnungen zu vermeiden. Wir nennen diese Klasse „CInputs“, da sie neben den Mauseingaben auch Strg- und Umschalttasten enthält.
//+------------------------------------------------------------------+ //| Mouse input processing class | //+------------------------------------------------------------------+ class CInputs { private: EInputState m_left_mouse_state; EInputState m_ctrl_state; EInputState m_shift_state; int m_pos_x; int m_pos_y; void UpdateState(EInputState &state, bool current); public: CInputs(); void OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); EInputState GetLeftMouseState() { return m_left_mouse_state; } EInputState GetCtrlState() { return m_ctrl_state; } EInputState GetShiftState() { return m_shift_state; } int X() { return m_pos_x; } int Y() { return m_pos_y; } }; //+------------------------------------------------------------------+ //| Inputs constructor (initialize variables) | //+------------------------------------------------------------------+ CInputs::CInputs(void) : m_left_mouse_state(INPUT_STATE_INACTIVE), m_ctrl_state(INPUT_STATE_INACTIVE), m_shift_state(INPUT_STATE_INACTIVE), m_pos_x(0), m_pos_y(0) { } //+------------------------------------------------------------------+ //| Mouse input event processing | //+------------------------------------------------------------------+ void CInputs::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id!=CHARTEVENT_MOUSE_MOVE) return; m_pos_x = (int)lparam; m_pos_y = (int)dparam; uint state = uint(sparam); UpdateState(m_left_mouse_state, ((state & 1) == 1)); UpdateState(m_ctrl_state, ((state & 8) == 8)); UpdateState(m_shift_state, ((state & 4) == 4)); } //+------------------------------------------------------------------+ //| Update state of a button (up, down, active, inactive) | //+------------------------------------------------------------------+ void CInputs::UpdateState(EInputState &state, bool current) { if (current) state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_ACTIVE : INPUT_STATE_DOWN; else state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_UP : INPUT_STATE_INACTIVE; } //+------------------------------------------------------------------+
In UpdateState prüfen wir den aktuellen Zustand (boolesch) und den letzten Zustand, um festzustellen, ob ein Eingang aktiv/inaktiv ist und ob es sich um das erste Ereignis nach einer Zustandsänderung (auf/ab) handelt. Wir erhalten Strg- und Shift-Informationen „umsonst“ in Sparam, und auch mittlere, rechte und 2 weitere zusätzliche Maustasten. Wir haben sie nicht in den Code aufgenommen, aber es ist einfach, die notwendigen Änderungen vorzunehmen, um sie hinzuzufügen, wenn Sie sie verwenden möchten.
Wir fügen dem Programm eine Instanz für Mauseingänge hinzu und machen sie für jedes Objekt mit einem Mauszeiger zugänglich. Bei jedem Ereignis wird die Instanz der Eingänge aktualisiert. Die Art des Ereignisses wird der Klasse inputs gefiltert (nur Mausbewegungsereignisse werden verwendet).
class CProgram { //... protected: CInputs m_inputs; //... public: void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam); //... }; //+------------------------------------------------------------------+ //| Program constructor (pass inputs reference to holder) | //+------------------------------------------------------------------+ CProgram::CProgram(void) { m_element_holder.SetInputs(GetPointer(m_inputs)); } //+------------------------------------------------------------------+ //| Process chart event | //+------------------------------------------------------------------+ void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { m_inputs.OnEvent(id, lparam, dparam, sparam); m_element_holder.OnChartEvent(id, lparam, dparam, sparam); }
Im nächsten Abschnitt werden wir genauer erklären, wie OnChartEvent verwendet wird.
Wenn Eingaben für das globale Element gesetzt werden, werden sie rekursiv an seine untergeordneten Elemente weitergegeben. Wir dürfen jedoch nicht vergessen, dass sie auch weitergegeben werden müssen, wenn später Kinder hinzugefügt werden (nach dem ersten SetInputs-Aufruf):
//+------------------------------------------------------------------+ //| Set mouse inputs reference | //+------------------------------------------------------------------+ void CElement::SetInputs(CInputs* inputs) { m_inputs = inputs; for (int i = 0; i < m_child_count; i++) m_children[i].SetInputs(inputs); } //+------------------------------------------------------------------+ //| Add child object reference (function not exposed) | //+------------------------------------------------------------------+ void CElement::AddChild(CElement *child) { if (CheckPointer(child) == POINTER_INVALID) return; ArrayResize(m_children, m_child_count + 1); m_children[m_child_count] = child; m_child_count++; child.SetInputs(m_inputs); }
Im nächsten Abschnitt werden wir die Ereignisbehandlungsfunktionen erstellen und jedem Objekt einige Mausereignisse hinzufügen.
Die Ereignisbehandlung
Bevor wir uns mit den Ereignissen selbst befassen, müssen wir uns einer Sache bewusst werden, die wir brauchen, wenn wir eine flüssige Bewegung in der Benutzeroberfläche haben wollen, und das ist das Neuzeichnen der Charts. Wenn sich eine Objekteigenschaft ändert, z. B. seine Position oder wenn es neu gezeichnet wird, müssen wir das Chart neu zeichnen, damit diese Änderungen sofort sichtbar werden. Allerdings kann ein zu häufiger Aufruf von ChartRedraw zu einem Flackern der GUI führen, weshalb ich es vorziehe, seine Ausführung zu zentralisieren:
class CProgram { private: bool m_needs_redraw; //... public: CProgram(); void OnTimer(); //... void RequestRedraw() { m_needs_redraw = true; } }; void CProgram::OnTimer(void) { if (m_needs_redraw) { ChartRedraw(0); m_needs_redraw = false; } }
Wie zu erwarten, muss OnTimer in der Ereignisbehandlungsfunktion mit demselben Namen aufgerufen werden, und jedes Element muss einen Verweis auf das Programm haben, um RequestRedraw aufrufen zu können. Diese Funktion setzt ein Flag, das, wenn sie aktiviert wird, alle Elemente beim nächsten Timer-Aufruf neu zeichnet. Außerdem müssen wir zuerst den Timer einstellen:
#define TIMER_STEP_MSC (16) void CProgram::CreateGUI(void) { ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); m_element_holder.CreateChildren(); ::EventSetMillisecondTimer(TIMER_STEP_MSC); }
16 Millisekunden ist die Grenze (oder fast die Grenze), in der Timer zuverlässig ausgeführt werden können. Schwere Programme können jedoch die Ausführung des Timers blockieren.
Im Folgenden wird erläutert, wie die Chartereignisse in den einzelnen Elementen implementiert werden:
//+------------------------------------------------------------------+ //| Send event recursively and respond to it (for this element) | //+------------------------------------------------------------------+ void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { for (int i = m_child_count - 1; i >= 0; i--) m_children[i].OnChartEvent(id, lparam, dparam, sparam); OnEvent(id, lparam, dparam, sparam); }
In dieser Bibliothek haben wir uns dafür entschieden, Ereignisse rekursiv von den Eltern an die Kinder weiterzugeben. Dies ist eine Design-Entscheidung (es könnte auch anders gemacht werden, z. B. durch die Verwendung von Listenern oder den sequentiellen Aufruf aller Objekte), aber wie wir später sehen werden, hat es einige Vorteile. OnEvent ist eine überschreibbare geschützte Funktion. Sie ist von OnChartEvent (das öffentlich ist) getrennt, sodass der Nutzer die Übergabe von Ereignissen an untergeordnete Objekte nicht überschreiben kann und wählen kann, wie die OnEvent-Klasse der übergeordneten Klasse ausgeführt werden soll (vor, nach oder ohne Ausführung).
Als Beispiel für die Behandlung von Ereignissen werden wir eine Ziehfunktion für die Quadrate implementieren. Klick- und Hover-Ereignisse könnten auch trivialer implementiert werden, aber für dieses Beispiel benötigen wir sie nicht. Im Moment werden Ereignisse nur durch Objekte weitergegeben, aber sie tun nichts. Es gibt jedoch ein Problem: Wenn Sie versuchen, ein Objekt zu ziehen, bewegen sich die Charts dahinter, als ob es nicht da wäre. Wir sollten nicht erwarten, dass sie sich vorerst bewegt, aber die Charts sollten sich auch nicht bewegen!
Um dieses Problem zu vermeiden, überprüfen wir zunächst, ob die Maus über einem Objekt schwebt (hover). Ist dies der Fall, so wird das Blättern im Chart blockiert. Aus Leistungsgründen werden wir nur die Objekte prüfen, die direkte Unterobjekte des globalen Halters sind (das Programm würde immer funktionieren , wenn andere Unterobjekte innerhalb der Grenzen des übergeordneten Objekts bleiben).
//+------------------------------------------------------------------+ //| Check if mouse is hovering any child element of this object | //+------------------------------------------------------------------+ bool CElement::CheckHovers(void) { for (int i = 0; i < m_child_count; i++) { if (m_children[i].IsMouseHovering()) return true; } return false; } //+------------------------------------------------------------------+ //| Process chart event | //+------------------------------------------------------------------+ void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { m_inputs.OnEvent(id, lparam, dparam, sparam); if (id == CHARTEVENT_MOUSE_MOVE) EnableControls(!m_element_holder.CheckHovers()); m_element_holder.OnChartEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+ //| Enable/Disable chart scroll responses | //+------------------------------------------------------------------+ void CProgram::EnableControls(bool enable) { //Allow or disallow displacing chart ::ChartSetInteger(0, CHART_MOUSE_SCROLL, enable); }
Jetzt vermeiden sie Verschiebungen... aber nur, wenn sich die Maus über dem Objekt befindet. Wenn wir sie außerhalb ihrer Grenzen ziehen, bewegen sich die Charts trotzdem.
Wir benötigen zusätzlich 2 Prüfungen und eine private bool-Variable zu CElement (m_dragging):
//+------------------------------------------------------------------+ //| Send event recursively and respond to it (for this element) | //+------------------------------------------------------------------+ void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { for (int i = m_child_count - 1; i >= 0; i--) m_children[i].OnChartEvent(id, lparam, dparam, sparam); //Check dragging start if (id == CHARTEVENT_MOUSE_MOVE) { if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) m_dragging = true; else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP) m_dragging = false; } OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+ //| Check if mouse hovers/drags any child element of this object | //+------------------------------------------------------------------+ bool CElement::CheckHovers(void) { for (int i = 0; i < m_child_count; i++) { if (m_children[i].IsMouseHovering() || m_children[i].IsMouseDragging()) return true; } return false; }
Jetzt funktioniert alles beim Ziehen von Objekten, aber es gibt eine kleine Korrektur, die fehlt... beim Ziehen von Charts, wenn die Maus über ein Objekt geht, würde es aufhören, sie zu ziehen. Wir müssen das Ziehen filtern, die außerhalb eines Objekts beginnen.
Glücklicherweise ist das nicht sehr schwer zu beheben:
//+------------------------------------------------------------------+ //| Check if mouse hovers/drags any child element of this object | //+------------------------------------------------------------------+ bool CElement::CheckHovers(void) { EInputState state = m_inputs.GetLeftMouseState(); bool state_check = state != INPUT_STATE_ACTIVE; //Filter drags that start in chart for (int i = 0; i < m_child_count; i++) { if ((m_children[i].IsMouseHovering() && state_check) || m_children[i].IsMouseDragging()) return true; } return false; }
Wenn nun die Maus über einem Objekt schwebt, aber die Maus aktiv ist, kann sie das Chart ziehen, sodass sie ignoriert wird (und wenn sie ein Objekt zieht, wäre dieses Objekt dasjenige, das true zurückgibt). Die Überprüfung des Schwebens der Maus ist immer noch erforderlich, da sonst die Ereignisse einen Frame zu spät deaktiviert würden.
Jetzt können wir der Klasse „square“ die Ziehfunktion hinzufügen. Anstatt es jedoch zu CCanvasElement hinzuzufügen, werden wir die Klasse durch Vererbung erweitern. Wir werden auch die Zeichnungslinien aus dem letzten Beispiel herausnehmen, sodass sie standardmäßig leer sind. Da wir bereits Prüfungen für das Ziehen hinzugefügt haben, können wir sie verwenden, um das Ereignis zu behandeln und Objekte zu bewegen. Um die Position eines Objekts im Chart zu ändern, müssen wir seine Variablen ändern, dann seine Positionseigenschaften aktualisieren, die Position seiner Kinder aktualisieren und das Chart neu zeichnen.
class CElement { //... public: void UpdatePosition(); }; //+------------------------------------------------------------------+ //| Update element (and children) position properties | //+------------------------------------------------------------------+ void CElement::UpdatePosition(void) { ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX()); ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY()); for (int i = 0; i < m_child_count; i++) m_children[i].UpdatePosition(); }
class CCanvasElement : public CElement { protected: CCanvas m_canvas; virtual void DrawCanvas() {} //... }; //+------------------------------------------------------------------+ //| Create bitmap label (override) | //+------------------------------------------------------------------+ void CCanvasElement::Create() { //... DrawCanvas(); }
//+------------------------------------------------------------------+ //| Canvas class which responds to mouse drag events | //+------------------------------------------------------------------+ class CDragElement : public CCanvasElement { private: int m_rel_mouse_x, m_rel_mouse_y; protected: virtual void DrawCanvas(); protected: virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); }; //+------------------------------------------------------------------+ //| Check mouse drag events | //+------------------------------------------------------------------+ bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id != CHARTEVENT_MOUSE_MOVE) return false; if (!IsMouseDragging()) return false; if (m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) //First click { m_rel_mouse_x = m_inputs.X() - m_x; m_rel_mouse_y = m_inputs.Y() - m_y; return true; } //Move object m_x = m_inputs.X() - m_rel_mouse_x; m_y = m_inputs.Y() - m_rel_mouse_y; UpdatePosition(); m_program.RequestRedraw(); return true; } //+------------------------------------------------------------------+ //| Custom canvas draw function (fill with random color) | //+------------------------------------------------------------------+ void CDragElement::DrawCanvas(void) { m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256)); m_canvas.Update(false); }
Beachten Sie, dass wir die Position des Objekts auf seine globale Position setzen müssen: Chartobjekte wissen nichts über die Hierarchie in ihnen. Wenn wir jetzt versuchen, ein Objekt zu verschieben, wird es funktionieren und auch die Position seiner Kinder aktualisieren, aber Sie können Objekte verschieben, die sich hinter dem Objekt befinden, auf das wir klicken:
Da jedes Ereignis rekursiv an alle Objekte gesendet wird, erhalten sie es auch dann, wenn sie sich hinter einem anderen Objekt befinden. Wir müssen einen Weg finden, Ereignisse zu filtern, wenn ein Objekt sie zuerst erhält, mit anderen Worten, wir müssen sie verdecken.
Wir werden eine boolesche Variable erstellen, um zu verfolgen, ob ein Objekt verdeckt ist oder nicht:
class CElement { private: //... bool m_occluded; //... public: //... void SetOccluded(bool occluded) { m_occluded = occluded; } bool IsOccluded() { return m_occluded; } //... };
Dann können wir OnChartEvent als eine Möglichkeit der „Kommunikation“ zwischen Objekten verwenden. Dazu geben wir ein boolsche Variable zurück, die true ist, wenn das Objekt ein Ereignis empfangen hat. Wenn das Objekt gezogen wurde (auch wenn es nicht darauf reagiert) oder wenn das Objekt verdeckt ist (z. B. durch ein untergeordnetes Objekt), würde es auch true zurückgeben, da dies auch Ereignisse für darunter liegende Objekte blockieren würde.
bool CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { for (int i = m_child_count - 1; i >= 0; i--) { m_children[i].SetOccluded(IsOccluded()); if (m_children[i].OnChartEvent(id, lparam, dparam, sparam)) SetOccluded(true); } //Check dragging start if (id == CHARTEVENT_MOUSE_MOVE && !IsOccluded()) { if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) m_dragging = true; else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP) m_dragging = false; } return OnEvent(id, lparam, dparam, sparam) || IsMouseDragging() || IsOccluded(); }
OnEvent für ein Objekt wird nach den Ereignissen der untergeordneten Objekte ausgeführt, um Verdeckungen zu berücksichtigen. Zuletzt müssen wir die Ereignisfilterung zu unserem nutzerdefinierten, ‚ziehbaren‘ Objekt hinzufügen:
bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (IsOccluded()) return false; if (id != CHARTEVENT_MOUSE_MOVE) return false; //... }
Beachten Sie, dass wir die Ereignisse beim Senden nicht filtern. Das liegt daran, dass einige Elemente auf Ereignisse reagieren können, auch wenn sie verdeckt sind (z. B. einige Ereignisse, die auf Hover oder nur auf die Mausposition reagieren).
Mit all diesen Elementen haben wir das gewünschte Verhalten in diesem zweiten Beispiel erreicht: Jedes Objekt kann gezogen werden, während es gleichzeitig von anderen Objekten darüber blockiert wird:
Es sollte jedoch beachtet werden, dass diese Struktur zwar für diesen Fall funktioniert, andere Schnittstellen mit höherer Komplexität jedoch möglicherweise einige Verfeinerungen erfordern. Wir lassen sie in diesem Artikel außen vor, da sich die Erhöhung der Komplexität des Codes für solche speziellen Fälle nicht lohnt. So könnten beispielsweise Verdeckungen durch untergeordnete Objekte und geschwisterliche Objekte (auf derselben Hierarchieebene, aber in der Reihenfolge davor) unterschieden werden; oder es könnte sinnvoll sein, zu verfolgen, welches Objekt ein Ereignis erhalten hat, und es im nächsten Bild zuerst zu überprüfen (um unerwartete Verdeckungen zu vermeiden).
Einblenden und Ausblenden von Objekten
Eine weitere wichtige Funktion, die in jeder Grafikbibliothek benötigt wird, ist die Möglichkeit, Objekte ein- und auszublenden: Dies wird zum Beispiel beim Öffnen und Schließen von Fenstern, beim Ändern des Inhalts, wie z. B. bei Navigationsregisterkarten, oder beim Entfernen von Schaltflächen, die unter bestimmten Bedingungen nicht verfügbar sind, verwendet.
Der naive Weg, dies zu erreichen, wäre das Löschen und Erstellen von Objekten jedes Mal, wenn Sie sie ein- oder ausblenden wollen, oder das Vermeiden dieser Funktion insgesamt. Es gibt jedoch eine Möglichkeit, Objekte auszublenden, indem man ihre Eigenschaften nativ verwendet (auch wenn der Name es nicht vermuten lässt):
ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); //Show ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); //Hide
Diese Eigenschaft bedeutet, „in welchen Zeitrahmen ein Objekt angezeigt wird und in welchen nicht“. Sie können Objekte nur in bestimmten Zeiträumen anzeigen, aber ich glaube nicht, dass dies in diesem Zusammenhang wirklich erforderlich ist.
Mit den oben beschriebenen ObjectSetInteger-Funktionsaufrufen können wir die Funktionalität zum Ein- und Ausblenden von Objekten in unserer GUI-Objekthierarchie implementieren:
class CElement { private: //... bool m_hidden; bool m_hidden_parent; void HideObject(); void HideByParent(); void HideChildren(); void ShowObject(); void ShowByParent(); void ShowChildren(); //... public: //... void Hide(); void Show(); bool IsHidden() { return m_hidden || m_hidden_parent; } }; //+------------------------------------------------------------------+ //| Display element (if parent is also visible) | //+------------------------------------------------------------------+ void CElement::Show(void) { if (!IsHidden()) return; m_hidden = false; ShowObject(); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } //+------------------------------------------------------------------+ //| Hide element | //+------------------------------------------------------------------+ void CElement::Hide(void) { m_hidden = true; if (m_hidden_parent) return; HideObject(); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } //+------------------------------------------------------------------+ //| Change visibility property to show (not exposed) | //+------------------------------------------------------------------+ void CElement::ShowObject(void) { if (IsHidden()) //Parent or self return; ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); ShowChildren(); } //+------------------------------------------------------------------+ //| Show object when not hidden and parent is shown (not exposed) | //+------------------------------------------------------------------+ void CElement::ShowByParent(void) { m_hidden_parent = false; ShowObject(); } //+------------------------------------------------------------------+ //| Show child objects recursively (not exposed) | //+------------------------------------------------------------------+ void CElement::ShowChildren(void) { for (int i = 0; i < m_child_count; i++) m_children[i].ShowByParent(); } //+------------------------------------------------------------------+ //| Change visibility property to hide (not exposed) | //+------------------------------------------------------------------+ void CElement::HideObject(void) { ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); HideChildren(); } //+------------------------------------------------------------------+ //| Hide element when parent is hidden (not exposed) | //+------------------------------------------------------------------+ void CElement::HideByParent(void) { m_hidden_parent = true; if (m_hidden) return; HideObject(); } //+------------------------------------------------------------------+ //| Hide child objects recursively (not exposed) | //+------------------------------------------------------------------+ void CElement::HideChildren(void) { for (int i = 0; i < m_child_count; i++) m_children[i].HideByParent(); }
Es ist wichtig, zwischen einem Objekt, das verborgen ist, und einem Objekt, das von seinem Elternteil verborgen wird, zu unterscheiden. Ohne diese Funktion würden Objekte, die standardmäßig ausgeblendet sind, angezeigt werden, wenn das übergeordnete Objekt ausgeblendet und dann angezeigt wird (da diese Funktionen rekursiv angewendet werden), oder man könnte Objekte anzeigen, deren übergeordnetes Objekt ausgeblendet ist (z. B. Schaltflächen ohne Fenster dahinter).
In diesem Entwurf sind Anzeigen und Ausblenden die einzigen von außen sichtbaren Funktionen zur Änderung der Sichtbarkeit eines Objekts. Im Wesentlichen werden die Funktionen verwendet, um die Sichtbarkeitskennzeichen zu ändern und ObjectSetProperty aufzurufen, falls erforderlich. Außerdem werden die Flags der untergeordneten Objekte rekursiv geändert. Es gibt weitere Schutzprüfungen, die unnötige Funktionsaufrufe vermeiden (z. B. das Ausblenden eines Kindobjekts, wenn es bereits ausgeblendet ist). Schließlich ist zu beachten, dass ein Neuzeichnen des Charts erforderlich ist, damit die Sichtbarkeitsänderungen angezeigt werden, weshalb wir in beiden Fällen RequestRedraw aufrufen.
Außerdem müssen wir Objekte bei der Erstellung ausblenden, da sie theoretisch vor der Erstellung als ausgeblendet markiert werden könnten:
void CElement::CreateChildren(void) { for (int i = 0; i < m_child_count; i++) { m_children[i].Create(); m_children[i].CreateChildren(); } if (IsHidden()) HideObject(); }
Mit all diesen Komponenten können wir eine kleine Demo erstellen, um die Funktionen zum Ausblenden und Anzeigen zu testen. Wir werden die Vorteile der nutzerdefinierten Klasse aus dem letzten Teil (ziehbare Objekte) nutzen und eine neue Klasse davon ableiten. Unsere neue Klasse reagiert auf die bisherigen Ziehereignisse, aber auch auf Tastaturereignisse, um ihren Ausblendungszustand zu ändern:
class CHideElement : public CDragElement { private: int key_id; protected: virtual void DrawCanvas(); protected: virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); public: CHideElement(int id); }; //+------------------------------------------------------------------+ //| Hide element constructor (set keyboard ID) | //+------------------------------------------------------------------+ CHideElement::CHideElement(int id) : key_id(id) { } //+------------------------------------------------------------------+ //| Hide element when its key is pressed (inherit drag events) | //+------------------------------------------------------------------+ bool CHideElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam); if (id != CHARTEVENT_KEYDOWN) return drag; if (lparam == '0' + key_id) //Toggle hide/show { if (IsHidden()) Show(); else Hide(); } return true; } //+------------------------------------------------------------------+ //| Draw canvas function (fill with color and display number ID) | //+------------------------------------------------------------------+ void CHideElement::DrawCanvas(void) { m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256)); m_canvas.FontSet("Arial", 50); m_canvas.TextOut(25, 25, IntegerToString(key_id), ColorToARGB(clrWhite)); m_canvas.Update(false); }
Bei der Erstellung der Objekte werden wir ihnen eine eindeutige ID-Nummer (von 0 bis 9) zuweisen, um ihre Sichtbarkeit beim Drücken dieser Taste umzuschalten. Der Einfachheit halber werden wir diese ID auch in den Objekten selbst anzeigen. Auch die Ziehereignisse werden zuerst aufgerufen (andernfalls würden sie vollständig überschrieben werden).
int OnInit() { MathSrand((uint)TimeLocal()); //100 is element size by default int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100; int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100; for (int i = 0; i < 10; i++) { CHideElement* drawing = new CHideElement(i); drawing.SetPosition(MathRand() % max_x, MathRand() % max_y); program.AddMainElement(drawing); } program.CreateGUI(); ChartRedraw(0); return(INIT_SUCCEEDED); }
Wenn wir das Programm nun ausführen, können wir überprüfen, ob die Objekte korrekt ein- und ausgeblendet werden, wenn wir die entsprechende Nummer auf der Tastatur drücken.
Z-Reihenfolge (und Neuordnen)
Die Z-Reihenfolge bezieht sich auf die Reihenfolge, in der die Objekte angezeigt werden. Einfach ausgedrückt: Während die X-Y-Koordinaten die Position eines Objekts auf dem Bildschirm bestimmen, legt die Z-Koordinate seine Tiefe oder Stapelreihenfolge fest. Objekte mit niedrigeren Z-Werten werden über denen mit höheren Werten gezeichnet
Sie wissen vielleicht schon, dass es in MetaTrader 5 keine Möglichkeit gibt, die Z-Reihenfolge nach Belieben zu ändern, da die Eigenschaft mit diesem Namen verwendet wird, um zu bestimmen, welches Objekt Klick-Ereignisse empfängt, wenn sie übereinander liegen (aber sie hat nichts mit der visuellen Z-Reihenfolge zu tun), und wir brauchen auch nicht auf Klick-Ereignisse zu prüfen, wie zuvor angegeben (zumindest in dieser Implementierung). In MetaTrader 5 werden die zuletzt erstellten Objekte immer oben platziert (es sei denn, sie sind für den Hintergrund eingestellt).
Wenn Sie jedoch mit dem letzten Beispiel herumspielen, werden Sie vielleicht etwas bemerken...
Wenn Sie ein Objekt aus- und wieder einblenden, erscheint es wieder über allen anderen! Bedeutet das, dass wir ein Objekt sofort ein- und ausblenden können und es über allen anderen erscheinen würde? Ja, das tut es!
Um dies zu testen, müssen wir nur unsere letzte abgeleitete Klasse (CHideElement) geringfügig ändern, sodass bei jeder Tastatureingabe die Z-Reihenfolge dieses spezifischen Objekts erhöht wird, anstatt die Sichtbarkeit umzuschalten. Wir werden auch den Namen der Klasse ändern...
bool CRaiseElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam); if (id != CHARTEVENT_KEYDOWN) return drag; if (lparam == '0' + key_id) { ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } return true; }
Und wie immer dürfen wir nicht vergessen, dass nach einer Änderung der Z-Reihenfolge ein erneutes Zeichnen erforderlich ist. Wenn wir den Test durchführen, werden wir folgendes sehen:
Wie Sie sehen können, können wir Objekte nach Belieben hochhieven. Wir werden diese spezielle Funktion nicht in der Bibliothek implementieren, um die Komplexität des Codes in diesem Artikel nicht noch weiter zu erhöhen (und außerdem kann dieser Effekt auch durch den Aufruf von Hide und dann Show unmittelbar danach erzielt werden). Darüber hinaus gibt es noch mehr Dinge, die mit der Z-Reihenfolge gemacht werden können: Was ist, wenn wir ein Objekt nur um eine Stufe erhöhen wollen? Oder wenn wir die Z-Reihenfolge nach unten verschieben wollen? In allen Fällen besteht die Lösung darin, raise Z in so vielen Objekten wie nötig in der erwarteten richtigen Reihenfolge (von unten nach oben) aufzurufen. Im ersten Fall rufen wir die Funktion für das Objekt auf, das wir um eine Stufe erhöhen möchten, und dann für alle darüber liegenden Objekte, und zwar in der Reihenfolge, in der sie sortiert sind. Im zweiten Fall würden wir das tun, aber für alle Objekte (auch wenn wir dasjenige ignorieren könnten, das am Ende der Z-Reihenfolge steht).
Dennoch gibt es bei der Implementierung dieser Z-Ordnungssysteme einen Haken, der hier nicht explizit gelöst werden soll („als Übung für den Leser“, wie manche sagen würden): Man kann ein ausgeblendetes Objekt nicht einblenden und seine Z-Reihenfolge im selben Frame ändern. Angenommen, Sie zeigen eine Schaltfläche innerhalb eines Fensters an, wollen aber gleichzeitig das Fenster, das die Schaltfläche enthält, anheben: Wenn Sie show für die Schaltfläche aufrufen (was OBJPROP_TIMEFRAMES für diese Schaltfläche auf alle Punkte setzt) und danach raise Z für das Fenster (was OBJPROP_TIMEFRAMES für das Fenster und dann für alle Objekte im Fenster in der richtigen Reihenfolge auf keine Punkte und dann auf alle Punkte setzt), dann würde die Schaltfläche hinter dem Fenster bleiben. Der Grund dafür scheint zu sein, dass nur die ersten Änderungen an der OBJPROP_TIMEFRAMES-Eigenschaft Wirkung zeigen, sodass das Schaltflächenobjekt nur beim Anzeigen (und nicht beim nächsten Erhöhen Z) effektiv angehoben wird.
Eine Lösung für dieses Problem könnte darin bestehen, eine Warteschlange von Objekten zu führen und auf Änderungen der Sichtbarkeit oder der Z-Reihenfolge zu prüfen. Dann führen wir alle nur einmal pro Frame aus. Auf diese Weise müssten Sie auch die Funktion „Show“ ändern, um das Objekt zu „markieren“, anstatt es direkt anzuzeigen. Wenn Sie noch ganz am Anfang stehen, würde ich Ihnen empfehlen, sich nicht zu viele Gedanken darüber zu machen, da es nicht sehr oft vorkommt und nicht kritisch ist (auch wenn Sie die Situationen vermeiden sollten, in denen dieses Problem auftreten könnte).
Schlussfolgerung
In diesem Artikel haben wir uns einige Schlüsselfunktionen angeschaut, die man kennen muss, um eine GUI-Bibliothek effektiv zu erstellen, wenn man sie kombiniert. Es soll ein grundlegendes Verständnis dafür vermitteln, wie Dinge innerhalb einer GUI-Bibliothek im Allgemeinen funktionieren. Die resultierende Bibliothek ist keineswegs vollständig funktionsfähig, sondern eher das Minimum, das nötig war, um einige der Funktionen zu demonstrieren, die in vielen anderen GUI-Bibliotheken verwendet werden.
Während der resultierende Code relativ einfach ist, sollte man beachten, dass GUI-Bibliotheken viel komplexer werden können (und werden), sobald man anfängt, mehr Funktionalität hinzuzufügen oder fertige Objekttypen zu erstellen (besonders wenn sie Unterobjekte mit Ereignisbeziehungen untereinander haben). Auch die Struktur anderer Bibliotheken kann sich je nach Designentscheidungen oder gewünschter Leistung bzw. spezifischer Funktionalität von der hier skizzierten unterscheiden.
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/13169
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.