MQL als Darstellungsmittel für graphische Schnittstellen von MQL-Programmen. Teil 1

9 Juli 2020, 16:37
Stanislav Korotky
0
328

Ob MQL-basierte Programme eine grafische Fensteroberfläche benötigen? Darüber besteht keine Einigkeit. Auf der einen Seite ist der Traum des Händlers die einfachste Art der Kommunikation mit einem Handelsroboter — ein Knopf, der den Handel ermöglicht und auf magische Weise "Geld zu prägen" beginnt. Auf der anderen Seite, weil es ein Traum ist, ist er weit von der Realität entfernt, da man normalerweise ein ganzes Durcheinander von Einstellungen mühsam und lange Zeit auswählen muss, bevor das System zu funktionieren beginnt; aber auch danach muss man es kontrollieren und gegebenenfalls manuell korrigieren. Ich sage nichts von den Anhängern des vollständig manuellen Handels — in ihrem Fall ist die Auswahl eines komfortablen, intuitiven Handelspanels die halbe Miete. Generell kann man sagen, dass eine Fensteroberfläche, in der einen oder anderen Form, eher notwendig wäre als nicht.

Einführung in die GUI-Markup-Technologie

Um eine grafische Oberfläche zu konstruieren, bietet MetaTrader einige oft verwendete Steuerelemente sowohl als unabhängige Objekte, die auf Diagrammen platziert werden können, als auch als solche, die in die "Steuerelemente" der Standardbibliothek eingeschlossen sind, die als ein einziges interaktives Fenster organisiert werden können. Es gibt auch einige alternative Lösungen für den Aufbau einer GUI. All diese Bibliotheken berühren jedoch nur sehr selten das Layout von Elementen, d.h. eine gewisse Automatisierung des Oberflächendesigns.

Natürlich ist es eine seltene Gelegenheit, wenn jemand auf die Idee kommt, im Chart ein Fenster zu zeichnen, das dem MetaTrader selbst entspricht; aber selbst ein scheinbar einfaches Handelspanel kann aus Dutzenden von "Steuerelementen" bestehen, die von MQL aus gesteuert werden, was sich von MQL aus in eine wahre Monotonie verwandelt.

Das Layout ist eine einheitliche Art und Weise, die Anordnungen und Attribute von Oberflächenelementen zu beschreiben, auf deren Grundlage wir sicherstellen können, dass die Fenster automatisch erstellt und mit dem Steuerungscode verknüpft werden.

Erinnern wir uns, wie die Schnittstelle in den Standardinstanzen von MQL erstellt wird.

  bool CPanelDialog::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);
    // create dependent controls
    if(!CreateEdit()) return(false);
    if(!CreateButton1()) return(false);
    if(!CreateButton2()) return(false);
    if(!CreateButton3()) return(false);
    ...
    if(!CreateListView()) return(false);
    return(true);
  }
  
  bool CPanelDialog::CreateButton2(void)
  {
    // coordinates
    int x1 = ClientAreaWidth() - (INDENT_RIGHT + BUTTON_WIDTH);
    int y1 = INDENT_TOP + BUTTON_HEIGHT + CONTROLS_GAP_Y;
    int x2 = x1 + BUTTON_WIDTH;
    int y2 = y1 + BUTTON_HEIGHT;
  
    if(!m_button2.Create(m_chart_id, m_name + "Button2", m_subwin, x1, y1, x2, y2)) return(false);
    if(!m_button2.Text("Button2")) return(false);
    if(!Add(m_button2)) return(false);
    m_button2.Alignment(WND_ALIGN_RIGHT, 0, 0, INDENT_RIGHT, 0);
    return(true);
  }
  ...

Alles geschieht in einem imperativen Stil, wobei viele Aufrufe der gleichen Art verwendet werden. Der MQL-Code erweist sich als langwierig und ineffizient, da er für jedes Element wiederholt wird, wobei jeweils eigene Konstanten (die so genannten "Magicnummer", die als potenzielle Fehlerquelle betrachtet werden) verwendet werden. Das Schreiben eines solchen Codes ist eine undankbare Aufgabe (insbesondere sind die Copy&Paste-Fehler unter Entwicklern sprichwörtlich geworden), und wenn Sie ein neues Element einfügen und die älteren verschieben müssen, müssen Sie höchstwahrscheinlich viele "Magicnummern" manuell neu berechnen und modifizieren.

Im Folgenden wird beschrieben, wie die Beschreibung von Oberflächenelementen in der Dialogklasse aussieht.

  CEdit        m_edit;          // the display field object
  CButton      m_button1;       // the button object
  CButton      m_button2;       // the button object
  CButton      m_button3;       // the fixed button object
  CSpinEdit    m_spin_edit;     // the up-down object
  CDatePicker  m_date;          // the datepicker object
  CListView    m_list_view;     // the list object
  CComboBox    m_combo_box;     // the dropdown list object
  CRadioGroup  m_radio_group;   // the radio buttons group object
  CCheckGroup  m_check_group;   // the check box group object

Diese flache Liste von "Bedienelementen" kann sehr lang sein, und es ist schwer, sie ohne visuelle "Hinweise", die ein Layout liefern könnte, wahrzunehmen und aufrechtzuerhalten.

In anderen Programmiersprachen ist das Oberflächendesign normalerweise von der Codierung getrennt. Deklarative Sprachen, wie XML oder JSON, werden zur Beschreibung des Layouts von Elementen verwendet.

Insbesondere Grundprinzipien zur Beschreibung von Schnittstellenelementen für Android-Projekte finden sich in den Dokumenten oder in Tutorials. Um das Wesentliche zu verstehen, muss man nur eine allgemeine Vorstellung von XML haben. In solchen Dateien ist die Hierarchie klar erkennbar, es werden Containerelemente wie LinearLayout oder RelativeLayout und einzelne "Steuerelemente" wie ImageView, TextView oder CheckBox definiert, wobei die Größen automatisch an den Inhalt angepasst werden, wie match_parent oder wrap_content, und in den Einstellungen werden Links zu den zentralisierten Stilbeschreibungen definiert, und optional werden Ereignisprozessoren angegeben, wobei sicherlich alle Elemente zusätzlich angepasst werden können und andere Ereignisprozessoren aus dem ausführbaren Code heraus an sie angehängt werden können.

Wenn wir uns an die .Net-Plattform erinnern, verwenden sie auch eine ähnliche deklarative Beschreibung von Schnittstellen mit XAML. Selbst für diejenigen, die noch nie in C# und anderen verwalteten Code-Infrastruktursprachen programmiert haben (deren Konzept übrigens der MetaTrader-Plattform und ihrer "verwalteten" MQL-Sprache sehr ähnlich ist), sind die Hauptpunkte — "Steuerelemente", Container, Eigenschaften, Reaktion auf Benutzeraktionen — ebenfalls insgesamt zu finden.

Warum wird das Layout vom Code getrennt und in einer speziellen Sprache beschrieben? Hier sind die grundlegenden Vorteile eines solchen Ansatzes.

  • Visuelle Darstellung von hierarchischen Beziehungen zwischen Elementen und Containern;
  • Logische Gruppierung;
  • Einheitliche Definition von Layout und Ausrichtung;
  • Einfaches Schreiben der Eigenschaften und ihrer Werte;
  • Deklarationen ermöglichen die Implementierung der automatischen Generierung des Codes unter Beibehaltung des Lebenszyklus und der Kontrolle der Elemente, wie z.B. Erstellen, Einrichten, Interagieren und Löschen;
  • Verallgemeinerte Abstraktionsebene, d.h. allgemeine Eigenschaften, Zustände und Initialisierungs-/Verarbeitungsphasen, was die Entwicklung der GUI unabhängig von der Codierung ermöglicht;
  • Wiederholte (mehrfache) Verwendung von Layouts, d.h. dasselbe Fragment kann mehrfach in verschiedenen Dialogen enthalten sein;
  • Dynamische Inhaltsimplementierung/-generierung on-the-fly, ähnlich wie beim Wechsel zwischen Registerkarten, wobei für jede Registerkarte ein spezifischer Satz von Elementen verwendet wird;
  • Dynamische Erzeugung von "Steuerelementen" innerhalb des Layouts, wobei sie in einem einzigen Array von Zeigern auf die Basisklasse gespeichert werden, wie z.B. CWnd im Falle der Standard-MQL-Bibliothek; und
  • Verwendung eines speziellen Grafikeditors für das interaktive Oberflächendesign — in diesem Fall fungiert das spezielle Format zur Beschreibung der Layouts als Bindeglied zwischen der Außendarstellung des Programms und seinem ausführenden Teil in der Programmiersprache.

Für die MQL-Umgebung wurden nur einige wenige Versuche unternommen, einige dieser Probleme zu lösen. Insbesondere wird ein visueller Dialogdesigner in dem Artikel Wie man Objektklassen entwirft und baut vorgestellt. Er arbeitet auf der Grundlage der Bibliothek MasterWindows. Die Möglichkeiten zur Anordnung von Layouts und die Liste der unterstützten Elementtypen sind darin jedoch erheblich eingeschränkt.

Ein fortschrittlicheres Layoutsystem, wenn auch ohne visuellen Designer, wird in den Artikeln Verwendung von Layouts und Containern für GUI Controls: Die CBox Klasse und Die CGrid-Klasse . Sie unterstützt alle Standard-Steuerelemente und andere, die von CWndObj oder CWndContainer geerbt wurden, überlässt aber dennoch dem Benutzer die Routinekodierung zum Erstellen und Anordnen von Komponenten.

Konzeptionell ist dieser Ansatz mit Containern sehr fortschrittlich (wenn es ausreicht, seine Popularität in praktisch allen Markup-Sprachen zu erwähnen). Daher werden wir ihn beherzigen. In einem meiner früheren Artikel (Anwendung von OLAP im Handel (Teil 2): Die Visualisierung der Ergebnisse der interaktiven, mehrdimensionalen Datenanalyse) schlug ich eine Modifikation der Container CBox und CGrid sowie einige Steuerelemente zur Unterstützung der "Gummi"-Eigenschaften vor. Im Folgenden werden wir diese Entwicklungen nutzen und verbessern, um das Problem der automatischen Anordnung von Elementen am Beispiel der Objekte einer Standardbibliothek zu lösen.

Grafischer Editor der Benutzeroberfläche: Vor- und Nachteile

Die Hauptfunktion des Editors der grafischen Oberfläche besteht darin, die Eigenschaften der Elemente im Fenster spontan durch die Befehle des Benutzers zu erstellen und einzurichten. Dies schlägt vor, Eingabefelder zur Auswahl der Eigenschaften zu verwenden; damit sie funktionieren, sollten Sie die Liste der Eigenschaften und ihre Typen für jede Klasse kennen. Daher muss jedes "Steuerelement" zwei miteinander verbundene Versionen haben: Die so genannten Laufzeitversionen (für Standardoperationen) und die Design-time-Versionen (für die interaktive Gestaltung Ihrer Schnittstelle). "Steuerelemente" haben standardmäßig die erste Version — es ist die Klasse, die in Fenstern arbeitet. Die zweite Version ist die Umhüllung des "Steuerelements", die für die Anzeige und Änderung seiner verfügbaren Eigenschaften bestimmt ist. Es wäre eine harte Arbeit, einen solchen Umbruch für jeden Elementtyp zu schreiben. Daher wäre es wünschenswert, diesen Prozess zu automatisieren. Theoretisch können Sie für diesen Zweck den MQL-Parser verwenden, der im Artikel MQL-Parsing mittels MQL beschrieben wird. In vielen Programmiersprachen wird der Begriff der Eigenschaft in die Sprachsyntax gesetzt und kombiniert einen "Setter" und einen "Getter" eines bestimmten internen Feldes des Objektes. MQL hat dies bisher nicht, aber ein ähnliches Prinzip wird in den Fensterklassen der Standardbibliothek verwendet: Um das gleiche Feld zu setzen und zu lesen, wird ein Paar gleichnamiger "Spiegel"-Methoden verwendet — die eine übernimmt den Wert eines bestimmten Typs, und die andere gibt ihn zurück. So wird z.B. die "ReadOnly"-Eigenschaft für das CEdit-Eingabefeld definiert:

    bool ReadOnly(void) const;
    bool ReadOnly(const bool flag);

Und so ermöglicht es das Arbeiten mit der Obergrenze von CSpinEdit:

    int  MaxValue(void) const;
    void MaxValue(const int value);

Mit dem MQL-Parser können Sie diese Methodenpaare in jeder Klasse finden und sie dann unter Berücksichtigung der Vererbungshierarchie in eine allgemeine Liste aufnehmen, wonach Sie eine Wrapper-Klasse erzeugen können, um die gefundenen Eigenschaften interaktiv zu setzen und zu lesen. Sie müssen dies nur einmal für jede Klasse von "Steuerelementen" tun (vorausgesetzt, dass die Klasse ihre 'public' Eigenschaften nicht ändert).

Ein implementierbares Projekt, wenn auch ein groß angelegtes. Bevor Sie es in Angriff nehmen, sollten Sie alle seine Vor- und Nachteile in Betracht ziehen.

Betonen wir 2 zentrale Entwurfsziele: Identifizierung der hierarchischen Abhängigkeiten der Elemente und ihrer Eigenschaften. Wenn irgendwelche alternativen Wege gefunden würden, um sie zu erreichen, könnten wir den visuellen Editor weglassen.

Bei bewusster Überlegung wird klar, dass die grundlegenden Eigenschaften aller Elemente Standard sind, d.h. Typ, Größe, Ausrichtung, Text und Stil (Farbe). Sie können auch spezifische Eigenschaften im MQL-Code einstellen. Glücklicherweise handelt es sich hierbei um einzelne Operationen, die normalerweise mit der Geschäftslogik verbunden sind. Was Typ, Größe und Ausrichtung betrifft, so werden sie implizit durch die Objekthierarchie selbst festgelegt.

Daher kommen wir zu dem Schluss, dass es in den meisten Fällen ausreicht, anstelle eines vollwertigen Editors eine bequeme Möglichkeit zu haben, die Hierarchie der Oberflächenelemente zu beschreiben.

Stellen Sie sich vor, dass alle Steuerelemente und Container innerhalb der Dialogklasse nicht durch eine fortlaufende Liste, sondern durch eine Einrückung beschrieben werden, die eine Baumstruktur der Verschachtelung/Abhängigkeit simuliert.

    CBox m_main;                       // main client window
    
        CBox m_edit_row;                   // top level container/group
            CEdit m_edit;                      // control
      
        CBox m_button_row;                 // top level container/group
            CButton m_button1;                 // control
            CButton m_button2;                 // control
            CButton m_button3;                 // control
      
        CBox m_spin_date_row;              // top level container/group
            SpinEdit m_spin_edit;              // control
            CDatePicker m_date;                // control
      
        CBox m_lists_row;                  // top level container/group
      
            CBox m_lists_column1;              // nested container/group
                ComboBox m_combo_box;              // control
                CRadioGroup m_radio_group;         // control
                CCheckGroup m_check_group;         // control
        
            CBox m_lists_column2;              // nested container/group
                CListView m_list_view;             // control

Auf diese Weise wird die Struktur viel besser sichtbar. Die geänderte Formatierung beeinträchtigt natürlich in keiner Weise die Fähigkeit des Programms, diese Objekte in besonderer Weise zu interpretieren.

Im Idealfall wünschen wir uns eine Methode zur Beschreibung der Schnittstelle, auf deren Grundlage die Bedienelemente nach einer definierten Hierarchie von selbst erstellt werden, einen richtigen Platz auf dem Bildschirm finden und die passende Größe berechnen.

Gestaltung der Auszeichnungssprache

Daher müssen wir eine Auszeichnungssprache entwickeln, die die allgemeine Struktur der Fensteroberfläche und die Eigenschaften der einzelnen Elemente beschreibt. Hier könnten wir uns auf das weit verbreitete XML-Format stützen und einen Satz relevanter Tags reservieren. Wir könnten sie sogar von einem anderen Framework, wie den oben erwähnten, ausleihen. Aber dann müssten wir XML parsen und dann in MQL interpretieren, um es in die Operationen zur Erstellung und Anpassung von Objekten zu übersetzen. Da zudem kein visueller Editor mehr benötigt wird, ist auch die "externe" Markup-Sprache als Kommunikationsmittel zwischen dem Editor und der Laufzeitumgebung überflüssig geworden.

Unter solchen Bedingungen entsteht eine Idee: Kann MQL selbst als Markup-Sprache verwendet werden? Das kann es in der Tat.

Die Hierarchie ist zunächst in MQL integriert. Von einander abgeleitete Klassen kommen einem sofort in den Sinn. Aber Klassen beschreiben die statische Hierarchie, die vor der Ausführung des Codes gebildet wird. Wir benötigen jedoch eine Hierarchie, die so interpretiert werden kann, wie der MQL-Code ausgeführt wird. In einigen anderen Programmiersprachen gibt es für diesen Zweck, d.h. für die Analyse der Hierarchie und der internen Struktur von Klassen aus dem Programm selbst, ein eingebettetes Werkzeug, die sogenannte Laufzeit-Typinformation, RTTI, auch bekannt als Reflections. MQL verfügt jedoch nicht über solche Werkzeuge.

MQL hat jedoch eine andere Hierarchie, wie in den meisten Programmiersprachen: Hierarchie der Kontexte der Ausführung der Codefragmente. Jedes Paar von Klammern in einer Funktion/Methode (d.h. mit Ausnahme derjenigen, die zur Beschreibung von Klassen und Strukturen verwendet werden) bildet einen Kontext, d.h. einen Lebensbereich von lokalen Variablen. Da die Verschachtelung der Einheiten nicht begrenzt ist, können wir sie zur Beschreibung zufälliger Hierarchien verwenden.

Ein ähnlicher Ansatz wurde bereits bei MQL verwendet, insbesondere zur Implementierung eines selbst erstellten Profilers zur Messung der Codeausführungsgeschwindigkeit (siehe MQL's OOP-Notizen: Selbsterstellter Profiler für statische und automatische Objekte). Seine Funktionsprinzipien sind recht einfach. Wenn wir zusammen mit den Operationen, die ein angewandtes Problem lösen, eine lokale Variable in der Code-Einheit deklarieren:

  {
    ProfilerObject obj;
    
    ... // code lines of your actual algorithm
  }

dann wird es sofort beim Betreten der Einheit erstellt und vor dem Verlassen der Einheit gelöscht. Dies gilt für die Objekte aller Klassen, einschließlich derer, die dieses Verhalten berücksichtigen können. Insbesondere können Sie die Zeit dieser Anweisungen im Konstruktor und Destruktor notieren und damit die Dauer des angewandten Algorithmus berechnen. Um diese Messungen zu akkumulieren, ist natürlich ein weiteres, übergeordnetes Objekt erforderlich, d.h. der Profiler selbst. Ein Austauschgerät zwischen ihnen ist hier jedoch nicht sehr wichtig (siehe mehr Details im Blog). Es geht darum, dasselbe Prinzip auch auf die Beschreibung des Layouts anzuwenden. Mit anderen Worten, es wird wie folgt aussehen:

  container<Dialog> dialog(&this);
  {
    container<classA> main; // create classA internal object 1
    
    {
      container<classB> top_level(name, property, ...); // create classB internal object 2
      
      {
        container<classC> next_level_1(name, property, ...); // create classC internal object 3
        
        {
          control<classX> ctrl1(object4, name, property, ...); // create classX object 4
          control<classX> ctrl2(object5, name, property, ...); // create classX object 5
        } // register objects 4&5 in object 3 (via ctrl1, ctrl2 in next_level_1) 
      } // register object 3 in object 2 (via next_level_1 in top_level)
      
      {
        container<classC> next_level2(name, property, ...); // create classC internal object 6
        
        {
          control<classY> ctrl3(object7, name, property, ...); // create classY object 7
          control<classY> ctrl4(object8, name, property, ...); // create classY object 8
        } // register objects 7&8 in object 6 (via ctrl3, ctrl4 in next_level_2)
      } // register object 6 in object 2 (via next_level_2 in top_level)
    } // register object 2 in object 1 (via top_level in main)
  } // register object 1 (main) in the dialog (this)

Bei der Ausführung dieses Codes werden die Objekte einer Klasse (fiktiv "Container" genannt) erzeugt, wobei ein Template-Parameter die Klasse eines bestimmten GUI-Elements definiert, das innerhalb des Dialogs erzeugt werden soll. Alle Container-Objekte werden in einem speziellen Array im Stapelmodus platziert: Jede nächste Verschachtelungsebene fügt dem Array einen Container hinzu, wobei die aktuelle Kontexteinheit am oberen Ende des Stapels verfügbar ist, während sich das Fenster immer ganz unten, d.h. an erster Stelle, befindet. Beim Schließen jeder Einheit werden alle in ihr erzeugten Kind-Elemente automatisch an die unmittelbare Elterneinheit gebunden (die sich genau am oberen Ende des Stapels befindet).

All diese "Magie" muss durch das Innere der Klassen "Container" und "Control" gewährleistet werden. Tatsächlich handelt es sich dabei um die gleiche Klasse, "Layout", aber aus Gründen der besseren Sichtbarkeit ist der Unterschied zwischen Containern und Steuerelementen in der obigen Grafik hervorgehoben. In der Realität hängt der Unterschied nur davon ab, welche Klassen durch die Template-Parameter spezifiziert sind. Daher müssen die Klassen Dialog, classA, classB und classC im obigen Beispiel Fenster-Container sein, d. h. sie müssen die Speicherung von "Steuerelementen" in ihnen unterstützen.

Zu unterscheiden sind kurzlebige Hilfsobjekte des Layouts (sie werden oben als main, top_level, next_level_1, ctrl1, ctrl2, next_level2, ctrl3 und ctrl4 bezeichnet) und die von ihnen kontrollierten Objekte der Interface-Klassen (Objekt 1 ... Objekt 8), die untereinander und mit dem Fenster verbunden bleiben. Dieser gesamte Code wird als Dialogmethode ausgeführt (ähnlich der Methode Create). Daher ist das Dialog-Objekt als "this" verfügbar.

An einige Layout-Objekte senden wir die GUI-Objekte als Klassenvariablen (Objekt 4, 5, 7, 8), an einige nicht (Name und Eigenschaften werden angegeben). In jedem Fall muss das GUI-Objekt existieren, aber wir benötigen es nicht immer explizit. Wenn das "Steuerelement" verwendet wird, um anschließend mit dem Algorithmus zu interagieren, ist es praktisch, einen Link zu ihm zu haben. Container sind in der Regel nicht mit der Programmlogik verbunden und erfüllen nur die Funktionen der Platzierung der "Steuerelemente", daher werden sie nicht explizit innerhalb des Layoutsystems erstellt.

Wir werden die spezifische Syntax der Aufzeichnung der Eigenschaften entwickeln und sie etwas später auflisten.

Klassen für das Oberflächen-Layout: Abstrakte Ebene

Lassen Sie uns Klassen schreiben, die es ermöglichen, die Bildung der Hierarchie der Schnittstellenelemente zu implementieren. Dieser Ansatz kann potenziell auf alle Bibliotheken von "Steuerelementen" angewendet werden. Daher werden wir die Menge der Klassen in 2 Teile aufteilen: Abstrakte Klassen (mit allgemeiner Funktionalität) und angewandte Klassen, die sich auf die spezifischen Aspekte einer bestimmten Bibliothek von Standard-Steuerelementen (CWnd abgeleitete Klassen) beziehen. Wir werden die Durchführbarkeit des Konzepts auf Standard-Dialoge überprüfen, und diejenigen, die dies wünschen, können es, geleitet von der abstrakten Ebene, auf andere Bibliotheken anwenden.

Die Klasse LayoutData ist zentral.

  class LayoutData
  {
    protected:
      static RubbArray<LayoutData *> stack;
      static string rootId;
      int _x1, _y1, _x2, _y2;
      string _id;
    
    public:
      LayoutData()
      {
        _x1 = _y1 = _x2 = _y2 = 0;
        _id = NULL;
      }
  };

In ihm wird die Mindestmenge an Informationen gespeichert, die jedem Layout-Element eigen ist: Eindeutiger Name _id und Koordinaten. Lassen Sie uns klarstellen, dass dieses Feld _id auf einer abstrakten Ebene definiert ist und in jeder einzelnen Bibliothek die GUI auf ihrer eigenen Eigenschaft "controls" "anzeigen" kann. Insbesondere in der Standardbibliothek wird dieses Feld m_name genannt und ist über die 'public' Methode CWnd::Name verfügbar. Zwei Objekte können nicht den gleichen Namen haben. In CWnd ist auch das Feld m_id vom Typ "long" definiert — es wird für den Nachrichtenversand verwendet. Wenn wir die Implementierung der Anwendung erreichen, sollten wir sie nicht mit unserer _id verwechseln.

Außerdem bietet die Klasse LayoutData eine statische Speicherung ihrer einen Instanzen als Stack und einen Window-Instanzbezeichner (rootId). Die Statik der beiden letzten Member ist kein Problem, da jedes MQL-Programm innerhalb eines einzigen Threads ausgeführt wird. Selbst wenn sich mehrere Fenster darin befinden, kann immer nur eines davon gleichzeitig erstellt werden. Sobald ein Fenster gezeichnet wird, ist der Stack bereits leer und bereit, mit einem anderen Fenster zu arbeiten. Die Fenster-ID, rootId, ist für die Standardbibliothek als Feld m_instance_id in der Klasse CAppDialog bekannt. Für andere Bibliotheken muss es etwas Ähnliches geben (nicht notwendigerweise eine Zeichenkette, sondern etwas Eindeutiges, das auf eine Zeichenkette reduzierbar ist), da sonst Fenster miteinander in Konflikt geraten können. Wir werden dieses Problem später noch einmal behandeln.

Von der Klasse LayoutBase wird die Klasse LayoutData abgeleitet. Es ist der Prototyp genau dieser Layout-Klasse, die die Schnittstellenelemente durch den MQL-Code mit Einheiten von geschweiften Klammern als Anweisungen generiert.

  template<typename P,typename C>
  class LayoutBase: public LayoutData
  {
    ...

Seine beiden Vorlagenparameter, P und C, sind für die Klassen von Elementen relevant, die als Container und "Steuerelemente" fungieren.

Container umfassen von der Konzeption her die "Steuerelemente" und/oder andere Container, während "Steuerelemente" als Ganzes wahrgenommen werden und nichts enthalten können. Hier sei ausdrücklich darauf hingewiesen, dass unter einem "Steuerelement" eine logisch monolithische Einheit der Schnittstelle zu verstehen ist, die in der Tat aus vielen Hilfsobjekten bestehen kann. Insbesondere die Klassen CListView oder CComboBox der Standardbibliothek sind zwar "Steuerelemente", aber sie sind darin über mehrere Objekte implementiert. Dies sind die technischen Einzelheiten der Implementierung, während ähnliche Arten von Steuerelementen in anderen Bibliotheken als eine einzige Gliederung implementiert werden können, auf der Schaltflächen und Texte gezeichnet werden. Im Zusammenhang mit abstrakten Layout-Klassen sollten wir uns nicht damit befassen und die Prinzipien der Kapselung brechen, aber die angewandte Implementierung, die für eine bestimmte Bibliothek entworfen wurde, muss natürlich diese Nuance berücksichtigen (und echte Container von zusammengesetzten "Steuerelementen" unterscheiden).

Für die Standardbibliothek sind die besten Kandidaten für die Parameter der Vorlage, P und C, CWndContainer und CWnd. Etwas vorspringend sei angemerkt, dass CWndObj nicht als Klasse von "Steuerelementen" verwendet werden darf, da viele "Steuerelemente" von CWndContainer geerbt werden. Dazu gehören z.B. CComboBox, CListView, CSpinEdit, CDatePicker usw. Als Parameter C sollten wir jedoch die nächste gemeinsame Klasse aller "Steuerelemente" auswählen, und CWnd ist dies für die Standardbibliothek. Wie wir sehen, kann eine Klasse von Containern, wie z.B. CWndContainer, in der Praxis auf einfache Elemente treffen; daher werden wir weiterhin eine genauere Prüfung sicherstellen müssen, ob eine bestimmte Instanz ein Container ist oder nicht. In ähnlicher Weise muss die nächste gemeinsame Klasse aller Container als Parameter P gewählt werden. In der Standardbibliothek ist die Fensterklasse CDialog, der Abkömmling von CWndContainer. Zusammen mit ihr werden wir jedoch die Klassen des CBox-Zweiges verwenden, um Elemente innerhalb von Dialogen zu gruppieren, und sie stammt von CWndClient ab, der wiederum von CWndContainer abstammt. Der nächste gemeinsame Vorfahre ist also CWndContainer.

Felder der Klasse LayoutBase speichern Zeiger auf das vom Layout-Objekt erzeugte Oberflächenelement.

    protected:
      P *container; // not null if container (can be used as flag)
      C *object;
      C *array[];
    public:
      LayoutBase(): container(NULL), object(NULL) {}

Hier zeigen Container und Objekt auf die gleiche Sache; Container ist jedoch nicht NULL, vorausgesetzt, dass das Element wirklich ein Container ist.

Das Array ermöglicht die Verwendung eines Layoutobjekts zur Erstellung einer Gruppe von Elementen desselben Typs, wie z.B. Schaltflächen. In diesem Fall sind Zeigercontainer und Objekt gleich NULL. Für alle Mitglieder gibt es triviale "Getter"-Methoden, wir werden sie nicht alle präsentieren. Zum Beispiel ist es einfach, mit der Methode get() einen Link auf ein Objekt zu erhalten.

Die nächsten drei Methoden deklarieren abstrakte Operationen über das gebundene Element, das in der Lage sein muss, das Layout-Objekt auszuführen.

    protected:
      virtual bool setContainer(C *control) = 0;
      virtual string create(C *object, const string id = NULL) = 0;
      virtual void add(C *object) = 0;

Die Methode setContainer ermöglicht die Unterscheidung eines Containers von einem normalen "Control" in dem übergebenen Parameter. In dieser Methode schlagen wir vor, das Container-Feld auszufüllen. Wenn es nicht NULL ist, dann wird true zurückgegeben.

Die Methode create initiiert das Element (eine ähnliche Methode, Create, gibt es in allen Klassen der Standardbibliothek; aber meiner Meinung nach enthalten andere Bibliotheken, wie EasyAndFastGUI, ähnliche Methoden; aber im Falle von EasyAndFastGUI werden sie aus irgendeinem Grund in verschiedenen Klassen unterschiedlich benannt; deshalb müssen wir für diejenigen, die den beschriebenen Layout-Mechanismus damit verbinden wollen, Adapterklassen schreiben, die die Programm-Schnittstelle der "Steuerelemente" verschiedener Zeiten vereinheitlichen; aber es gibt noch mehr: Es ist viel wichtiger, Klassen ähnlich wie CBox und CGrid für EasyAndFastGUI zu schreiben). Sie können der Methode den gewünschten Bezeichner des Elements übergeben, aber es ist nicht unbedingt der Fall, dass der ausführende Algorithmus diesen Wunsch ganz oder teilweise berücksichtigt (insbesondere kann instance_id hinzugefügt werden). Daher können Sie den wirklichen Bezeichner aus der zurückzugebenden Zeichenkette erfahren.

Die Methode "add" fügt dem übergeordneten Containerelement ein Element hinzu (in der Standardbibliothek wird diese Operation von der Methode Add ausgeführt, während sie in EasyAndFastGUI anscheinend von MainPointer ausgeführt wird).

Nun wollen wir sehen, wie diese 3 Methoden auf der abstrakten Ebene beteiligt sind. Wir haben jedes Element der Schnittstelle an das Layoutobjekt gebunden und durchlaufen 2 Phasen: Erstellung (beim Initiieren der lokalen Variable in der Code-Einheit) und Löschung (beim Verlassen der Code-Einheit und Aufruf des Destruktors der lokalen Variable). Für die erste Phase werden wir die Methode init schreiben, die von den Konstruktoren der abgeleiteten Klassen aufgerufen wird.

      template<typename T>
      void init(T *ref, const string id = NULL, const int x1 = 0, const int y1 = 0, const int x2 = 0, const int y2 = 0)
      {
        object = ref;
        setContainer(ref);
        
        _x1 = x1;
        _y1 = y1;
        _x2 = x2;
        _y2 = y2;
        if(stack.size() > 0)
        {
          if(_x1 == 0 && _y1 == 0 && _x2 == 0 && _y2 == 0)
          {
            _x1 = stack.top()._x1;
            _y1 = stack.top()._y1;
            _x2 = stack.top()._x2;
            _y2 = stack.top()._y2;
          }
          
          _id = rootId + (id == NULL ? typename(T) + StringFormat("%d", object) : id);
        }
        else
        {
          _id = (id == NULL ? typename(T) + StringFormat("%d", object) : id);
        }
        
        string newId = create(object, _id);
        
        if(stack.size() == 0)
        {
          rootId = newId;
        }
        if(container)
        {
          stack << &this;
        }
      }

Der erste Parameter ist der Zeiger auf das Element der betreffenden Klasse. Hier beschränken wir uns bisher darauf, einen Fall zu betrachten, in dem das Element von außen übergeben wird. In der obigen Entwurfssyntax des Layouts hatten wir jedoch einige implizite Elemente (es wurden nur Namen dafür angegeben). Wir werden auf dieses Operationsschema etwas später zurückkommen.

Die Methode speichert den Zeiger auf das Element in einem Objekt, prüft mit setContainer, ob es sich um einen Container handelt (was darauf hindeutet, dass, wenn ja, auch das Container-Feld ausgefüllt wird), und übernimmt die angegebenen Koordinaten aus Eingaben oder optional aus dem Elterncontainer, sofern dieser sich bereits im Stapel befindet. Der Aufruf von "create" initiiert das Interface-Element. Wenn der Stapel noch leer ist, speichern wir den Bezeichner in rootId (im Falle der Standardbibliothek ist es instance_id), da das erste Element auf dem Stapel immer der vorderste Container sein wird, d.h. das Fenster, das für alle absteigenden Elemente verantwortlich ist (in der Standardbibliothek ist es die Klasse CDialog oder ein abgeleitetes). Wenn das aktuelle Element schließlich ein Container ist, legen wir es auf den Stapel (Stapel << &this).

Die Methode init ist eine Vorlage. Dies erlaubt die automatische Generierung der Namen von "Controls" nach Typen; darüber hinaus werden wir bald weitere ähnliche Methoden init hinzufügen. Eine von ihnen wird Elemente im Inneren erzeugen, anstatt sie von außen fertig zu nehmen, und in diesem Fall brauchen wir einen bestimmten Typ. Eine andere Version von init ist darauf ausgelegt, im Layout mehrere Elemente desselben Typs gleichzeitig zu registrieren (denken Sie an das array[]-Mitglied), während Arrays über Links übergeben werden, und die Links unterstützen keine Typkonvertierung ("Parameter-Konvertierung nicht erlaubt", "keine der Überladungen kann auf den Funktionsaufruf angewendet werden", je nach Code-Struktur), weswegen wir wiederum über den Template-Parameter auf einen bestimmten Typ verweisen müssen. Somit werden alle Methoden init den gleichen "Template"-Vertrag haben, d.h. Regeln für die Verwendung.

Die interessantesten Dinge passieren im Destruktor LayoutBase.

      ~LayoutBase()
      {
        if(container)
        {
          stack.pop();
        }
        
        if(object)
        {
          LayoutBase *up = stack.size() > 0 ? stack.top() : NULL;
          if(up != NULL)
          {
            up.add(object);
          }
        }
      }
  };

Wenn das aktuell gebundene Element ein Container ist, werden wir es vom Stapel löschen, da wir aus der entsprechenden Einheit von Klammern aussteigen (Container ist leer). Die Sache ist die, dass es innerhalb jeder Einheit der oberste Teil des Stapels ist, der den höchstverschachtelten Container enthält, wo die innerhalb der Einheit vorkommenden Elemente hinzugefügt werden (tatsächlich wurden sie bereits hinzugefügt), wobei diese Elemente sowohl "Steuerelemente" als auch kleinere Container sein können. Dann werden die aktuellen Elemente mit der Methode des "add" in den Container eingefügt, der seinerseits an die Spitze des Stapels gelangt ist.

Klassen für das Interface-Layout: Angewandte Ebene für die Elemente der Standardbibliothek

Lassen Sie uns zu spezifischeren Dingen kommen — zur Implementierung der Klassen für das Layout der Schnittstellenelemente der Standardbibliothek. Mit den Klassen CWndContainer und CWnd als Schablonenparameter definieren wir die Zwischenklasse StdLayoutBase.

  class StdLayoutBase: public LayoutBase<CWndContainer,CWnd>
  {
    public:
      virtual bool setContainer(CWnd *control) override
      {
        CDialog *dialog = dynamic_cast<CDialog *>(control);
        CBox *box = dynamic_cast<CBox *>(control);
        if(dialog != NULL)
        {
          container = dialog;
        }
        else if(box != NULL)
        {
          container = box;
        }
        return true;
      }

Die Methode setContainer verwendet dynamische Casts, um zu prüfen, ob das Element CWnd von CDialog oder CBox abgeleitet ist, und wenn ja, dann ist es ein Container.

      virtual string create(CWnd *child, const string id = NULL) override
      {
        child.Create(ChartID(), id != NULL ? id : _id, 0, _x1, _y1, _x2, _y2);
        return child.Name();
      }

Die Methode "create" initiiert das Element und gibt seinen Namen zurück. Beachten Sie, dass wir nur mit dem aktuellen Diagramm (ChartID()) und im Hauptfenster arbeiten (Unterfenster wurden in diesem Projekt nicht berücksichtigt, aber Sie können den Code an Ihre Bedürfnisse anpassen, wenn Sie möchten).

      virtual void add(CWnd *child) override
      {
        CDialog *dlg = dynamic_cast<CDialog *>(container);
        if(dlg != NULL)
        {
          dlg.Add(child);
        }
        else
        {
          CWndContainer *ptr = dynamic_cast<CWndContainer *>(container);
          if(ptr != NULL)
          {
            ptr.Add(child);
          }
          else
          {
            Print("Can't add ", child.Name(), " to ", container.Name());
          }
        }
      }
  };

Die Methode "add" fügt dem Elternelement ein untergeordnetes Element hinzu, wobei vorläufig so viel "Upcasting" wie möglich durchgeführt wird, da die Methode Add in der Standardbibliothek nicht virtuell ist (technisch gesehen könnten wir eine relevante Änderung in der Standardbibliothek vornehmen, aber wir werden später über die Modifizierung sprechen).

Auf der Grundlage der Klasse StdLayoutBase werden wir ein Arbeitsklassen-Layout _layout erstellen, das im Code mit der Beschreibung des Layouts in MQL vorhanden sein wird. Der Name beginnt mit einem Unterstrich, um die Aufmerksamkeit auf den nicht standardmäßigen Zweck der Objekte dieser Klasse zu lenken. Betrachten wir eine vereinfachte Version der Klasse. Wir werden ihr später noch etwas mehr Funktionalität hinzufügen. Tatsächlich werden alle Aktivitäten durch Konstruktoren gestartet, innerhalb derer die eine oder andere Methode init von LayoutBase aus aufgerufen wird.

  template<typename T>
  class _layout: public StdLayoutBase
  {
    public:
      
      _layout(T &ref, const string id, const int dx, const int dy)
      {
        init(&ref, id, 0, 0, dx, dy);
      }
      
      _layout(T *ptr, const string id, const int dx, const int dy)
      {
        init(ptr, id, 0, 0, dx, dy);
      }
      
      _layout(T &ref, const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(&ref, id, x1, y1, x2, y2);
      }
      
      _layout(T *ptr, const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(ptr, id, x1, y1, x2, y2);
      }
      
      _layout(T &refs[], const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(refs, id, x1, y1, x2, y2);
      }
  };

Mit Hilfe des folgenden Klassendiagramms können Sie das Gesamtbild überblicken. Es ist etwas darauf, was wir kennen lernen müssen, aber die meisten Klassen sind uns vertraut.

Diagramm der GUI-Layout-Klassen

Diagramm der GUI-Layout-Klassen

Nun könnten wir praktisch überprüfen, wie die Beschreibung eines Objekts, wie z.B. _layout<CButton> button(m_button, 100, 20), das Objekt m_button in einem Dialog initiiert und registriert, sofern es in einer externen Einheit wie dieser beschrieben wird: _layout<CAppDialog> dialog(this, name, x1, y1, x2, y2). Elemente haben jedoch neben der Größe noch viele andere Eigenschaften. Einige Eigenschaften, wie z.B. die Ausrichtung nach Seiten, sind für das Layout von nicht geringerer Bedeutung als Koordinaten. Wenn das Element eine horizontale Ausrichtung im Sinne der "Ausrichtung" der Standardbibliothek hat, wird es in der Tat über die gesamte Breite des übergeordneten Containerbereichs gestreckt, abzüglich der vordefinierten Felder auf der linken und rechten Seite. Somit hat die Ausrichtung Vorrang vor den Koordinaten. Darüber hinaus ist in den Containern der Klasse CBox die Ausrichtung (Richtung) wichtig, in der die untergeordneten Elemente platziert werden, d.h. horizontal (standardmäßig) oder vertikal. Es wäre auch richtig, andere Eigenschaften zu unterstützen, die die Außendarstellung beeinflussen, wie z.B. Schriftgröße oder -farbe und den Betriebsmodus, wie z.B. schreibgeschützt, "klebrige" Schaltflächen usw.

Wenn ein GUI-Objekt in einer Fensterklasse beschrieben und an das Layout übergeben wird, könnten wir die "nativen" Methoden zum Setzen der Eigenschaften verwenden, wie z.B. edit.Text("text"). Das Layoutsystem unterstützt diese alte Technik, aber es handelt sich nicht um die einzige oder eine optimale Technik. In vielen Fällen wäre es bequem, Objekte zu erstellen, die dem Layoutsystem zugewiesen werden können, dann sind sie nicht direkt vom Fenster aus verfügbar. Daher ist es notwendig, die Möglichkeiten der Klasse _Layout hinsichtlich der Anpassung der Elemente irgendwie zu erweitern.

Da es viele Eigenschaften gibt, empfiehlt es sich, die Arbeit an ihnen nicht der gleichen Klasse aufzubürden, sondern die Verantwortung zwischen ihr und einer speziellen Hilfsklasse zu teilen. Gleichzeitig ist _layout immer noch der Ausgangspunkt für die Registrierung der Elemente, delegiert aber alle Einrichtungsdetails an die neue Klasse. Das ist um so wichtiger, um die Layout-Technik so unabhängig wie möglich von der spezifischen Bibliothek der Bedienelemente zu machen.

Klassen zur Konfiguration der Eigenschaften von Elementen

Auf der abstrakten Ebene wird die Menge der Eigenschaften durch ihre Werttypen unterteilt. Wir werden die grundlegenden eingebetteten Typen von MQL unterstützen, sowie einige andere, die später besprochen werden. Syntaktisch wäre es praktisch, Eigenschaften durch eine Aufrufkette des bekannten Musters Builder zuzuweisen:

  _layout<CBox> column(...);
  column.style(LAYOUT_STYLE_VERTICAL).color(clrGray).margin(5);

Diese Syntax impliziert jedoch einen sehr langen Satz von Methoden innerhalb einer Klasse, wobei letztere die Layout-Klasse sein muss, da der Dereferenzoperator (Punkt) nicht überschrieben werden kann. In der Klasse _layout könnte eine Methode reserviert werden, um eine Instanz des Hilfsobjekts für Eigenschaften wie diese zurückzugeben:

  _layout<CBox> column(...);
  column.properties().style(LAYOUT_STYLE_VERTICAL).color(clrGray).margin(5);

Aber es wäre nicht unangebracht, viele Proxy-Klassen zu definieren — jede für ihren eigenen Elementtyp, um die Richtigkeit der zugewiesenen Eigenschaften in der Kompilierungsphase zu überprüfen. Dies würde das Projekt komplizieren, aber wir möchten für die erste Testimplementierung alles so einfach wie möglich machen. Nun, dieser Ansatz ist nun zur weiteren Ausdehnung überlassen.

Es sei auch darauf hingewiesen, dass die Namen der Methoden in der "builder"-Vorlage in gewisser Weise redundant sind, da Werte wie LAYOUT_STYLE_VERTICAL oder clrGray selbsterklärend sind und andere Typen oft keiner detaillierten Beschreibung bedürfen — so bedeutet für das CEdit-"Steuerelement" der boolartige Wert normalerweise das "Nur-Lesen"-Flag, während er für CButton das "Klebe-Signal" ist. Infolgedessen ist es verlockend, Werte einfach mit einem überladenen Operator zuzuweisen. Seltsamerweise passt der Zuweisungsoperator jedoch nicht zu uns, da er kein Einfädeln der Aufrufkette erlaubt.

  _layout<CBox> column(...);
  column = LAYOUT_STYLE_VERTICAL = clrGray = 5; // 'clrGray' - l-value required ...

Einzeilige Zuweisungsoperatoren werden von rechts nach links ausgeführt, d.h. nicht ausgehend von dem Objekt, in dem die überladene Zuweisung eingeführt wird. Dies würde wie folgt funktionieren:

  ((column = LAYOUT_STYLE_VERTICAL) = clrGray) = 5; 

Aber es sieht ein bisschen umständlich aus.

Die Version:

  column = LAYOUT_STYLE_VERTICAL; // orientation
  column = clrGray;               // color
  column = 5;                     // margin

ist auch zu lang. Daher haben wir beschlossen, den Operator <= zu überladen und wie folgt zu verwenden:

  column <= LAYOUT_STYLE_VERTICAL <= clrGray <= 5.0;

Zu diesem Zweck verfügt die Klasse LayoutBase über einen Anknüpfungspunkt:

    template<typename V>
    LayoutBase<P,C> *operator<=(const V value) // template function cannot be virtual
    {
      Print("Please, override " , __FUNCSIG__, " in your concrete Layout class");
      return &this;
    }

Sein doppeltes Ziel ist es, die Absicht zu erklären, die Operator-Überladung zu verwenden, und daran zu erinnern, die Methode in der abgeleiteten Klasse außer Kraft zu setzen. Theoretisch muss dort das Objekt der Mediatorklasse mit dem folgenden Interface (nicht vollständig dargestellt) verwendet werden.

  template<typename T>
  class ControlProperties
  {
    protected:
      T *object;
      string context;
      
    public:
      ControlProperties(): object(NULL), context(NULL) {}
      ControlProperties(T *ptr): object(ptr), context(NULL) {}
      void assign(T *ptr) { object = ptr; }
      T *get(void) { return object; }
      virtual ControlProperties<T> *operator[](const string property) { context = property; StringToLower(context); return &this; };
      virtual T *operator<=(const bool b) = 0;
      virtual T *operator<=(const ENUM_ALIGN_MODE align) = 0;
      virtual T *operator<=(const color c) = 0;
      virtual T *operator<=(const string s) = 0;
      virtual T *operator<=(const int i) = 0;
      virtual T *operator<=(const long l) = 0;
      virtual T *operator<=(const double d) = 0;
      virtual T *operator<=(const float f) = 0;
      virtual T *operator<=(const datetime d) = 0;
  };

Wie wir sehen, ist in der Mediatorklasse ein Link auf das einzurichtende Element (Objekt) gespeichert. Die Bindung erfolgt im Konstruktor oder über die Methode assign. Wenn wir davon ausgehen, dass wir einen bestimmten Mediator der Klasse MyControlProperties geschrieben haben:

  template<typename T>
  class MyControlProperties: public ControlProperties<T>
  {
    ...
  };

dann können wir in der Klasse _layout ihr Objekt nach folgendem Schema verwenden (hinzugefügte Zeilen und Methoden sind kommentiert):

  template<typename T>
  class _layout: public StdLayoutBase
  {
    protected:
      C *object;
      C *array[];
      
      MyControlProperties helper;                                          // +
      
    public:
      ...
      _layout(T *ptr, const string id, const int dx, const int dy)
      {
        init(ptr, id, 0, 0, dx, dy); // this will save ptr in the 'object'
        helper.assign(ptr);                                                // +
      }
      ...
      
      // non-virtual function override                                     // +
      template<typename V>                                                 // +
      _layout<T> *operator<=(const V value)                                // +
      {
        if(object != NULL)
        {
          helper <= value;
        }
        else
        {
          for(int i = 0; i < ArraySize(array); i++)
          {
            helper.assign(array[i]);
            helper <= value;
          }
        }
        return &this;
      }

Da der Operator <= in _Layout ein Template-Operator ist, wird er automatisch einen Aufruf für einen korrekten Parametertyp aus der Schnittstelle von ControlProperties erzeugen (es geht natürlich nicht um die abstrakten Methoden der Schnittstelle, sondern um deren Implementierung in der abgeleiteten Klasse MyControlProperties; wir werden demnächst einen solchen für eine bestimmte Fensterbibliothek schreiben).

In einigen Fällen wird derselbe Datentyp verwendet, um mehrere verschiedene Eigenschaften zu definieren. Zum Beispiel wird dasselbe bool in CWnd verwendet, wenn die Flags der Sichtbarkeit und des aktiven Zustands von Elementen gesetzt werden, zusammen mit den oben erwähnten Modi "read only" (für CEdit) und "sticking" (für CButton). Um einen Eigenschaftsnamen explizit angeben zu können, wird in der Schnittstelle ControlProperties der Operator [] mit dem Parameter für den Zeichenkettentyp bereitgestellt. Er legt das Feld "context" fest, auf dessen Grundlage die abgeleitete Klasse das erforderliche Merkmal ändern kann.

Für jede Kombination des Eingabetyps und der Elementklasse wird eine der Eigenschaften (die am häufigsten verwendete) als Standardeigenschaft betrachtet (ihre Beispiele für CEdit und CButton sind oben dargestellt). Andere Eigenschaften benötigen einen Kontext, der angegeben werden muss.

Für CButton wird er beispielsweise wie folgt aussehen:

  button1 <= true;
  button2["visible"] <= false;

In der ersten Zeile wird kein Kontext angegeben; daher wird die Eigenschaft "locking" (eine Schaltfläche mit zwei Positionen) impliziert. In der zweiten Zeile wird die Schaltfläche zunächst als unsichtbar angelegt, was normalerweise ein seltener Fall ist.

Betrachten wir die grundlegenden Details der Implementierung des Mediators StdControlProperties für die Bibliothek der Standardelemente. Der vollständige Code ist in den beigefügten Dateien zu finden. Zu Beginn können Sie sehen, wie der Operator <= für den Typ "bool" überschrieben wird.

  template<typename T>
  class StdControlProperties: public ControlProperties<T>
  {
    public:
      StdControlProperties(): ControlProperties() {}
      StdControlProperties(T *ptr): ControlProperties(ptr) {}
      
      // we need dynamic_cast throughout below, because control classes
      // in the standard library does not provide a set of common virtual methods
      // to assign specific properties for all of them (for example, readonly
      // is available for edit field only)
      virtual T *operator<=(const bool b) override
      {
        if(StringFind(context, "enable") > -1)
        {
          if(b) object.Enable();
          else  object.Disable();
        }
        else
        if(StringFind(context, "visible") > -1)
        {
          object.Visible(b);
        }
        else
        {
          CEdit *edit = dynamic_cast<CEdit *>(object);
          if(edit != NULL) edit.ReadOnly(b);
          
          CButton *button = dynamic_cast<CButton *>(object);
          if(button != NULL) button.Locking(b);
        }
        
        return object;
      }

Die folgende Regel wird für Zeichenfolgen verwendet: Jeder Text gelangt in die "Control"-Kopf nur, wenn kein "Font"-Kontext, d.h. der Fontname, angegeben wird:

      virtual T *operator<=(const string s) override
      {
        CWndObj *ctrl = dynamic_cast<CWndObj *>(object);
        if(ctrl != NULL)
        {
          if(StringFind(context, "font") > -1)
          {
            ctrl.Font(s);
          }
          else // default
          {
            ctrl.Text(s);
          }
        }
        return object;
      }

In der Klasse StdControlProperties haben wir zusätzlich die <== Überschreibungen für die Typen eingeführt, die nur in der Standardbibliothek enthalten sind. Insbesondere kann sie die Enumeration ENUM_WND_ALIGN_FLAGS nehmen, die eine Ausrichtungsversion beschreibt. Bitte beachten Sie, dass in dieser Enumeration neben den vier Seiten (links, rechts, oben und unten) nicht alle, sondern die am häufigsten verwendeten Kombinationen beschrieben werden, wie z.B. die Ausrichtungsbreite (WND_ALIGN_WIDTH = WND_ALIGN_LEFT|WND_ALIGN_RIGHT) oder der gesamte Client-Bereich (WND_ALIGN_CLIENT = WND_ALIGN_WIDTH|WND_ALIGN_HEIGHT). Wenn Sie jedoch ein Element nach der Breite und dem oberen Rand ausrichten müssen, ist diese Kombination von Flags nicht mehr Teil der Aufzählung. Daher müssen wir die Typkonvertierung dazu explizit angeben ((ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_WIDTH|WND_ALIGN_TOP)). Andernfalls wird die bitweise ODER-Operation den Typ int erzeugen, und die falsche Überladung beim Einrichten der Integer-Eigenschaften wird aufgerufen. Eine alternative Lösung ist die Angabe des Kontextes "align".

Es überrascht nicht, dass ein Überschreiben für den int Typ am mühsamsten ist. Insbesondere können Eigenschaften wie Breite, Höhe, Ränder, Schriftgröße usw. eingestellt werden. Um diese Situation zu erleichtern, wurde die Möglichkeit geschaffen, Größen direkt im Layout-Objektkonstruktor anzugeben, während Ränder alternativ durch die Verwendung doppelter Typnummern oder durch eine spezielle Verpackung namens PackedRect angegeben werden können. Natürlich wurde dafür auch die Operatorüberlastung hinzugefügt; es ist praktisch, sie dort einzusetzen, wo unsymmetrische Ränder erforderlich sind:

  button <= PackedRect(5, 100, 5, 100); // left, top, right, bottom

weil es einfacher ist, gleichseitige Felder mit nur einem Wert vom Typ double zu trennen:

  button <= 5.0;

Der Nutzer kann jedoch eine Alternative wählen, d.h. den Kontext "margin"; dann benötigt er kein double, und der äquivalente Datensatz wird wie folgt aussehen:

  button["margin"] <= 5;

Was Ränder und Einzüge betrifft, sollten Sie nur einen Vorbehalt beachten. Es gibt den Ausrichtungsterm in der Standardbibliothek, der Ränder enthält, die automatisch um das "Steuerelement" herum hinzugefügt werden. Gleichzeitig ist in den CBox-Klassen ein eigener Auffüllmechanismus implementiert, der eine Lücke innerhalb eines Containers zwischen seiner äußeren Begrenzung und den untergeordneten "Steuerelementen" (Inhalt) darstellt. Daher bedeuten Felder im Sinne von "Steuerelementen" und Einzüge im Sinne von Containern im Wesentlichen dasselbe. Da leider zwei Positionierungsalgorithmen einander nicht berücksichtigen, kann die gleichzeitige Verwendung sowohl von Rändern als auch von Einrückungen Probleme verursachen (am offensichtlichsten ist die Verschiebung von Elementen, die nicht Ihren Erwartungen entspricht). Die allgemeine Empfehlung lautet, Einzüge bei Null zu belassen und mit Rändern zu manipulieren. Falls erforderlich, könnten Sie jedoch versuchen, auch Einzüge einzufügen, insbesondere wenn es sich um einen bestimmten Behälter und nicht um allgemeine Einstellungen handelt.

Dieses Papier ist eine Machbarkeitsstudie (Proof-of-Concept (POC)) und stellt keine fertige Lösung dar. Sein Zweck ist es, die vorgeschlagene Technik an den Standard-Bibliotheksklassen und -containern auszuprobieren, die zum Zeitpunkt des Verfassens zur Verfügung stehen, mit den minimalen Modifikationen all dieser Komponenten. Im Idealfall müssen die Container (nicht notwendig die CBox-Container) als integraler Bestandteil der GUI-Elemente-Bibliothek geschrieben werden und unter Berücksichtigung aller möglichen Kombinationen von Modi funktionieren.

Unten finden Sie die Tabelle der unterstützten Eigenschaften und Elemente. Die Klasse CWnd bedeutet die Anwendbarkeit der Eigenschaften auf alle Elemente, während die Klasse CWndObj für einfache "Steuerelemente" steht (zwei davon, CEdit und CButton, sind ebenfalls in der Tabelle angegeben). Die Klasse CWndClient verallgemeinert "Steuerelemente" (CCheckGroup, CRadioGroup und CListView), und sie ist die übergeordnete Klasse für die Container CBox/CGrid.

Tabelle der von Datentypen und Elementklassen unterstützten Eigenschaften

type/control CWnd CWndObj CWndClient CEdit CButton CSpinEdit CDatePicker CBox/CGrid
bool visible
enable
visible
enable
visible
enable
(readonly)
visible
enable
(locking)
visible
enable
visible
enable
visible
enable
visible
enable
color (text)
background
border
(background)
border
(text)
background
border
(text)
background
border
(background)
border
string (text)
font
(text)
font
(text)
font
int width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
fontsize
width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
fontsize
width
height
margin
left
top
right
bottom
align
fontsize
(value)
width
height
margin
left
top
right
bottom
align
min
max
width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
long (id) (id)
zorder
(id) (id)
zorder
(id)
zorder
(id) (id) (id)
double (margin) (margin) (margin) (margin) (margin) (margin) (margin) (margin)
float (padding)
left *
top *
right *
bottom *
datetime (value)
PackedRect (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4])
ENUM_ALIGN_MODE (text align)
ENUM_WND_ALIGN_FLAGS (alignment) (alignment) (alignment) (alignment) (alignment) (alignment) (alignment) (alignment)
LAYOUT_STYLE (style)
VERTICAL_ALIGN (vertical align)
HORIZONTAL_ALIGN (horizonal align)


Der vollständige Quellcode der Klasse StdControlProperties ist beigefügt, der die Übersetzung der Eigenschaften der Layoutelemente und den Aufruf der Methoden der Standardkomponentenbibliothek sicherstellt.

Versuchen wir, die Layout-Klassen auszuprobieren. Endlich können wir damit beginnen, die Instanzen zu studieren, von einfach bis komplex. Entsprechend der Tradition, die sich seit der Veröffentlichung der beiden Originalartikel über das Layout der GUI mit Hilfe von Containern entwickelt hat, wollen wir das Schiebepuzzle (SlidingPuzzle4) und eine Standard-Demo für die Arbeit mit "controls" (ControlsDialog4) an die neue Technik anpassen. Die Indizes entsprechen den Etappen der Aktualisierung dieser Projekte. Im Artikel werden die gleichen Programme mit den Indizes 3 vorgestellt, und Sie können die Quellcodes vergleichen, wenn Sie möchten. Beispiele finden Sie im Ordner MQL5/Experts/Examples/Layouts/.

Das Beispiel 1. SlidingPuzzle

Die einzige wesentliche Änderung in der öffentlichen Oberfläche der Hauptform von CSlidingPuzzleDialog ist die neue Methode CreateLayout. Sie sollte aus dem Handler OnInit aufgerufen werden statt aus dem konventionellen Create. Beide Methoden haben die gleichen Parameterlisten. Diese Ersetzung war erforderlich, da der Dialog selbst ein Layout-Objekt (die äußerste Ebene) ist und seine Methode Create vom neuen Framework automatisch aufgerufen wird (Methode StdLayoutBase::create tut dies, was wir oben betrachtet haben). Alle Informationen für das Framework über das Formular und seinen Inhalt werden in der Methode CreateLayout unter Verwendung der MQL-basierten Markup-Sprache spezifisch definiert. Hier ist die Methode selbst:

  bool CSlidingPuzzleDialog::CreateLayout(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    {
      _layout<CSlidingPuzzleDialog> dialog(this, name, x1, y1, x2, y2);
      {
        _layout<CGridTkEx> clientArea(m_main, NULL, 0, 0, ClientAreaWidth(), ClientAreaHeight());
        {
          SimpleSequenceGenerator<long> IDs;
          SimpleSequenceGenerator<string> Captions("0", 15);
          
          _layout<CButton> block(m_buttons, "block");
          block["background"] <= clrCyan <= IDs <= Captions;
          
          _layout<CButton> start(m_button_new, "New");
          start["background;font"] <= clrYellow <= "Arial Black";
          
          _layout<CEdit> label(m_label);
          label <= "click new" <= true <= ALIGN_CENTER;
        }
        m_main.Init(5, 4, 2, 2);
        m_main.SetGridConstraints(m_button_new, 4, 0, 1, 2);
        m_main.SetGridConstraints(m_label, 4, 2, 1, 2);
        m_main.Pack();
      }
    }
    m_empty_cell = &m_buttons[15];
    
    SelfAdjustment();
    return true;
  }

Hier werden nacheinander zwei verschachtelte Container gebildet, die jeweils durch ein eigenes Layoutobjekt gesteuert werden:

  • dialog für eine Instanz von CSlidingPuzzleDialog (Variable "this");
  • clientArea für das Element CGridTkEx m_main;

Dann wird im Client-Bereich die Gruppe von Schaltflächen, CButton m_buttons[16], initialisiert, der an das einzelne Layout-Objekt, den Block, sowie an die Spielstartschaltfläche (CButton m_button_new im "Start"-Objekt) und das informierende Label (CEdit m_label, Objekt "label") gebunden ist. Alle lokalen Variablen, d.h. Dialog, clientArea, Block, Start und Label, sorgen dafür, dass bei der Ausführung des Codes automatisch Create für die Schnittstellenelemente aufgerufen wird, ihnen die definierten Zusatzparameter zugewiesen werden (Parameter werden etwas später besprochen), und die an sie gebundenen Schnittstellenelemente beim Löschen, d.h. wenn sie über die Sichtbarkeit des nächsten Klammerblocks hinausgehen, im übergeordneten Container registriert werden. Dadurch wird der Client-Bereich m_Haupt in das Fenster "this" aufgenommen, während alle "Steuerelemente" im Client-Bereich liegen. In diesem Fall wird er jedoch in umgekehrter Reihenfolge ausgeführt, da die Blöcke geschlossen werden, beginnend mit dem am stärksten verschachtelten Block. Aber das ist nicht so wichtig. Praktisch dasselbe passiert, wenn Sie die herkömmliche Methode zur Erstellung von Dialogen verwenden: Die größeren Interface-Gruppen erzeugen die kleineren, und die letzteren wiederum erzeugen noch kleinere, bis hinunter auf die Ebene der einzelnen "Steuerelemente", und beginnen mit dem Hinzufügen der initialisierten Elemente in umgekehrter (aufsteigender) Reihenfolge: Zuerst werden "Steuerelemente" in die mittleren Blöcke und dann die mittleren in die größeren Blöcke eingefügt.

Für einen Dialog und für den Client-Bereich werden alle Parameter über die Konstruktor-Parameter übergeben (wie bei der Standard-Methode Create). Wir brauchen keine Größen an "Steuerelemente" zu übergeben, da die Klasse GridTkEx sie automatisch korrekt zuweist, während andere Parameter mit dem Operator <= übergeben werden.

Ein Block von 16 Schaltflächen wird ohne sichtbare Schleife initialisiert (er ist jetzt im Layout-Objekt verborgen). Die Hintergrundfarbe aller Schaltflächen wird durch die Zeichenfolge block["background"] <= clrCyan definiert. Dann werden Hilfsobjekte, die wir noch nicht kennen, an dasselbe Layoutobjekt (SimpleSequenceGenerator) übergeben.

Bei der Bildung einer Benutzeroberfläche ist es oft notwendig, mehrere Elemente desselben Typs zu erzeugen und diese im Batch-Modus mit einigen bekannten Daten auszufüllen. Zu diesem Zweck ist es zweckmäßig, den sogenannten Generator zu verwenden.

Generator ist eine Klasse mit der Methode, die in einer Schleife aufgerufen werden kann, um das nächste Element aus einer bestimmten Liste zu erhalten.

  template<typename T>
  class Generator
  {
    public:
      virtual T operator++() = 0;
  };

Normalerweise muss der Generator die Anzahl der benötigten Elemente kennen, und er speichert einen Cursor (Index des aktuellen Elements). Insbesondere wenn Sie die Sequenzen der Werte eines bestimmten eingebetteten Typs, wie Integer oder String, erstellen müssen, eignet sich die folgende einfache Implementierung von SimpleSequenceGenerator.

  template<typename T>
  class SimpleSequenceGenerator: public Generator<T>
  {
    protected:
      T current;
      int max;
      int count;
      
    public:
      SimpleSequenceGenerator(const T start = NULL, const int _max = 0): current(start), max(_max), count(0) {}
      
      virtual T operator++() override
      {
        ulong ul = (ulong)current;
        ul++;
        count++;
        if(count > max) return NULL;
        current = (T)ul;
        return current;
      }
  };

Generatoren werden für die Bequemlichkeit von Batch-Operationen hinzugefügt (Datei Generators.mqh), während es für Generatoren in der Layout-Klasse die Überschreibung des Operators <= gibt. Dies erlaubt es uns, 16 Buttons mit Bezeichnern und Beschriftungen in einer Zeile auszufüllen.

In den folgenden Strings der Methode CreateLayout wird der Button m_button_new erzeugt.

        _layout<CButton> start(m_button_new, "New");
        start["background;font"] <= clrYellow <= "Arial Black";

Die Zeichenfolge "New" ist sowohl eine Kennung als auch eine Überschrift. Wenn wir eine andere Bildunterschrift zuweisen möchten, könnten wir dies wie folgt tun: Beginnen Sie mit <= "Bildunterschrift". Im Allgemeinen ist es auch nicht notwendig, einen Bezeichner zu definieren (wenn wir ihn nicht brauchen). Er wird vom System selbst generiert.

In der zweiten Zeichenfolge wird der Kontext definiert, der zwei Tooltips auf einmal enthält: Hintergrund und Schriftart. Ersterer wird benötigt, um die Farbe clrYellow korrekt zu interpretieren. Da die Schaltfläche von CWndObj abgeleitet ist, bedeutet "unbenannte" Farbe die Textfarbe für sie. Der zweite Tooltip stellt sicher, dass die verwendete Schriftart durch die Zeichenfolge "Arial Black" geändert wird (ohne jeglichen Kontext würde die Zeichenfolge die Beschriftung ändern). Wenn Sie es wünschen, können Sie weitere Details angeben:

        start["background"] <= clrYellow;
        start["font"] <= "Arial Black";

Natürlich stehen die Methoden der Schaltfläche weiterhin zur Verfügung, d.h. wir können wie bisher schreiben:

        m_button_new.ColorBackground(clrYellow);
        m_button_new.Font("Arial Black");

Dazu benötigen wir jedoch das Objekt einer Schaltfläche, was nicht immer der Fall sein wird — später werden wir zu einem Schema kommen, bei dem das Layoutsystem für alles verantwortlich sein wird, einschließlich der Konstruktion und Speicherung Ihrer Elemente.

Um ein Label einzurichten, werden die folgenden Zeilen verwendet:

        _layout<CEdit> label(m_label);
        label <= "click new" <= true <= ALIGN_CENTER;

Hier wird das Objekt mit einer automatischen Kennung erstellt (wenn Sie das Fenster mit der Liste der Objekte im Diagramm öffnen, sehen Sie die eindeutige Nummer der Instanz). In der zweiten Zeile definieren wir den Beschriftungstext, das Attribut "schreibgeschützt" und die zentrierte Ausrichtung des Textes.

Dann folgen die Zeilen zur Anpassung des m_Hauptobjekts der Klasse CGridTKEx:

      m_main.Init(5, 4, 2, 2);
      m_main.SetGridConstraints(m_button_new, 4, 0, 1, 2);
      m_main.SetGridConstraints(m_label, 4, 2, 1, 2);
      m_main.Pack();

CGridTKEx ist das leicht verbesserte CGridTk (bekannt aus den vorhergehenden Artikeln). In CGridTkEx haben wir die Art und Weise der Definition von Einschränkungen für untergeordnete "Steuerelemente" mit der neuen Methode SetGridConstraints implementiert. In GridTk ist dies nur möglich, wenn gleichzeitig ein Element innerhalb der Methode Grid hinzugefügt wird. Dies ist von Natur aus schlecht, da es zwei grundsätzlich verschiedene Operationen innerhalb einer Methode vermischt: Herstellung von Beziehungen zwischen Objekten und Anpassung der Eigenschaften. Außerdem stellt sich heraus, dass Sie Add nicht zum Hinzufügen von Elementen zum Grid verwenden sollten, sondern nur diese Methode (da dies die einzige Möglichkeit ist, Einschränkungen zu definieren, ohne die GridTk nicht funktionieren kann). Dies verstößt gegen den allgemeinen Ansatz der Bibliothek, wo Add immer zu diesem Zweck verwendet wird. Und die Funktionsweise des automatischen Markup-Systems ist wiederum daran gebunden. In der Klasse CGridTkEx haben wir 2 Operationen getrennt — jetzt hat jede von ihnen ihre eigene Methode.

Es sei daran erinnert, dass es für die Hauptcontainer (einschließlich des gesamten Fensters) der Klassen CBox/CGridTk wichtig ist, die Methode Pack aufzurufen — es ist diese Methode, die das Layout durchführt und gegebenenfalls in den verschachtelten Containern Pack aufruft.

Wenn wir die Quellcodes von SlidingPuzzle3.mqh und SlidingPuzzle4.mqh vergleichen, werden wir leicht feststellen, dass der Quellcode wesentlich kompakter geworden ist. Die Methoden Create, CreateMain, CreateButton, CreateButtonNew und CreateLabel haben die Klasse "verlassen". Das einzige CreateLayout funktioniert jetzt anstelle von allen.

Nachdem wir das Programm gestartet haben, können wir sehen, dass die Elemente erstellt werden und wie erwartet funktionieren.

Nun, wir haben immer noch die Liste, in der alle "Steuerelemente" und Container in der Klasse deklariert sind. Da die Programme immer komplexer werden und die Anzahl der Komponenten zunimmt, wird es nicht sehr bequem sein, ihre Beschreibungen in der Fensterklasse und im Layout zu duplizieren. Könnte alles mit Hilfe des Layouts gemacht werden? Es ist leicht zu erraten, dass es das könnte. Dies wird jedoch im zweiten Teil besprochen.

Schlussfolgerungen

Dieser Text stellt die theoretischen Grundlagen und Ziele der Markup-Sprachen für grafische Oberflächen vor. Wir haben das Konzept der Implementierung einer Markup-Sprache in MQL entwickelt und die Kernklassen betrachtet, die diese Idee verkörpern. Aber es werden noch komplexere und konstruktivere Beispiele folgen.

Übersetzt aus dem Russischen von MetaQuotes Software Corp.
Originalartikel: https://www.mql5.com/ru/articles/7734

Beigefügte Dateien |
MQL5GUI1.zip (86.86 KB)
Zeitreihen in der Bibliothek DoEasy (Teil 38): Kollektion von Zeitreihen - Aktualisierungen in Echtzeit und Datenzugriff aus dem Programm Zeitreihen in der Bibliothek DoEasy (Teil 38): Kollektion von Zeitreihen - Aktualisierungen in Echtzeit und Datenzugriff aus dem Programm

Der Artikel befasst sich mit der Echtzeit-Aktualisierung von Zeitreihendaten und dem Senden von Meldungen über das Ereignis "New bar" an die Kontrollprogramm auf dem Chart aus allen Zeitreihen aller Symbole, um diese Ereignisse in benutzerdefinierten Programmen handhaben zu können. Die Klasse "New tick" wird verwendet, um die Notwendigkeit der Aktualisierung der Zeitreihen von Symbolen und Perioden zu bestimmen, die nicht dem aktuellen Chart entsprechen.

Die Handelssignale mehrerer Währungen überwachen (Teil 3): Einführung von Suchalgorithmen Die Handelssignale mehrerer Währungen überwachen (Teil 3): Einführung von Suchalgorithmen

Im vorherigen Artikel haben wir den visuellen Teil der Anwendung sowie die grundlegende Interaktion der GUI-Elementen entwickelt. Diesmal fügen wir die interne Logik und den Algorithmus der Vorbereitung der Handelssignaldaten hinzu, sowie die Fähigkeit, Signale einzurichten, sie zu durchsuchen und auf dem Monitor zu visualisieren.

Die Handelssignale mehrerer Währungen überwachen (Teil 4): Erweiterung der Funktionsweise und Verbesserung des Signalsuchsystems Die Handelssignale mehrerer Währungen überwachen (Teil 4): Erweiterung der Funktionsweise und Verbesserung des Signalsuchsystems

In diesem Teil erweitern wir das System zur Suche und Bearbeitung von Handelssignalen und führen die Möglichkeit ein, benutzerdefinierte Indikatoren zu verwenden und Programmlokalisierung hinzuzufügen. Zuvor haben wir ein Basissystem für die Suche nach Signalen geschaffen, das jedoch auf einem kleinen Satz von Indikatoren und einem einfachen Satz von Suchregeln basierte.

Zeitreihen in der Bibliothek DoEasy (Teil 39): Bibliotheksbasierte Indikatoren - Vorbereitung der Daten und Zeitreihen Zeitreihen in der Bibliothek DoEasy (Teil 39): Bibliotheksbasierte Indikatoren - Vorbereitung der Daten und Zeitreihen

Der Artikel befasst sich mit der Anwendung der DoEasy-Bibliothek zur Erstellung von Mehrsymbol- und Mehrperiodenindikatoren. Wir werden die Bibliotheksklassen auf die Arbeit mit Indikatoren vorbereiten und die Erstellung von Zeitreihen testen, die als Datenquellen in Indikatoren verwendet werden können. Wir werden auch das Erstellen und Versenden von Zeitreihen-Ereignissen implementieren.