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

29 Juli 2020, 09:58
Stanislav Korotky
0
88

In Teil 1 befassten wir uns mit den Grundprinzipien der Beschreibung des grafischen Oberflächen-Layouts von MQL-Programmen in MQL. Um sie zu implementieren, mussten wir einige Klassen erstellen, die direkt dafür verantwortlich sind, die Oberflächenelemente zu initialisieren, sie in einer gemeinsamen Hierarchie zu kombinieren und ihre Eigenschaften anzupassen. Nun gehen wir zu einigen komplizierteren Beispielen über und widmen uns, um nicht durch praktische Dinge abgelenkt zu werden, kurz der Bibliothek der Standardkomponenten, aus der wir unsere Beispiele aufbauen werden.

Anpassen der Standardsteuerungsbibliothek

Bei der Ausarbeitung der Fensterschnittstelle der früheren Artikel über OLAP, die ebenfalls auf der Standardbibliothek und den CBox-Containern basierte, mussten wir die Komponenten der Standardbibliothek korrigieren. Wie sich herausstellte, benötigte die Bibliothek Controls zur Integration des vorgeschlagenen Layoutsystems noch mehr Korrekturen — teils hinsichtlich der Erweiterung der Fähigkeiten und teils hinsichtlich der Fehlerkorrekturen. Aus diesem Grund beschlossen wir, die vollständige Kopie (den Versionszweig) aller Klassen zu erstellen, sie in den Ordner ControlsPlus zu legen und dann nur mit ihnen zu arbeiten.

Hier sind die wichtigsten Aktualisierungen.

Praktisch in allen Klassen wird die private Zugriffsebene für die geschützte geändert, um die Erweiterbarkeit der Bibliothek zu gewährleisten.

Um das Debuggen der Projekte zu erleichtern, die GUI-Elemente enthalten, wurde das String-Feld _rtti zur Klasse CWind hinzugefügt, und es wird im Konstruktor jeder abgeleiteten Klasse mit Hilfe des RTTI-Makros mit dem Namen einer bestimmten Klasse ausgefüllt.

  #define RTTI _rtti = StringFormat("%s %d", typename(this), &this);

Sie erlaubt es, im Debugger-Fenster eine reale Objektklasse zu sehen, die durch die Verknüpfung mit der Basisklasse dereferenziert wird (in diesem Fall zeigt der Debugger die Basisklasse an).

Informationen über die Felder und die Ausrichtung des Elements in der Klasse CWnd wurden mit zwei neuen überladenen Methoden zugänglich gemacht. Außerdem ist es nun möglich, Ausrichtung und Felder getrennt voneinander zu ändern.

    ENUM_WND_ALIGN_FLAGS Alignment(void) const
    {
      return (ENUM_WND_ALIGN_FLAGS)m_align_flags;
    }
    CRect Margins(void) const
    {
      CRectCreator rect(m_align_left, m_align_top, m_align_right, m_align_bottom);
      return rect;
    }
    void Alignment(const int flags)
    {
      m_align_flags = flags;
    }
    void Margins(const int left, const int top, const int right, const int bottom)
    {
      m_align_left = left;
      m_align_top = top;
      m_align_right = right;
      m_align_bottom = bottom;
    }

Methode CWnd::Align wurde in Übereinstimmung mit dem erwarteten Verhalten aller Ausrichtungsmodi außer Kraft gesetzt. Die Standardimplementierung gewährleistet keine Verschiebung zur Grenze des vordefinierten Feldes, wenn die Strecke definiert ist (beide Dimensionen sind dafür anfällig).

Die Methode DeleteAll wird der Klasse CWndContainer hinzugefügt, um beim Löschen eines Containers alle untergeordneten Elemente zu löschen. Sie wird von Delete(CWnd *control) aufgerufen, wenn der Zeiger auf das übergebene "Control" ein Container-Objekt enthält.

An verschiedenen Stellen der Klasse CWndClient haben wir Zeilen hinzugefügt, die die Sichtbarkeit von Bildlaufleisten regeln, die sich aufgrund von Größenänderungen ändern können.

Die Klasse CAppDialog berücksichtigt nun die instance_id des Fensters bei der Zuweisung von Bezeichnern an die Oberflächenelemente. Ohne diese Korrektur kommt es zu Konflikten (gegenseitige Beeinflussung) von Steuerelementen in verschiedenen Fenstern mit gleichem Namen.

In den Gruppen der Steuerelemente, d. h. CRadioGroup, CCheckGroup und CListView, wurde die Methode Redraw virtuell gemacht, damit die abgeleiteten "Gummi"-Klassen korrekt auf Größenänderungen reagieren können. Wir haben auch die Neuberechnung der Breite ihrer untergeordneten Elemente leicht korrigiert.

Für den gleichen Zweck wurde die virtuelle Methode OnResize den Klassen CDatePicker, CCheckBox und CRadioButton hinzugefügt. In der Klasse CDatePicker wurde der Fehler mit niedriger Priorität für den Pop-up-Kalender (Mausklicks wurden durch ihn hindurchgeleitet) behoben.

Die Methode CEdit::OnClick "frisst" Mausklicks nicht.

Außerdem hatten wir bereits zuvor einige Klassen von Steuerelementen entwickelt, die die Größenänderung unterstützten; und die Anzahl der "Gummi"-Klassen wurde im Rahmen dieses spezifischen Projekts erweitert. Ihre Dateien befinden sich im Ordner "Layouts".

  • ComboBoxResizable
  • SpinEditResizable
  • ListViewResizable
  • CheckGroupResizable
  • RadioGroupResizable

Es sollte daran erinnert werden, dass einige Steuerelemente, wie z.B. die Schaltfläche oder das Eingabefeld, ursprünglich ein Ausdehnen unterstützen.

Die allgemeine Struktur der Bibliothek der Standardelemente unter Berücksichtigung der angepassten Versionen, die die "Gummi"-Natur und Container von Drittanbietern unterstützen, ist im Klassendiagramm dargestellt.

Hierarchie der Bedienelemente

Hierarchie der Bedienelemente


Erzeugen und Zwischenspeichern von Elementen

Bislang wurden Elemente als automatische Instanzen innerhalb des Objektfensters konstruiert. Tatsächlich handelt es sich dabei um "Dummies", die dann durch Methoden wie Create initialisiert werden. Das Layoutsystem für GUI-Elemente kann diese Elemente unabhängig erstellen, anstatt sie aus dem Fenster zu holen. Dazu benötigen Sie lediglich einen Speicherplatz. Nennen wir ihn LayoutCache.

  template<typename C>
  class LayoutCache
  {
    protected:
      C *cache[];   // autocreated controls and boxes
      
    public:
      virtual void save(C *control)
      {
        const int n = ArraySize(cache);
        ArrayResize(cache, n + 1);
        cache[n] = control;
      }
      
      virtual C *get(const long m)
      {
        if(m < 0 || m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual C *get(const string name) = 0;
      virtual bool find(C *control);
      virtual int indexOf(C *control);
      virtual C *findParent(C *control) = 0;
      virtual bool revoke(C *control) = 0;
      virtual int cacheSize();
  };

Tatsächlich handelt es sich dabei um ein (für alle Elemente gemeinsames) Array der Zeiger der Basisklasse, wo sie mit der Methode "save" platziert werden können. In der Schnittstelle implementieren wir auch (wenn es auf dieser abstrakten Ebene möglich ist) oder deklarieren (zur weiteren Neudefinition) Methoden, um Elemente nach Nummer, Name, Link oder der Tatsache von "elterlichen" Beziehungen (Rückmeldung von verschachtelten Elementen an den Container) zu suchen.

Fügen wir den Cache als statisches Mitglied zur Klasse LayoutBase hinzu.

  template<typename P,typename C>
  class LayoutBase: public LayoutData
  {
    protected:
      ...
      static LayoutCache<C> *cacher;
      
    public:
      static void setCache(LayoutCache<C> *c)
      {
        cacher = c;
      }

Jedes Fenster muss für sich selbst eine Instanz seines Caches erstellen und diese mit setCache ganz am Anfang der Methode, wie z.B. CreateLayout, als Arbeitsinstanz festlegen. Da MQL-Programme Single-Threaded-Programme sind, ist garantiert, dass Fenster (falls mehr als eines benötigt wird) nicht parallel gebildet werden oder um dem "Cache"-Zeiger konkurrieren. Wir werden den Zeiger automatisch im Destruktor LayoutBase löschen; wenn der Stack fertig ist, bedeutet dies, dass wir den letzten externen Container in der Layoutbeschreibung belassen haben und es nicht nötig ist, noch etwas anderes zu speichern.

      ~LayoutBase()
      {
        ...
        if(stack.size() == 0)
        {
          cacher = NULL;
        }
      }

Das Zurücksetzen eines Links bedeutet nicht, dass wir den Cache löschen. Dies ist nur der Weg, um sicherzustellen, dass das potentielle nächste Layout dort nicht versehentlich die "Bedienelemente" eines anderen Fensters hinzufügt.

Um den Cache zu füllen, werden wir einen neuen Typ der Methode init zu LayoutBase hinzufügen — diesmal ohne einen Zeiger oder einen Link zu "Drittanbieter"-Elemente der GUI in den Parametern.

      // nonbound layout, control T is implicitly stored in internal cache
      template<typename T>
      T *init(const string name, const int m = 1, const int x1 = 0, const int y1 = 0, const int x2 = 0, const int y2 = 0)
      {
        T *temp = NULL;
        for(int i = 0; i < m; i++)
        {
          temp = new T();
          if(save(temp))
          {
            init(temp, name + (m > 1 ? (string)(i + 1) : ""), x1, y1, x2, y2);
          }
          else return NULL;
        }
        return temp;
      }
      
      virtual bool save(C *control)
      {
        if(cacher != NULL)
        {
          cacher.save(control);
          return true;
        }
        return false;
      }

Mit der Schablone können wir ein neues T schreiben und Objekte im Layout erzeugen (standardmäßig 1 Objekt pro Zeit, aber wir können auch optional mehrere machen).

Für die Standard-Bibliothekselemente haben wir eine spezielle Cache-Implementierung geschrieben, StdLayoutCache (sie ist hier verkürzt dargestellt, der vollständige Code ist hier angehängt).

  // CWnd implementation specific!
  class StdLayoutCache: public LayoutCache<CWnd>
  {
    public:
      ...
      virtual CWnd *get(const long m) override
      {
        if(m < 0)
        {
          for(int i = 0; i < ArraySize(cache); i++)
          {
            if(cache[i].Id() == -m) return cache[i];
            CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
            if(container != NULL)
            {
              for(int j = 0; j < container.ControlsTotal(); j++)
              {
                if(container.Control(j).Id() == -m) return container.Control(j);
              }
            }
          }
          return NULL;
        }
        else if(m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual CWnd *findParent(CWnd *control) override
      {
        for(int i = 0; i < ArraySize(cache); i++)
        {
          CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
          if(container != NULL)
          {
            for(int j = 0; j < container.ControlsTotal(); j++)
            {
              if(container.Control(j) == control)
              {
                return container;
              }
            }
          }
        }
        return NULL;
      }
      ...
  };

Beachten Sie, dass Methode get das Steuerelement entweder nach seiner Indexierungsnummer (wenn die Eingabe positiv ist) oder nach seinem Bezeichner (er ist mit einem Minuszeichen versehen) durchsucht. Unter Bezeichner ist hier eine eindeutige Nummer zu verstehen, die von der Standardkomponentenbibliothek für Versandereignisse vergeben wird. Bei Ereignissen wird sie im Parameter lparam übergeben.

In der Anwendungsklasse des Fensters können wir diese Klasse StdLayoutCache direkt verwenden oder eine von ihr abgeleitete schreiben.

Wie das Caching es erlaubt, die Beschreibung der Fensterklasse zu reduzieren, sehen wir im Beispiel unten. Bevor wir jedoch dazu übergehen, wollen wir einige zusätzliche Möglichkeiten betrachten, die der Cache eröffnet. Wir werden sie auch in unseren Beispielen verwenden.

Styler

Da der Cache ein Objekt ist, das Elemente auf zentralisierte Weise verarbeitet, liegt es nahe, ihn für die Lösung vieler anderer Aufgaben zu verwenden, die über das Layouten hinausgehen. Insbesondere für Elemente können wir mit den Regeln eines einzigen Stils, wie Farbe, Schriftart oder Einzüge, eine Vereinheitlichung erreichen. Gleichzeitig ist es ausreichend, diesen Stil an einer Stelle einzurichten und nicht für jedes Steuerelement separat dieselben Eigenschaften zu schreiben. Außerdem kann der Cache die Verarbeitung von Nachrichten für zwischengespeicherte Elemente übernehmen. Möglicherweise können wir absolut alle Elemente dynamisch konstruieren, zwischenspeichern und mit ihnen interagieren. Dann besteht überhaupt keine Notwendigkeit, irgendwelche "expliziten" Elemente zu deklarieren. Etwas später werden wir sehen, welchen offensichtlichen Vorteil die dynamisch erzeugten Elemente gegenüber automatisierten haben.

Um die zentralisierten Stile in der Klasse StdLayoutCache zu unterstützen, wird eine Pseudo-Methode bereitgestellt:

    virtual LayoutStyleable<C> *getStyler() const
    {
      return NULL;
    }

Wenn Sie keine Stile verwenden möchten, dann ist keine zusätzliche Codierung erforderlich. Wenn Sie jedoch die Vorteile einer Zentralisierung der Stilverwaltung erkennen, können Sie die Nachkommenklasse LayoutStyleable implementieren. Die Schnittstelle ist sehr einfach.

  enum STYLER_PHASE
  {
    STYLE_PHASE_BEFORE_INIT,
    STYLE_PHASE_AFTER_INIT
  };
  
  template<typename C>
  class LayoutStyleable
  {
    public:
      virtual void apply(C *control, const STYLER_PHASE phase) {};
  };

Die angewandte Methode wird für jedes Steuerelement zweimal aufgerufen: In der Initialisierungsphase (STYLE_PHASE_BEFORE_INIT) und in der Phase der Registrierung im Container (STYLE_PHASE_AFTER_INIT). In den Methoden LayoutBase::init wird also in der ersten Stufe ein Aufruf hinzugefügt:

      if(cacher != NULL)
      {
        LayoutStyleable<C> *styler = cacher.getStyler();
        if(styler != NULL)
        {
          styler.apply(object, STYLE_PHASE_BEFORE_INIT);
        }
      }

während wir in Destructor ähnliche Zeilen hinzufügen, aber mit STYLE_PHASE_AFTER_INIT für die zweite Stufe.

Zwei Phasen sind erforderlich, da die Stylingziele unterschiedlich sein können. Bei einigen Elementen ist es manchmal notwendig, individuelle Eigenschaften festzulegen, die eine höhere Priorität haben als die gemeinsamen, die im Styler festgelegt wurden. In der Initialisierungsphase ist das Steuerelement noch leer, d.h. es werden keine Einstellungen im Layout vorgenommen. In der Registrierungsphase sind darin bereits alle Eigenschaften eingestellt, und wir können den Stil auf der Grundlage dieser Eigenschaften zusätzlich ändern. Das offensichtlichste Beispiel ist das folgende. Alle Eingabefelder, die mit "schreibgeschützt" gekennzeichnet sind, sollten vorzugsweise grau dargestellt werden. Die Eigenschaft "schreibgeschützt" wird das Steuerelement jedoch erst beim Darstellen, nach der Initialisierung, zugewiesen. Daher passt die erste Stufe hier nicht, und die zweite Stufe ist erforderlich. Andererseits werden in der Regel nicht alle Felder dieses Flag haben; in allen anderen Fällen ist es notwendig, die Standardfarbe festzulegen, bevor die Layoutsprache die selektive Anpassung vornimmt.

Übrigens kann eine ähnliche Technologie bei der zentralen Lokalisierung der MQL-Programmoberflächen in verschiedene Sprachen verwendet werden.

Behandlung der Ereignisse

Die zweite Funktion, die dem Cache logisch zugeordnet werden muss, ist die Ereignisverarbeitung. Für sie wird eine Pseudo-Methode (C ist der Klassenvorlagenparameter) in der Klasse LayoutCache hinzugefügt:

    virtual bool onEvent(const int event, C *control)
    {
      return false;
    }

Auch hier können wir es in einer abgeleiteten Klasse implementieren, aber es ist nicht notwendig. Ereigniscodes werden durch die spezifische Bibliothek definiert.

Damit diese Methode funktioniert, benötigen wir Makrodefinitionen zum Abfangen von Ereignissen, die den in der Standardbibliothek verfügbaren Makrodefinitionen ähneln und wie folgt in die Karte geschrieben sind:

  EVENT_MAP_BEGIN(Dialog)
    ON_EVENT(ON_CLICK, m_button1, OnClickButton1)
    ...
  EVENT_MAP_END(AppDialog)

Neue Makros leiten die Ereignisse in das Cache-Objekt um. Hier ist eines von ihnen:

  #define ON_EVENT_LAYOUT_ARRAY(event, cache)  if(id == (event + CHARTEVENT_CUSTOM) && cache.onEvent(event, cache.get(-lparam))) { return true; }

Hier sehen wir die Suche im Cache nach einem Identifier, der in lparam (aber mit umgekehrtem Vorzeichen) kommt, wonach das gefundene Element an den oben besprochenen Ereignisbehandlung onEvent geschickt wird. Im Grunde können wir die Suche nach dem Element bei der Verarbeitung jedes Ereignisses auslassen und den Elementindex im Cache speichern und dann den spezifischen Prozessor mit dem Index verknüpfen.

Die aktuelle Cache-Größe ist der Index, für den das neue Element gerade gespeichert wurde. Wir können den Index der Steuerelemente speichern, die beim Layouten benötigt werden.

          _layout<CButton> button1("Button");
          button1index = cache.cacheSize() - 1;

Hier ist button1index eine ganzzahlige Variable in der Fensterklasse. Sie sollte in einem anderen Makro verwendet werden, das für die Verarbeitung von Elementen durch den Cache-Index definiert ist:

  #define ON_EVENT_LAYOUT_INDEX(event, cache, controlIndex, handler)  if(id == (event + CHARTEVENT_CUSTOM) && lparam == cache.get(controlIndex).Id()) { handler(); return(true); }

Außerdem können wir die Ereignisse direkt in die Elemente selbst und nicht in den Cache senden. Zu diesem Zweck muss das Element in sich selbst eine Schnittstelle Notifiable Template der erforderlichen Klasse Steuerelement implementieren.

  template<typename C>
  class Notifiable: public C
  {
    public:
      virtual bool onEvent(const int event, void *parent) = 0;
  };

Mit dem übergeordneten Parameter kann ein beliebiges Objekt übergeben werden, einschließlich eines Dialogfensters. Basierend auf Notifiable ist es zum Beispiel einfach, eine Schaltfläche zu erstellen, abgeleitet von CButton.

  class NotifiableButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        this.StateFlagsReset(7);
        return true;
      }
  };

Es gibt 2 Makros, um mit den Elementen "notifiable" zu arbeiten. Sie unterscheiden sich nur in der Anzahl der Parameter: ON_EVENT_LAYOUT_CTRL_ANY ermöglicht die Übergabe eines zufälligen Objekts an die letzten Parameter, während ON_EVENT_LAYOUT_CTRL_DLG diesen Parameter nicht besitzt, da es immer "this" des Dialogs als Objekt sendet.

  #define ON_EVENT_LAYOUT_CTRL_ANY(event, cache, type, anything)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, anything)) { return true; }}
  #define ON_EVENT_LAYOUT_CTRL_DLG(event, cache, type)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, &this)) { return true; }}

Im Zusammenhang mit dem zweiten Beispiel werden wir verschiedene Optionen für die Verarbeitung von Ereignissen betrachten.

Fall 2. Dialog mit Controls

Das Demoprojekt enthält die Klasse CControlsDialog mit den Haupttypen der Steuerelemente der Standardbibliothek. In ähnlicher Weise werden wir im ersten Fall alle Methoden zu deren Erstellung löschen und durch die einzige, CreateLayout, ersetzen. Übrigens gab es im alten Projekt bis zu 17 Methoden, und sie wurden mit zusammengesetzten Bedingungsoperatoren voneinander aufgerufen.

Um die Steuerelemente bei ihrer Generierung im Cache zu speichern, werden wir eine einfache Cache-Klasse und auch eine Styling-Klasse hinzufügen. Hier geht es zunächst um den Cache.

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      MyLayoutStyleable styler;
      CControlsDialog *parent;
      
    public:
      MyStdLayoutCache(CControlsDialog *owner): parent(owner) {}
      
      virtual StdLayoutStyleable *getStyler() const override
      {
        return (StdLayoutStyleable *)&styler;
      }
      
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          parent.SetCallbackText(__FUNCTION__ + " " + control.Name());
          return true;
        }
        return false;
      }
  };

In der Cache-Klasse wird der Ereignisprozessor, onEvent, deklariert, den wir über eine Event-Map verbinden werden. Hier sendet der Prozessor eine Nachricht an das Elternfenster, wo sie, wie in den vorhergehenden Fallversionen, im Informationsfeld angezeigt wird.

In der Styler-Klasse bieten wir die Einstellung identischer Felder für alle Elemente, eine nicht standardisierte Schriftart auf allen Schaltflächen und die Anzeige von CEdit mit dem Attribut "nur lesen" in Grau (wir haben nur eine solche, aber wenn eine weitere hinzugefügt wird, fällt sie automatisch unter die gemeinsame Einstellung).

  class MyLayoutStyleable: public StdLayoutStyleable
  {
    public:
      virtual void apply(CWnd *control, const STYLER_PHASE phase) override
      {
        CButton *button = dynamic_cast<CButton *>(control);
        if(button != NULL)
        {
          if(phase == STYLE_PHASE_BEFORE_INIT)
          {
            button.Font("Arial Black");
          }
        }
        else
        {
          CEdit *edit = dynamic_cast<CEdit *>(control);
          if(edit != NULL && edit.ReadOnly())
          {
            if(phase == STYLE_PHASE_AFTER_INIT)
            {
              edit.ColorBackground(clrLightGray);
            }
          }
        }
        
        if(phase == STYLE_PHASE_BEFORE_INIT)
        {
          control.Margins(DEFAULT_MARGIN);
        }
      }
  };

Der Link zum Cache wird im Fenster gespeichert; er wird im Konstruktor und im Destruktor erstellt bzw. gelöscht, wobei ein Link zum Fenster beim Erstellen als Parameter übergeben wird, um die Rückmeldung sicherzustellen.

  class CControlsDialog: public AppDialogResizable
  {
    private:
      ...
      MyStdLayoutCache *cache;
    public:
      CControlsDialog(void)
      {
        cache = new MyStdLayoutCache(&this);
      }

Betrachten wir nun die Methode CreateLayout in Stufen. Wenn man die detaillierten Beschreibungen liest, mag die Methode sehr lang und kompliziert erscheinen. Dies ist jedoch in der Tat nicht der Fall. Wenn die informativen Kommentare (die im realen Projekt nicht verwendet werden) entfernt werden, passt die Methode in einen Bildschirm, und sie enthält keine komplexe Logik.

Ganz zu Beginn wird der Cache durch den Aufruf von setCache aktiviert. Dann wird der Hauptcontainer, CControlsDialog, im ersten Block beschrieben. Er wird nicht im Cache sein, da wir den Link dem bereits erstellten "this" übergeben.

  bool CControlsDialog::CreateLayout(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    StdLayoutBase::setCache(cache); // assign the cache object to store implicit objects
    
    {
      _layout<CControlsDialog> dialog(this, name, x1, y1, x2, y2);

Dann wird eine implizite Instanz des geschachtelten Containers der Klasse CBox für den Client-Bereich des Fensters erzeugt. Er ist vertikal ausgerichtet, so dass die geschachtelten Container den Raum von oben nach unten ausfüllen. Wir speichern den Link auf das Objekt in der Variablen m_main, da wir bei einer Größenänderung des Fensters dessen Methode Pack aufrufen müssen. Wenn Ihr Dialog kein "Gummi"-Dialog ist, brauchen Sie das nicht zu tun. Schließlich werden für den Client-Bereich Null-Felder und die Ausrichtung in alle Richtungen festgelegt, damit das Panel das gesamte Fenster ausfüllt, auch bei einer Größenänderung.

      {
        // example of implicit object in the cache
        _layout<CBox> clientArea("main", ClientAreaWidth(), ClientAreaHeight(), LAYOUT_STYLE_VERTICAL);
        m_main = clientArea.get(); // we can get the pointer to the object from cache (if required)
        clientArea <= WND_ALIGN_CLIENT <= 0.0; // double type is important

Auf der nächsten Ebene folgt der Container als erster, der die gesamte Fensterbreite ausfüllt, aber nur wenig höher als das Eingabefeld ist. Außerdem wird er mit der Ausrichtung WND_ALIGN_TOP (zusammen mit WND_ALIGN_WIDTH) an den oberen Rand des Fensters "geklebt".

        {
          // another implicit container (we need no access it directly)
          _layout<CBox> editRow("editrow", ClientAreaWidth(), EDIT_HEIGHT * 1.5, (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH));

Darin befindet sich das einzige Steuerelement der Klasse CEdit im "Nur-Lesen"-Modus. Die explizite Variable m_edit ist für sie reserviert, so dass sie nicht in den Cache gelangt.

          {
            // for editboxes default boolean property is ReadOnly
            _layout<CEdit> edit(m_edit, "Edit", ClientAreaWidth(), EDIT_HEIGHT, true);
          }
        }

Zu diesem Zeitpunkt haben wir bereits 3 Elemente initialisiert. Nach der geschlossenen Klammer wird das Layout-Objekt "edit" zerstört und im Laufe der Ausführung seines Destruktors m_edit dem Container "editrow" hinzugefügt. Eine weitere schließende Klammer folgt jedoch unmittelbar darauf. Sie zerstört den Kontext, in dem das Layoutobjekt "editRow" "gelebt" hat. Dieser Container wird also wiederum dem Container des Client-Bereichs hinzugefügt, der auf dem Stapel verbleibt. Somit wird die erste Zeile für das vertikale Layout in m_main gebildet.

Dann haben wir eine Zeile mit drei Schaltflächen. Zunächst wird dafür ein Container erstellt.

        {
          _layout<CBox> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          buttonRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);

Hier sollten Sie die nicht standardmäßige Art der Ausrichtung von WND_ALIGN_CONTENT beachten. Sie bedeutet Folgendes.

Der Klasse CBox wird ein Algorithmus hinzugefügt, um die geschachtelten Elemente für die Containergröße zu skalieren. Er wird in der Methode AdjustFlexControls ausgeführt und tritt nur dann in Kraft, wenn in den Flags der Containerausrichtung ein räumlicher Wert von WND_ALIGN_CONTENT angegeben ist. Sie ist nicht Bestandteil der standardmäßigen Enumeration ENUM_WND_ALIGN_FLAGS. Der Container analysiert die Steuerelemente im Hinblick darauf, welche von ihnen eine feste Größe haben und welche nicht. Die Steuerelemente mit fester Größe sind solche, für die keine Ausrichtung durch die Containerseiten (in einer bestimmten Dimension) vorgegeben ist. Für alle diese Steuerelemente berechnet der Container die Summe ihrer Größen, subtrahiert sie von der Gesamtgröße des Containers und teilt den Rest anteilig auf die verbleibenden Steuerelemente auf. Wenn sich z. B. zwei Steuerelemente im Container befinden und keines von ihnen einen Datenfluss hat, dann gehen sie im gesamten Containerbereich zur Hälfte ineinander über.

Es ist ein sehr bequemer Modus, aber Sie sollten ihn nicht für einen Satz von verschachtelten Containern missbrauchen — aufgrund des Single-Pass-Algorithmus zur Berechnung der Größen werden interne Elemente über den Bereich des Containers ausgerichtet, der sich seinerseits an den Inhalt anpasst und Unsicherheit erzeugt (aus diesem Grund wird in Layout-Klassen ein spezielles Ereignis, ON_LAYOUT_REFRESH, durchgeführt, das das Fenster an sich selbst senden kann, um die Berechnung der Größen zu wiederholen).

Im Falle unserer Zeile mit drei Schaltflächen ändern alle proportional ihre Länge, wenn die Fensterbreite verändert wird. Die erste Schaltfläche der Klasse CButton wird implizit erzeugt und im Cache gespeichert.

          { // 1
            _layout<CButton> button1("Button1");
            button1index = cache.cacheSize() - 1;
            button1["width"] <= BUTTON_WIDTH;
            button1["height"] <= BUTTON_HEIGHT;
          } // 1

Die zweite Schaltfläche hat die Klasse NotifiableButton (sie wurde bereits oben beschrieben). Die Schaltfläche verarbeitet Nachrichten selbstständig.

          { // 2
            _layout<NotifiableButton> button2("Button2", BUTTON_WIDTH, BUTTON_HEIGHT);
          } // 2

Die dritte Schaltfläche wird auf der Grundlage der explizit definierten Fenster-Variablen m_button3 erstellt und hat die Eigenschaft "sticking" (klebrig).

          { // 3
            _layout<CButton> button3(m_button3, "Button3", BUTTON_WIDTH, BUTTON_HEIGHT, "Locked");
            button3 <= true; // for buttons default boolean property is Locking
          } // 3
        }

Bitte beachten Sie, dass alle Schaltflächen von eigenen Klammerblöcken umrahmt sind. Sie werden daher in der Reihenfolge in die Zeile eingefügt, in der schließende geschweifte Klammern erscheinen, die mit 1, 2 und 3 gekennzeichnet sind; d.h. in einer natürlichen Reihenfolge. Wir könnten darauf verzichten, diese "persönlichen" Blöcke für jede Schaltfläche zu machen und uns mit dem allgemeinen Block des Containers einschränken lassen. Aber dann würden die Schaltflächen in umgekehrter Reihenfolge hinzugefügt, denn die Destruktoren der Objekte werden immer in der umgekehrten Reihenfolge aufgerufen, in der sie erstellt wurden. Wir könnten die Situation "beheben", indem wir die Reihenfolge, in der die Schaltflächen im Layout beschrieben werden, umkehren.

In der dritten Reihe befindet sich ein Container mit den Steuerelementen, dem Spinner und dem Kalender. Der Container wird "anonym" erstellt und im Cache gespeichert.

        {
          _layout<CBox> spinDateRow("spindaterow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          spinDateRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);
          
          {
            _layout<SpinEditResizable> spin(m_spin_edit, "SpinEdit", GROUP_WIDTH, EDIT_HEIGHT);
            spin["min"] <= 10;
            spin["max"] <= 1000;
            spin["value"] <= 100; // can set value only after limits (this is how SpinEdits work)
          }
          
          {
            _layout<CDatePicker> date(m_date, "Date", GROUP_WIDTH, EDIT_HEIGHT, TimeCurrent());
          }
        }

Der letzte Container schließlich füllt den gesamten verbleibenden Bereich des Fensters aus und enthält zwei Spalten mit Elementen. Helle Farben sind ausschließlich dazu bestimmt, deutlich zu zeigen, welcher Container sich wo im Fenster befindet.

        {
          _layout<CBox> listRow("listsrow", ClientAreaWidth(), LIST_HEIGHT);
          listRow["top"] <= (int)(EDIT_HEIGHT * 1.5 * 3);
          listRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_CLIENT);
          (listRow <= clrMagenta)["border"] <= clrBlue;
          
          createSubList(&m_lists_column1, LIST_OF_OPTIONS);
          createSubList(&m_lists_column2, LIST_LISTVIEW);
          // or vice versa (changed order gives swapped left/right side location)
          // createSubList(&m_lists_column1, LIST_LISTVIEW);
          // createSubList(&m_lists_column2, LIST_OF_OPTIONS);
        }

Hierbei ist besonders zu beachten, dass zwei Spalten, m_lists_column1 und m_lists_column2, nicht in der Methode CreateLayout selbst, sondern mit der Hilfsmethode createSubList ausgefüllt werden. Vom Layout her unterscheidet sich der Aufruf der Funktion nicht von der Eingabe in den nächsten Klammerblock. Das bedeutet, dass das Layout nicht notwendigerweise aus einer langen statischen Liste besteht, aber es kann Fragmente enthalten, die durch eine Bedingung modifiziert werden. Oder Sie können dasselbe Fragment in verschiedene Dialoge aufnehmen.

In unserem Fall können wir die Reihenfolge der Spalten im Fenster ändern, indem wir den zweiten Parameter der Funktion ändern.

      }
    }

Wenn alle Klammern geschlossen werden, werden alle GUI-Elemente initialisiert und miteinander verbunden. Wir rufen die Methode Pack auf (direkt oder über SelfAdjustment, wo sie auch als Antwort auf die Anforderung eines "Gummi"-Dialogs aufgerufen wird).

    // m_main.Pack();
    SelfAdjustment();
    return true;
  }

Wir werden nicht auf Einzelheiten der Methode createSubList eingehen. Im Inneren sind die Möglichkeiten implementiert, die es erlauben, einen Satz von 3 Steuerelemente (Combo-Box, Gruppe von Optionen und Gruppe von Radialspalten) oder eine Liste (ListView) zu erzeugen, die alle in Form von "Gummi" ausgeführt sind. Interessant ist, dass die Steuerelemente mit Hilfe einer anderen Klasse von Generatoren, ItemGenerator, gefüllt werden.

  template<typename T>
  class ItemGenerator
  {
    public:
      virtual bool addItemTo(T *object) = 0;
  };

Die einzige Methode dieser Klasse wird vom Layout für das Objekt Steuerelement aufgerufen, bis die Methode false (ein Vorzeichen des Datenendes) zurückgibt.

Standardmäßig werden einige einfache Generatoren für die Standardbibliothek bereitgestellt (sie verwenden die Methode der Steuerelemente, AddItem): StdItemGenerator, StdGroupItemGenerator, SymbolsItemGenerator und ArrayItemGenerator. Insbesondere erlaubt SymbolsItemGenerator, das Steuerelement mit den Symbolen von Market Watch zu füllen.

  template<typename T>
  class SymbolsItemGenerator: public ItemGenerator<T>
  {
    protected:
      long index;
      
    public:
      SymbolsItemGenerator(): index(0) {}
      
      virtual bool addItemTo(T *object) override
      {
        object.AddItem(SymbolName((int)index, true), index);
        index++;
        return index < SymbolsTotal(true);
      }
  };

Im Layout wird sie auf die gleiche Weise spezifiziert, wie die Generatoren von "Kontrollen". Alternativ ist es erlaubt, dem Layout-Objekt den Link zu einem Zeiger auf das dynamisch verteilte Objekt des Generators zu übergeben, anstatt auf ein automatisiertes oder statisches Objekt (das irgendwo früher im Code beschrieben werden muss).

        _layout<ListViewResizable> list(m_list_view, "ListView", GROUP_WIDTH, LIST_HEIGHT);
        list <= WND_ALIGN_CLIENT < new SymbolsItemGenerator<ListViewResizable>();

Zu diesem Zweck wird der Operator < verwendet. Dynamisch verteilter Generator wird nach Abschluss der Arbeit automatisch gelöscht.

Um neue Ereignisse zu verbinden, werden der Karte die entsprechenden Makros hinzugefügt.

  EVENT_MAP_BEGIN(CControlsDialog)
    ...
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, NotifiableButton)
    ON_EVENT_LAYOUT_INDEX(ON_CLICK, cache, button1index, OnClickButton1)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
  EVENT_MAP_END(AppDialogResizable)

Das Makro ON_EVENT_LAYOUT_CTRL_DLG verbindet Benachrichtigungen bei Mausklicks für beliebige Schaltflächen der Klasse NotifiableButton (in unserem Fall ist es eine einzige). Das Makro ON_EVENT_LAYOUT_INDEX sendet das gleiche Ereignis an die Schaltfläche mit dem angegebenen Index im Cache. Wir könnten jedoch auf das Schreiben dieses Makros verzichten, da das Makro ON_EVENT_LAYOUT_ARRAY mit der letzten Zeichenfolge den Mausklick an jedes Element im Cache sendet, vorausgesetzt, seine Kennung fällt mit lparam zusammen.

Grundsätzlich könnten alle Elemente an den Cache übergeben werden, und ihre Ereignisse könnten auf eine neue Art und Weise verarbeitet werden; das alte funktioniert jedoch auch, und sie können kombiniert werden.

Im Folgenden animierten Bild ist die Reaktion auf die Ereignisse dargestellt.

Mit der MQL-Markup-Sprache gestalteter Dialog mit Steuerelementen

Mit der MQL-Markup-Sprache gestalteter Dialog mit Steuerelementen

Bitte beachten Sie, dass die Art und Weise, wie ein Ereignis übersetzt wird, indirekt an der Signatur der im Informationsfeld angezeigten Funktion zu erkennen ist. Sie können auch sehen, dass die Ereignisse sowohl in den "Controls" als auch in den Containern vorkommen. Rote Rahmen von Containern werden zur Fehlersuche angezeigt, und Sie können sie mit dem Makro LAYOUT_BOX_DEBUG deaktivieren.

Fall 3. Dynamische Layouts von DynamicForm

In diesem letzten Beispiel betrachten wir die Form, in der alle Elemente dynamisch im Cache erstellt werden. Dies wird uns einige neue wichtige Möglichkeiten eröffnen.

Wie im vorherigen Fall wird der Cache das Styling der Elemente unterstützen. Die einzige Einstellung des Stils sind identische Unterscheidungsfelder, die es erlauben, die Verschachtelung von Containern zu sehen und sie, falls gewünscht, mit der Maus auszuwählen.

Die folgende einfache Schnittstellenstruktur wird innerhalb der Methode CreateLayout beschrieben. Wie üblich füllt der Hauptcontainer den gesamten Client-Bereich des Fensters aus. Im oberen Teil befindet sich ein Block mit zwei Schaltflächen: "Inject" und "Export". Der gesamte Raum darunter wird mit dem in die linke und rechte Spalte unterteilten Container ausgefüllt. Die grau markierte linke Spalte ist ursprünglich leer. In der rechten Spalte befindet sich eine Gruppe von Radiobuttons, die die Auswahl des Steuerungstyps ermöglicht.

      {
        // example of implicit object in the cache
        _layout<CBoxV> clientArea("main", ClientAreaWidth(), ClientAreaHeight());
        m_main = clientArea.get();
        clientArea <= WND_ALIGN_CLIENT <= PackedRect(10, 10, 10, 10);
        clientArea["background"] <= clrYellow <= VERTICAL_ALIGN_TOP;
        
        {
          _layout<CBoxH> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 5);
          buttonRow <= 5.0 <= (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH);
          buttonRow["background"] <= clrCyan;
          
          {
            // these 2 buttons will be rendered in reverse order (destruction order)
            // NB: automatic variable m_button3
            _layout<CButton> button3(m_button3, "Export", BUTTON_WIDTH, BUTTON_HEIGHT);
            _layout<NotifiableButton> button2("Inject", BUTTON_WIDTH, BUTTON_HEIGHT);
          }
        }
        
        {
          _layout<CBoxH> buttonRow("buttonrow2", ClientAreaWidth(), ClientAreaHeight(),
            (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_CONTENT|WND_ALIGN_CLIENT));
          buttonRow["top"] <= BUTTON_HEIGHT * 5;
          
          {
            {
              _layout<CBoxV> column("column1", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
              column <= clrGray;
              {
                // dynamically created controls will be injected here
              }
            }
            
            {
              _layout<CBoxH> column("column2", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
            
              _layout<RadioGroupResizable> selector("selector", GROUP_WIDTH, CHECK_HEIGHT);
              selector <= WND_ALIGN_HEIGHT;
              string types[3] = {"Button", "CheckBox", "Edit"};
              ArrayItemGenerator<RadioGroupResizable,string> ctrls(types);
              selector <= ctrls;
            }
          }
        }
      }

Es wird angenommen, dass der Nutzer, nachdem er den Elementtyp in einer Radiogruppe ausgewählt hat, auf die Schaltfläche "Inject" drückt und das entsprechende Steuerelement im linken Teil des Fensters erstellt wird. Sie können natürlich auch mehrere verschiedene Steuerelemente nacheinander erstellen. Diese werden automatisch entsprechend den Containereinstellungen zentriert. Um diese Logik zu implementieren, hat die Schaltfläche Inject die Klasse NotifiableButton mit dem Prozessor onEvent.

  class NotifiableButton: public Notifiable<CButton>
  {
      static int count;
      
      StdLayoutBase *getPtr(const int value)
      {
        switch(value)
        {
          case 0:
            return new _layout<CButton>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 1:
            return new _layout<CCheckBox>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 2:
            return new _layout<CEdit>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
        }
        return NULL;
      }
      
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        DynamicForm *parent = dynamic_cast<DynamicForm *>(anything);
        MyStdLayoutCache *cache = parent.getCache();
        StdLayoutBase::setCache(cache);
        CBox *box = cache.get("column1");
        if(box != NULL)
        {
          // put target box to the stack by retrieving it from the cache
          _layout<CBox> injectionPanel(box, box.Name());
          
          {
            CRadioGroup *selector = cache.get("selector");
            if(selector != NULL)
            {
              const int value = (int)selector.Value();
              if(value != -1)
              {
                AutoPtr<StdLayoutBase> base(getPtr(value));
                (~base).get().Id(rand() + (rand() << 32));
              }
            }
          }
          box.Pack();
        }
        
        return true;
      }
  };

Der Container, in den neue Elemente eingefügt werden sollen, wird zunächst im Cache unter dem Namen "column1" gesucht. Dieser Container wird als erster Parameter bei der Erstellung des Objektes injectionPanel verwendet. Die Tatsache, dass sich das zu übergebende Element bereits im Cache befindet, wird im Layout-Algorithmus speziell berücksichtigt — es wird nicht erneut zwischengespeichert, sondern üblicherweise in den Container-Stapel gelegt. Dies ermöglicht das Hinzufügen von Elementen zu "alten" Containern.

Basierend auf der Wahl des Nutzers wird ein Objekt des gewünschten Typs mit dem Operator "new" in der Hilfsmethode getPtr erzeugt. Damit die hinzugefügten Steuerelemente korrekt funktionieren, werden für sie nach dem Zufallsprinzip eindeutige Bezeichner generiert. Spezielle Klasse, AutoPtr stellt sicher, dass der Zeiger beim Verlassen des Codeblocks gelöscht wird.

Wenn zu viele Elemente hinzugefügt werden, gehen sie über die Containergrenzen hinaus. Dies geschieht, weil unsere verfügbaren Container-Klassen noch nicht gelernt haben, wie sie auf einen Überlauf reagieren sollen. In diesem Fall könnten wir z.B. die Bildlaufleiste einblenden, während die Elemente jenseits der Grenzen ausgeblendet werden könnten.

Dies ist jedoch nicht wichtig. In diesem Fall geht es darum, dass wir durch die Einrichtung des Formulars dynamische Inhalte erzeugen und die erforderlichen Inhalte und Größen der Container sicherstellen können.

Neben dem Hinzufügen von Elementen kann dieser Dialog auch deren Löschung bewirken. Jedes Element im Formular kann durch einen Mausklick ausgewählt werden. Gleichzeitig werden die Klasse und der Name des Elements protokolliert, während das Element selbst mit einem roten Rahmen hervorgehoben wird. Wenn Sie auf ein bereits ausgewähltes Element klicken, erscheint im Dialog die Aufforderung, die Löschung zu bestätigen und, falls sie bestätigt wird, das Element zu löschen. All dies ist in unserer Cache-Klasse implementiert.

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      DynamicForm *parent;
      CWnd *selected;
      
      bool highlight(CWnd *control, const color clr)
      {
        CWndObj *obj = dynamic_cast<CWndObj *>(control);
        if(obj != NULL)
        {
          obj.ColorBorder(clr);
          return true;
        }
        else
        {
          CWndClient *client = dynamic_cast<CWndClient *>(control);
          if(client != NULL)
          {
            client.ColorBorder(clr);
            return true;
          }
        }
        return false;
      }
      
    public:
      MyStdLayoutCache(DynamicForm *owner): parent(owner) {}
      
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          highlight(selected, CONTROLS_BUTTON_COLOR_BORDER);
          
          CWnd *element = control;
          if(!find(element)) // this is an auxiliary object, not a compound control
          {
            element = findParent(control); // get actual GUI element
          }
          
          if(element == NULL)
          {
            Print("Can't find GUI element for ", control._rtti + " / " + control.Name());
            return true;
          }
          
          if(selected == control)
          {
            if(MessageBox("Delete " + element._rtti + " / " + element.Name() + "?", "Confirm", MB_OKCANCEL) == IDOK)
            {
              CWndContainer *container;
              container = dynamic_cast<CWndContainer *>(findParent(element));
              if(container)
              {
                revoke(element); // deep remove of all references (with subtree) from cache
                container.Delete(element); // delete all subtree of wnd-objects
                
                CBox *box = dynamic_cast<CBox *>(container);
                if(box) box.Pack();
              }
              selected = NULL;
              return true;
            }
          }
          selected = control;
          
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", b);
          
          return true;
        }
        return false;
      }
  };

Wir können jedes Schnittstellenelement löschen, das im Cache verfügbar ist, d.h. nicht nur diejenigen, die durch die Schaltfläche Einfügen hinzugefügt wurden. Auf diese Weise können Sie z.B. die gesamte linke Hälfte oder die rechte "radiobox" löschen. Am interessantesten wird es, wenn wir versuchen, den oberen Container mit zwei Schaltflächen zu löschen. Das hat zur Folge, dass die Schaltfläche Exportieren nicht mehr an den Dialog gebunden ist und im Diagramm verbleibt.

Editierbares Formular: Hinzufügen und Löschen von Elementen

Editierbares Formular: Hinzufügen und Löschen von Elementen

Dies geschieht, da es das einzige Element ist, das absichtlich als automatische und nicht als dynamische Variable beschrieben wird (in der Formularklasse gibt es eine Instanz von CButton, m_button3).

Wenn die Standardbibliothek versucht, Schnittstellenelemente zu löschen, delegiert sie dies an die Array-Klasse CArrayObj, die wiederum den Zeigertyp prüft und nur Objekte mit POINTER_DYNAMIC löscht. Damit wird deutlich, dass zur Konstruktion einer adaptiven Schnittstelle, bei der Elemente sich gegenseitig ersetzen oder vollständig gelöscht werden können, eine dynamische Platzierung wünschenswert ist, und der Cache bietet hierfür eine fertige Lösung.

Abschließend sei auf die zweite Schaltfläche des Dialogs, Export, verwiesen. Wie wir an ihrem Namen erkennen können, ist er dazu gedacht, den aktuellen Zustand des Dialogs als Textdatei in der besprochenen MQL-Layout-Syntax zu speichern. Das Formular erlaubt natürlich nur in begrenztem Umfang die Einstellung seines Erscheinungsbildes. Aber die Möglichkeit selbst, das Erscheinungsbild in den fertigen MQL-Code zu exportieren, den man dann einfach in das Programm kopieren kann, um die gleiche Oberfläche zu erhalten, stellt potentiell eine recht wertvolle Technologie dar. Natürlich wird nur die Schnittstelle exportiert, während Sie den Ereignisverarbeitungscode oder allgemeine Einstellungen unabhängig davon aktivieren müssen.

Das Exportieren wird durch die Klasse LayoutExporter sichergestellt; wir werden dies nicht in allen Einzelheiten berücksichtigen, und die Quellcodes sind beigefügt.

Schlussfolgerungen

In diesem Artikel haben wir die Implementierbarkeit des Konzepts zur Beschreibung des grafischen Oberflächen-Layouts von MQL-Programmen im MQL selbst überprüft. Die Verwendung der dynamischen Generierung von Elementen mit der zentralen Speicherung im Cache ermöglicht es, die Erstellung von und Kontrolle über die Hierarchie der Komponenten zu erleichtern. Basierend auf dem Cache können Sie die meisten Aufgaben im Zusammenhang mit dem Design der Schnittstelle implementieren, insbesondere das einheitliche Restyling, die Ereignisverarbeitung, die On-the-fly-Bearbeitung des Layouts und die Speicherung in einem für die spätere Verwendung geeigneten Format.

Wenn wir diese Funktionen zusammenfügen, wird sich herausstellen, dass praktisch alles für einen einfachen visuellen Formular-Editor zur Verfügung steht. Er könnte nur die wichtigsten Eigenschaften unterstützen, die für viele Steuerelemente üblich sind, aber dennoch würde er die Bildung von Schnittstellenvorlagen ermöglichen. Wir können jedoch feststellen, dass bereits die Anfangsphase der Beurteilung dieses neuen Konzepts viel Arbeit gekostet hat. Daher stellt die praktische Umsetzung des neuen Editors ein recht komplexes Problem dar. Aber das ist eine andere Geschichte.

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

Beigefügte Dateien |
MQL5GUI2.zip (98.72 KB)
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.

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.

Kontinuierliche Walk-Forward-Optimierung (Teil 6): Logikteil und die Struktur des Auto-Optimizers Kontinuierliche Walk-Forward-Optimierung (Teil 6): Logikteil und die Struktur des Auto-Optimizers

Wir haben bereits früher die Schaffung einer automatischen Walk-Forward-Optimierung in Betracht gezogen. Dieses Mal werden wir zur internen Struktur des Auto-Optimizers übergehen. Der Artikel wird für all diejenigen nützlich sein, die mit dem erstellten Projekt weiterarbeiten und es modifizieren möchten, sowie für diejenigen, die die Programmlogik verstehen möchten. Der aktuelle Artikel enthält UML-Diagramme, die die interne Struktur des Projekts und die Beziehungen zwischen den Objekten darstellen. Er beschreibt auch den Prozess des Optimierungsstarts, enthält jedoch keine Beschreibung des Implementierungsprozesses des Optimizers.

Zeitreihen in der Bibliothek DoEasy (Teil 40): Bibliotheksbasierte Indikatoren - Aktualisierung der Daten in Echtzeit Zeitreihen in der Bibliothek DoEasy (Teil 40): Bibliotheksbasierte Indikatoren - Aktualisierung der Daten in Echtzeit

Der Artikel befasst sich mit der Entwicklung eines einfachen Mehrperiodenindikators auf der Grundlage der DoEasy-Bibliothek. Wir verbessern die Klasse der Zeitreihen so, dass sie Daten aus beliebigen Zeitrahmen empfangen können, um sie in der aktuellen Diagrammperiode anzuzeigen.