MQL als Darstellungsmittel für graphische Schnittstellen von MQL-Programmen (Teil 3). Formular-Designer

7 September 2020, 10:40
Stanislav Korotky
0
173

In den ersten beiden Artikeln (1 und 2) befassten wir uns mit dem allgemeinen Konzept des Aufbaus des Systems eines Interface-Markups in MQL und der Implementierung von Basisklassen, die die hierarchische Initialisierung der Interface-Elemente, deren Caching, Styling, Einrichtung ihrer Eigenschaften und die Verarbeitung der Ereignisse darstellen. Die dynamische Erstellung der Elemente auf Anforderung ermöglichte die sofortige Änderung des einfachen Dialog-Layouts, während die Verfügbarkeit eines einzigen Speichers bereits erstellter Elemente es routinemäßig ermöglichte, diese in der vorgeschlagenen MQL-Syntax zu erstellen, um sie anschließend "so wie sie ist" in das MQL-Programm einzufügen, wo eine GUI erforderlich ist. Auf diese Weise sind wir an die Erstellung eines grafischen Formular-Editors herangegangen. Dieser Aufgabe werden wir uns in diesem Artikel intensiv widmen.

Problemstellung

Der Editor muss die Anordnung der Elemente im Fenster und die Anpassung ihrer grundlegenden Eigenschaften sicherstellen. Nachstehend finden Sie eine allgemeine Liste der unterstützten Eigenschaften, aber nicht alle Eigenschaften sind für alle Elementtypen verfügbar.

  • Typ,
  • Name,
  • Breite,
  • Höhe,
  • Stil der internen Inhaltsausrichtung,
  • Text oder Überschrift,
  • Hintergrundfarbe,
  • Ausrichtung im übergeordneten Container, und
  • Offsets/Felder der Containergrenzen.

Viele andere Eigenschaften sind hier nicht enthalten, wie z.B. der Name und die Größe der Schriftart oder die spezifischen Eigenschaften verschiedener Arten von Steuerelementen (controls) (insbesondere die Eigenschaft von "klebenden" Schaltflächen). Dies geschieht absichtlich, um das Projekt, das im Wesentlichen auf den Proof of Concept (POC) abzielt, zu vereinfachen. Falls erforderlich, kann die Unterstützung für zusätzliche Eigenschaften später im Editor hinzugefügt werden.

Die Positionierung in absoluten Koordinaten ist indirekt über Offsets möglich, wird aber nicht empfohlen. Die Verwendung des Containers CBox legt nahe, dass die Positionierung gemäß den Ausrichtungseinstellungen automatisch von den Containern selbst vorgenommen werden sollte.

Der Editor ist für die Klassen der Schnittstellenelemente der Standardbibliothek konzipiert. Um ähnliche Werkzeuge für andere Bibliotheken zu erstellen, müssen Sie die spezifischen Implementierungen aller abstrakten Entitäten aus dem vorgeschlagenen Markup-System schreiben. Gleichzeitig sollten Sie sich von der Implementierung der Markup-Klassen für die Standardbibliothek leiten lassen.

Es ist zu beachten, dass die Definition der "Bibliothek der Standardkomponenten" sachlich nicht korrekt ist, da wir sie im Zusammenhang mit unseren vorangegangenen Artikeln erheblich modifizieren und in den parallelen Versionszweig im Ordner ControlsPlus legen mussten. Hier werden wir sie weiterhin verwenden und modifizieren.

Listen wir die Arten von Elementen auf, die vom Editor unterstützt werden sollen.

  • Container CBox mit horizontaler (CBoxH) und vertikaler (CBoxV) Ausrichtung,
  • CButton,
  • CEdit Eingabefeld,
  • CLabel,
  • SpinEditResizable,
  • CDatePicker Kalender,
  • Dropdown-Liste ComboBoxResizable,
  • Liste ListViewResizable,
  • CheckGroupResizable, und
  • RadioGroupResizable.

Alle Klassen gewährleisten eine adaptive Größenanpassung (einige Standardtypen konnten das am Anfang, während wir bei den anderen erhebliche Änderungen vornehmen mussten).

Das Programm wird aus zwei Fenstern bestehen: Dialog Inspektor, in dem der Nutzer die erforderlichen Eigenschaften der zu erstellenden Bedienelemente auswählt, und Formular Designer, in dem diese Elemente erstellt werden und das Erscheinungsbild der zu gestaltenden grafischen Oberfläche bilden.

Skizze der Benutzeroberfläche des GUI-MQL-Designer-Programms

Skizze der Benutzeroberfläche des GUI-MQL-Designer-Programms

In Bezug auf MQL wird das Programm 2 grundlegende Klassen haben, InspectorDialog und DesignerForm, die in den Header-Dateien der jeweiligen Namen beschrieben sind.

  #include "InspectorDialog.mqh"
  #include "DesignerForm.mqh"
  
  InspectorDialog inspector;
  DesignerForm designer;
  
  int OnInit()
  {
      if(!inspector.CreateLayout(0, "Inspector", 0, 20, 20, 200, 400)) return (INIT_FAILED);
      if(!inspector.Run()) return (INIT_FAILED);
      if(!designer.CreateLayout(0, "Designer", 0, 300, 50, 500, 300)) return (INIT_FAILED);
      if(!designer.Run()) return (INIT_FAILED);
      return (INIT_SUCCEEDED);
  }

Beide Fenster sind abgeleitet von AppDialogResizable (im Folgenden CAppDialog), die durch die MQL-Markup-Technologie gebildet werden. Daher sehen wir den Aufruf von CreateLayout anstelle von Create.

Jedes Fenster hat seinen eigenen Cache der Oberflächenelemente. Im Inspektor ist er jedoch von Anfang an mit Steuerelementen gefüllt, die in einem recht komplexen Layout beschrieben werden (wir werden versuchen, das in allgemeinen Begriffen zu erklären), während er im Designer leer ist. Es ist leicht zu erklären: Praktisch die gesamte Geschäftslogik des Programms ist im Inspektor gespeichert, während der Designer ein Dummy ist, in den der Inspektor nach und nach neue Elemente durch die Befehle des Benutzers implementiert.

PropertySet

Jede der oben aufgeführten Eigenschaften wird durch den Wert eines bestimmten Typs dargestellt. Beispielsweise ist Elementname eine Zeichenfolge, während Breite und Höhe ganze Zahlen sind. Der vollständige Satz von Werten beschreibt vollständig das Objekt, das im Designer erscheinen muss. Es ist sinnvoll, den Satz an einer Stelle zu speichern. Zu diesem Zweck wurde eine spezielle Klasse, PropertySet, eingeführt. Aber welche Mitglieds-Variablen müssen darin enthalten sein?

Auf den ersten Blick scheint die Verwendung der Variablen von einfachen eingebetteten Typen eine offensichtliche Lösung zu sein. Ihnen fehlt jedoch ein wichtiges Merkmal, das weiter benötigt wird. MQL unterstützt keine Verknüpfungen zu einfachen Variablen. Gleichzeitig ist die Verknüpfung eine sehr wichtige Sache in den Algorithmen zur Verarbeitung einer Benutzerschnittstelle. Daraus ergeben sich oft komplexe Reaktionen auf Änderungen von Werten. Zum Beispiel muss ein in eines der Felder eingegebener Wert, der außerhalb des zulässigen Bereichs liegt, einige abhängige Steuerelemente blockieren. Es wäre praktisch, wenn diese Steuerelemente ihre eigenen Zustände steuern könnten, geführt von einer einzigen Stelle, die den zu prüfenden Wert speichert. Und das geht am einfachsten mit der "Verteilung" von Verweisen auf die gleiche Variable. Statt einfacher eingebetteter Typen werden wir daher eine Template-Wrapper-Klasse verwenden, die ungefähr wie folgt aussieht und vorläufig den Namen Value trägt.

  template<typename V>
  class Value
  {
    protected:
      V value;
      
    public:
      V operator~(void) const // getter
      {
        return value;
      }
      
      void operator=(V v)     // setter
      {
        value = v;
      }
  };

Das Wort "ungefähr" wird aus gutem Grund verwendet. Tatsächlich werden der Klasse einige weitere Funktionen hinzugefügt, die weiter unten betrachtet werden.

Die Verfügbarkeit eines Objekt-Wrappers erlaubt es, die Zuweisung neuer Werte in dem überladenen Operator '=' abzufangen, was bei der Verwendung einfacher Typen unmöglich ist. Und wir werden sie brauchen.

Unter Berücksichtigung dieser Klasse kann der Satz der Eigenschaften des neuen Schnittstellenobjekts ungefähr wie folgt beschrieben werden.

  class PropertySet
  {
    public:
      Value<string> name;
      Value<int> type;
      Value<int> width;
      Value<int> height;
      Value<int> style; // VERTICAL_ALIGN / HORIZONTAL_ALIGN / ENUM_ALIGN_MODE
      Value<string> text;
      Value<color> clr;
      Value<int> align; // ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT
      Value<ushort> margins[4];
  };

Im Dialog des Inspektors werden wir eine Variable dieser Klasse als zentrale Speicherung der aktuellen Einstellungen einführen, die über die Inspektor-Steuerelemente eingegeben wurden.

Natürlich wird im Inspektor ein geeignetes Steuerelement verwendet, um jede Eigenschaft zu definieren. Um beispielsweise den Typ des zu erstellenden Steuerelements auszuwählen, wird eine Dropdown-Liste, CComboBox, verwendet, während das Eingabefeld CEdit für den Namen verwendet wird. Die Eigenschaft repräsentiert den Einzelwert eines Typs, wie z. B. Zeile, Zahl oder Index in einer Liste. Auch zusammengesetzte Eigenschaften, wie z.B. Offsets, die für jede der 4 Seiten separat definiert werden, sollten unabhängig voneinander betrachtet werden (links, oben usw.), da 4 Eingabefelder für ihre Eingabe reserviert werden und daher jeder Wert mit einem ihm zugeordneten Steuerelement verbunden ist.

Formulieren wir also eine offensichtliche Regel für den Inspektor-Dialog — jedes Steuerelement in ihm definiert die Eigenschaft, die mit ihm verbunden ist und immer einen bestimmten Wert eines bestimmten Typs hat. Dies führt uns zu der folgenden architektonischen Lösung.

Charakteristische Eigenschaften von Steuerelementen

In unseren vorhergehenden Artikeln haben wir eine spezielle Schnittstelle, Notifiable, eingeführt, mit der die Ereignisverarbeitung für eine bestimmte Steuerung definiert werden konnte.

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

Hier steht das C für eine Klasse der Steuerelemente, wie z.B. CEdit, CSpinEdit, usw. Die Funktion onEvent wird vom Layout-Cache automatisch für die relevanten Elemente und Ereignistypen aufgerufen. Dies geschieht natürlich nur unter der Voraussetzung, dass korrekte Textwerte in die Ereignisliste eingefügt worden sind. Zum Beispiel wurde im vorhergehenden Teil die Verarbeitung der Klicks auf die Inject-Schaltfläche nach diesem Prinzip angepasst (sie wurde als Abkömmling von Notifiable<CButton> beschrieben).

Wenn ein Steuerelement zur Anpassung der Eigenschaften eines vordefinierten Typs verwendet wird, ist es verlockend, eine spezialisiertere Schnittstelle, PlainTypeNotifiable, zu erstellen.

  template<typename C, typename V>
  class PlainTypeNotifiable: public Notifiable<C>
  {
    public:
      virtual V value() = 0;
  };

Der Methodenwert ist dafür vorgesehen, von einem C-Element den Wert vom Typ V zurückzugeben, der für C am charakteristischsten ist. Für die Klasse CEdit beispielsweise sieht die Rückgabe eines Wertes vom Typ String ganz natürlich aus (in einer bestimmten hypothetischen Klasse ExtendedEdit).

  class ExtendedEdit: public PlainTypeNotifiable<CEdit, string>
  {
    public:
      virtual string value() override
      {
        return Text();
      }
  };

Für jede Art von Steuerelementen gibt es einen einzigen charakteristischen Datentyp oder einen begrenzten Bereich davon (z. B. können Sie für ganze Zahlen die Genauigkeit von short, int oder long wählen). Für alle Steuerelemente steht die eine oder andere Methode zur Werteabfrage bereit, um den Wert in der überladbaren Methode value bereitzustellen.

Damit sind wir an den Punkt der architektonischen Lösung gekommen — Harmonisierung der Klassen Value und PlainTypeNotifiable. Sie wird unter Verwendung der abgeleiteten Klasse PlainTypeNotifiable implementiert, die den Wert des Steuerelements vom Inspektor in die mit ihm verknüpfte Value-Eigenschaft verschiebt.

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      Value<V> *property;
      
    public:
      void bind(Value<V> *prop)
      {
        property = prop;     // pointer assignment
        property = value();  // overloaded operator assignment for value of type V
      }
      
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CHANGE || event == ON_END_EDIT)
        {
          property = value();
          return true;
        }
        return false;
      };
  };

Aufgrund der Ableitung von der Vorlagenklasse PlainTypeNotifiable repräsentiert die neue Klasse NotifiableProperty sowohl die C-Klasse des Steuerelements als auch einen Anbieter der V-Typ-Werte.

Die Methode bind ermöglicht es, innerhalb des Steuerelements eine Verknüpfung zu Value beizubehalten und dann als Reaktion auf die Operationen des Benutzers mit dem Steuerelement den Eigenschaftswert (durch Verweis) automatisch an Ort und Stelle zu ändern.

Beispielsweise wurde für die Eingabefelder vom Typ String die EditProperty eingeführt, ähnlich wie die Instanzen von ExtendedEdit, jedoch abgeleitet von NotifiableProperty:

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      virtual string value() override
      {
        return Text(); // Text() is a standard method of CEdit
      }
  };

Für eine Dropdown-Liste beschreibt eine ähnliche Klasse die Eigenschaft mit einem ganzzahligen Wert.

  class ComboBoxProperty: public NotifiableProperty<ComboBoxResizable,int>
  {
    public:
      virtual int value() override
      {
        return (int)Value(); // Value() is a standard method of CComboBox
      }
  };

Die Klassen der Steuerelemente einer Eigenschaft werden im Programm für alle Grundtypen von Elementen beschrieben.

Diagramm der Klassen der Notifiable Properties.

Diagramm der Klassen der Notifiable Properties.

Jetzt ist es an der Zeit, das Attribut "ungefähr" zu entfernen und die Klassen ganz kennen zu lernen.

StdWert: Wert, Überwachung und Abhängigkeiten

Eine Standardsituation wurde oben bereits erwähnt, in der es notwendig ist, die Änderung einiger Steuerelemente zu überwachen, um die Gültigkeit und die Änderungen der Zustände anderer Steuerelemente zu überprüfen. Mit anderen Worten, wir brauchen einen Beobachter, der in der Lage ist, ein Steuerelement zu überwachen und andere beteiligte Steuerelemente über Änderungen in diesem Steuerelement zu informieren.

Zu diesem Zweck wurde die Schnittstelle StateMonitor (Beobachter) eingeführt.

  class StateMonitor
  {
    public:
      virtual void notify(void *sender) = 0;
  };

Die Methode notify ist dafür vorgesehen, von der Quelle von Änderungen aufgerufen zu werden, damit dieser Beobachter gegebenenfalls reagieren kann. Die Quelle der Änderungen kann durch den Parameter sender identifiziert werden. Natürlich muss die Quelle der Änderungen vorläufig irgendwie wissen, dass ein bestimmter Beobachter an einer Benachrichtigung interessiert ist. Zu diesem Zweck muss die Quelle die Schnittstelle Publisher implementieren.

  class Publisher
  {
    public:
      virtual void subscribe(StateMonitor *ptr) = 0;
      virtual void unsubscribe(StateMonitor *ptr) = 0;
  };

Mit der Methode subscribe kann der Beobachter den Link zu sich selbst an den Publisher weitergeben. Wie leicht zu erraten ist, werden die Quellen der Änderungen für uns Eigenschaften sein, und daher wird die hypothetische Klasse Value tatsächlich vom Publisher geerbt und erscheint wie folgt.

  template<typename V>
  class ValuePublisher: public Publisher
  {
    protected:
      V value;
      StateMonitor *dependencies[];
      
    public:
      V operator~(void) const
      {
        return value;
      }
      
      void operator=(V v)
      {
        value = v;
        for(int i = 0; i < ArraySize(dependencies); i++)
        {
          dependencies[i].notify(&this);
        }
      }
      
      virtual void subscribe(StateMonitor *ptr) override
      {
        const int n = ArraySize(dependencies);
        ArrayResize(dependencies, n + 1);
        dependencies[n] = ptr;
      }
      ...
  };

Jeder registrierte Beobachter gelangt zu dependencies und wird, falls sich der Wert ändert, durch Aufruf seiner Methode notify benachrichtigt.

Da Eigenschaften eindeutig mit den Steuerelementen verbunden sind, mit denen sie eingeführt werden, werden wir für die Speicherung eines Links zum Steuerelement in der letzten Klasse von Eigenschaften für die Standardbibliothek sorgen, d. h. StdValue (er verwendet den Grundtyp aller Steuerelemente CWind).

  template<typename V>
  class StdValue: public ValuePublisher<V>
  {
    protected:
      CWnd *provider;
      
    public:
      void bind(CWnd *ptr)
      {
        provider = ptr;
      }
      
      CWnd *backlink() const
      {
        return provider;
      }
  };

Dieser Link wird später nützlich sein.

Dies sind die Instanzen von StdValue, die PropertySet füllen.

StdValue-Kommunikationsdiagramm

StdValue-Kommunikationsdiagramm

In der oben erwähnten Klasse NotifiableProperty wird in der Realität auch StdValue verwendet, und in der Methode bind binden wir den Eigenschaftswert an das Steuerelement (this).

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      StdValue<V> *property;
    public:
      void bind(StdValue<V> *prop)
      {
        property = prop;
        property.bind(&this);        // +
        property = value();
      }
      ...
  };

Automatische Verwaltung der Zustände der Steuerelemente — EnableStateMonitor

Die wichtigste Art und Weise, auf Änderungen in einigen Einstellungen zu reagieren, ist das Blockieren/Entblockieren anderer abhängiger Steuerelemente. Der Zustand jedes dieser adaptiven Steuerelements kann von mehreren Einstellungen abhängen (nicht nur von einer). Um sie zu überwachen, wurde eine spezielle abstrakte Klasse, EnableStateMonitorBase, entwickelt.

  template<typename C>
  class EnableStateMonitorBase: public StateMonitor
  {
    protected:
      Publisher *sources[];
      C *control;
      
    public:
      EnableStateMonitorBase(): control(NULL) {}
      
      virtual void attach(C *c)
      {
        control = c;
        for(int i = 0; i < ArraySize(sources); i++)
        {
          if(control)
          {
            sources[i].subscribe(&this);
          }
          else
          {
            sources[i].unsubscribe(&this);
          }
        }
      }
      
      virtual bool isEnabled(void) = 0;
  };

Das Steuerelement, dessen Zustand von einem bestimmten Beobachter überwacht wird, wird in das Feld control eingetragen. Das Array sources enthält die Quellen von Änderungen, die den Zustand beeinflussen. Das Array muss in den abgeleiteten Klassen gefüllt werden. Wenn wir den Beobachter durch Aufruf von attach mit einem bestimmten Steuerelement verbinden, abonniert der Beobachter alle Quellen von Änderungen. Dann beginnt er, über Änderungen in den Quellen durch Aufruf seiner Methode notify benachrichtigt zu werden.

Ob ein Steuerelement blockiert oder aktiviert werden soll, entscheidet die Methode isEnabled, aber sie wird hier als abstrakt deklariert und in den abgeleiteten Klassen implementiert.

Für die Klassen der Standardbibliothek ist ein Mechanismus bekannt, der die Steuerelemente sowohl mit Enabled als auch mit Disable aktiviert bzw. deaktiviert. Verwenden wir sie, um die spezifische Klasse EnableStateMonitor zu implementieren.

  class EnableStateMonitor: public EnableStateMonitorBase<CWnd>
  {
    public:
      EnableStateMonitor() {}
      
      void notify(void *sender) override
      {
        if(control)
        {
          if(isEnabled())
          {
            control.Enable();
          }
          else
          {
            control.Disable();
          }
        }
      }
  };

In der Praxis wird dieser Kurs in dem Programm häufig verwendet werden, aber wir werden nur ein Beispiel betrachten. Um neue Objekte zu erstellen oder die geänderten Eigenschaften im Designer zu verwenden, gibt es die Schaltfläche Anwenden im Dialogfeld des Inspektors (dafür ist die Klasse ApplyButton, abgeleitet von Notifiable<CButton>, definiert).

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          ...
        }
      };
  };

Wenn der Objektname nicht definiert oder sein Typ nicht ausgewählt ist, muss die Schaltfläche blockiert werden. Deshalb implementieren wir ApplyButtonStateMonitor mit zwei Änderungsquellen ("Publisher"): Name und Typ.

  class ApplyButtonStateMonitor: public EnableStateMonitor
  {
    // what's required to detect Apply button state
    const int NAME;
    const int TYPE;
    
    public:
      ApplyButtonStateMonitor(StdValue<string> *n, StdValue<int> *t): NAME(0), TYPE(1)
      {
        ArrayResize(sources, 2);
        sources[NAME] = n;
        sources[TYPE] = t;
      }
      
      virtual bool isEnabled(void) override
      {
        StdValue<string> *name = sources[NAME];
        StdValue<int> *type = sources[TYPE];
        return StringLen(~name) > 0 && ~type != -1 && ~name != "Client";
      }
  };

Der Klassenkonstruktor nimmt zwei Parameter, die auf die relevanten Eigenschaften zeigen. Sie werden im Array sources gespeichert. Mit der Methode isEnabled wird geprüft, ob der Name ausgefüllt ist und ob der Typ ausgewählt ist (ob er nicht -1 ist). Wenn die Bedingungen erfüllt sind, kann der Knopf gedrückt werden. Zusätzlich wird der Name auf eine spezielle Zeichenfolge, Client, geprüft, die in den Dialogen der Standardbibliothek für den Client-Bereich reserviert ist und daher nicht im Namen von Benutzerelementen erscheinen kann.

In der Dialogklasse des Inspektors gibt es eine Variable vom Typ ApplyButtonStateMonitor, die im Konstruktor durch Links zu den Objekten StdValue initialisiert wird, die den Namen und den Typ speichern.

  class InspectorDialog: public AppDialogResizable
  {
    private:
      PropertySet props;
      ApplyButtonStateMonitor *applyMonitor;
    public:
      InspectorDialog::InspectorDialog(void)
      {
        ...
        applyMonitor = new ApplyButtonStateMonitor(&props.name, &props.type);
      }

Im Dialoglayout sind die Eigenschaften des Namens und des Typs an die entsprechenden Steuerelemente gebunden, während der Beobachter an die Schaltfläche Anwenden gebunden ist.

          ...
          _layout<EditProperty> edit("NameEdit", BUTTON_WIDTH, BUTTON_HEIGHT, "");
          edit.attach(&props.name);
          ...
          _layout<ComboBoxProperty> combo("TypeCombo", BUTTON_WIDTH, BUTTON_HEIGHT);
          combo.attach(&props.type);
          ...
          _layout<ApplyButton> button1("Apply", BUTTON_WIDTH, BUTTON_HEIGHT);
          button1["enable"] <= false;
          applyMonitor.attach(button1.get());

Die Methode attach im Objekt applyMonitor ist uns bereits bekannt, während attach in den _Layout-Objekten etwas Neues ist. Die Klasse _layout wurde in unserem zweiten Artikel ausführlich behandelt, und die Methode attach ist die einzige Änderung gegenüber dieser Version. Diese Zwischenmethode ruft lediglich bind für das Steuerelement auf, das durch das _Layout-Objekt innerhalb des Inspektordialogs erzeugt wird.

  template<typename T>
  class _layout: public StdLayoutBase
  {
      ...
      template<typename V>
      void attach(StdValue<V> *v)
      {
        ((T *)object).bind(v);
      }
      ...
  };

Es sei daran erinnert, dass alle Steuerelemente der Eigenschaften, einschließlich EditProperty und ComboBoxProperty, wie in diesem Beispiel, die abgeleiteten Klasse NotifiableProperty sind, in der es die Methode bind gibt, um die Steuerelemente an die StdValue-Variablen zu binden, die die relevanten Eigenschaften speichern. So stellen sich die Steuerelemente im Inspektorfenster als mit den relevanten Eigenschaften gebunden heraus, während die letzteren wiederum vom Beobachter ApplyButtonStateMonitor überwacht werden. Sobald der Nutzer den Wert eines der beiden Felder ändert, wird er in PropertySet angezeigt (erinnern Sie sich an den onEvent für die Ereignisse ON_CHANGE und ON_END_EDIT in NotifiableProperty) und benachrichtigt die registrierten Beobachter, einschließlich ApplyButtonStateMonitor. Dies führt dazu, dass der Status der Schaltfläche für den aktuellen Status der Schaltfläche automatisch geändert wird.

Wir werden im Inspektor-Dialog mehrere Monitore benötigen, die den Zustand der Steuerelemente auf ähnliche Weise überwachen. Wir werden die spezifischen Regeln der Blockierung in einem Abschnitt des Benutzerhandbuchs beschreiben.

Die Klassen StateMonitor

Die Klassen StateMonitor

Nun, benennen wir die endgültige Relevanz aller Eigenschaften des zu erstellenden Objekts und der Steuerelemente im Inspektordialog.

  • name — EditProperty, Zeichenfolge;
  • type — ComboBoxProperty, Ganzzahl, Typnummer aus der Liste der unterstützten Elemente;
  • width — SpinEditPropertySize, Ganzzahl, Pixel;
  • height — SpinEditPropertySize, Ganzzahl, Pixel;
  • style — ComboBoxProperty, Ganzzahl, Ganzzahl, die dem Wert einer der Enumeration entspricht (abhängig vom Elementtyp): VERTICAL_ALIGN (CBoxV), HORIZONTAL_ALIGN (CBoxH) und ENUM_ALIGN_MODE (CEdit);
  • text — EditProperty, Zeichenfolge;
  • background color — ComboBoxColorProperty, Farbwert aus der Liste;
  • boundary alignment — AlignCheckGroupProperty, Bitmasken, Gruppe unabhängiger Flags (ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT); und
  • indents — vier SpinEditPropertyShort, ganzzahlig;

Der Name der Klassen einiger Eigenschaftselemente weist auf ihre Spezialisierung hin, d.h. auf die erweiterte Funktionalität im Vergleich zur Basisimplementierung, die durch "einfache" SpinEditProperty, ComboBoxProperty, CheckGroupProperty usw. angeboten wird. Wozu sie verwendet werden, wird aus dem Nutzerhandbuch deutlich.

Um diese Steuerelemente genau und klar darzustellen, enthält das Dialog-Markup sicherlich zusätzliche Container und Datenbeschriftungen. Der vollständige Code ist im Anhang zu diesem Handbuch zu finden.

Behandlung der Ereignisse

Die Behandlung der Ereignisse für alle Steuerelemente ist in der Ereignisliste definiert:

  EVENT_MAP_BEGIN(InspectorDialog)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_END_EDIT, cache, EditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditPropertyShort)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxColorProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, AlignCheckGroupProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, ApplyButton)
    ...
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache) // default (stub)
  EVENT_MAP_END(AppDialogResizable)

Um die Effizienz der Behandlung der Ereignisse im Cache zu erhöhen, wurden einige besondere Schritte unternommen. Die Makros ON_EVENT_LAYOUT_CTRL_ANY und ON_EVENT_LAYOUT_CTRL_DLG, die in unserem zweiten Artikel vorgestellt wurden, basieren auf der Suche nach Steuerelementen im Cache-Array durch eine eindeutige Nummer, die vom System im Parameter lparam erhalten wird. Gleichzeitig führt die grundlegende Cache-Implementierung eine lineare Suche durch das Array durch.

Um den Prozess zu beschleunigen, wurde der Klasse MyStdLayoutCache (abgeleitet von StdLayoutCache) die Methode buildIndex hinzugefügt, von der eine Instanz in Inspektor gespeichert und verwendet wird. Die darin implementierte bequeme Indexierungsfunktion basiert auf der besonderen Eigenschaft der Standardbibliothek, allen Elementen eindeutige Nummern zuzuweisen. In der Methode CAppDialog::Run eine Zufallszahl, d.h. die uns bereits bekannte m_instance_id, von der ausgehend alle vom Fenster erzeugten Diagrammobjekte nummeriert werden. Auf diese Weise können wir den Bereich der erhaltenen Werte in Erfahrung bringen. Wenn man m_instance_id abzieht, wird jeder Wert von lparam, der mit einem Ereignis einhergeht, zur direkten Nummer des Objekts. Das Programm erzeugt jedoch viel mehr Objekte im Diagramm als die im Cache gespeicherten, da viele Steuerelemente (und das Fenster selbst als Aggregation des Rahmens, der Kopfzeile, des Minimierungsknopfes usw.) aus mehreren Low-Level-Objekten bestehen. Daher stimmt der Index im Cache nie mit der Objektkennung minus m_instance_id überein. Daher mussten wir ein spezielles Index-Array verwenden (dessen Größe der Anzahl der Objekte im Fenster entspricht) und irgendwie die fortlaufenden Nummern dieser "echten" im Cache verfügbaren Steuerelemente einschreiben. Infolgedessen erfolgt der Zugriff praktisch sofort, nach dem Prinzip der indirekten Adressierung.

Das Array sollte erst gefüllt werden, nachdem die grundlegende Implementierung von CAppDialog::Run eindeutige Nummern zugewiesen hat, aber bevor OnInit seine Arbeit beendet. Zu diesem Zweck besteht die beste Lösung darin, die Methode Run virtuell zu machen (in der Standardbibliothek ist sie nicht so) und sie z.B. im InspectorDialog wie folgt zu überschreiben.

  bool InspectorDialog::Run(void)
  {
    bool result = AppDialogResizable::Run();
    if(result)
    {
      cache.buildIndex();
    }
    return result;
  }

Die Methode buildIndex selbst ist recht einfach.

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      InspectorDialog *parent;
      // fast access
      int index[];
      int start;
      
    public:
      MyStdLayoutCache(InspectorDialog *owner): parent(owner) {}
      
      void buildIndex()
      {
        start = parent.GetInstanceId();
        int stop = 0;
        for(int i = 0; i < cacheSize(); i++)
        {
          int id = (int)get(i).Id();
          if(id > stop) stop = id;
        }
        
        ArrayResize(index, stop - start + 1);
        ArrayInitialize(index, -1);
        for(int i = 0; i < cacheSize(); i++)
        {
          CWnd *wnd = get(i);
          index[(int)(wnd.Id() - start)] = i;
        }
      ...
  };

Jetzt können wir eine schnelle Implementierung der Methode schreiben, die Steuerelemente nach Nummern zu suchen.

      virtual CWnd *get(const long m) override
      {
        if(m < 0 && ArraySize(index) > 0)
        {
          int offset = (int)(-m - start);
          if(offset >= 0 && offset < ArraySize(index))
          {
            return StdLayoutCache::get(index[offset]);
          }
        }
        
        return StdLayoutCache::get(m);
      }

Aber genug von der internen Struktur des Inspektors.

So sieht sein Fenster im laufenden Programm aus.

Dialog-Inspektor und Formular-Designer

Dialog-Inspektor und Formular-Designer

Neben den Eigenschaften können wir hier auch einige unbekannte Elemente sehen. Sie alle werden später beschrieben. Werfen wir nun einen Blick auf die Schaltfläche Apply (anwenden). Nachdem der Nutzer die Werte für die Eigenschaften festgelegt hat, kann das gewünschte Objekt durch Drücken dieser Schaltfläche im Designer-Formular erzeugt werden. Da die Schaltfläche eine von Notifiable abgeleitete Klasse hat, kann er die Klicks in seiner eigenen Methode onEvent verarbeiten.

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          Properties p = inspector.getProperties().flatten();
          designer.inject(p);
          ChartRedraw();
          return true;
        }
        return false;
      };
  };

Es sei daran erinnert, dass die Variablen Inspektor und Designer globale Objekte mit dem Inspektor-Dialog und dem Designer-Formular sind. In seiner Programmoberfläche verfügt der Inspektor über die Methode getProperties, um den aktuellen Satz von Eigenschaften, PropertySet, wie oben beschrieben, bereitzustellen:

    PropertySet *getProperties(void) const
    {
      return (PropertySet *)&props;
    }

PropertySet kann sich selbst in eine flache (normale) Struktur, Properties, packen, um sie an die Methode des Designers inject zu übergeben. Hier wechseln wir zum Fenster des Designers.

Designer

Abgesehen von zusätzlichen Überprüfungen ist das Wesen der Methode inject ähnlich dem, was wir am Ende unseres zweiten Artikels gesehen haben: Form platziert den Zielcontainer in den Layout-Stapel (er wurde im zweiten Artikel statisch gesetzt, d.h. er war immer derselbe) und erzeugt ein Element mit den übergebenen Eigenschaften darin. Im neuen Formular können alle Elemente per Mausklick ausgewählt werden, wodurch sich der Einfügekontext ändert. Außerdem löst ein solcher Klick die Übertragung der Eigenschaften des ausgewählten Elements in den Inspektor aus. Auf diese Weise erscheint die Möglichkeit, die Eigenschaften bereits erstellter Objekte zu bearbeiten und sie mit der gleichen Schaltfläche Anwenden zu aktualisieren. Der Designer erkennt, ob der Nutzer ein neues Element einfügen oder ein bestehendes bearbeiten möchte, indem er den Namen und den Typ des Elements vergleicht. Wenn eine solche Kombination bereits im Designer-Cache vorhanden ist, dann bedeutet dies Bearbeiten.

So sieht im Allgemeinen das Hinzufügen eines neuen Elements aus.

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        ...
      }
      else
      {
        CBox *box = dynamic_cast<CBox *>(cache.getSelected());
        
        if(box == NULL) box = cache.findParent(cache.getSelected());
        
        if(box)
        {
          CWnd *added;
          StdLayoutBase::setCache(cache);
          {
            _layout<CBox> injectionPanel(box, box.Name());
            
            {
              AutoPtr<StdLayoutBase> base(getPtr(props));
              added = (~base).get();
              added.Id(rand() + ((long)rand() << 32));
            }
          }
          box.Pack();
          cache.select(added);
        }
      }

Die Variable cache wird in DesignerForm beschrieben und enthält ein Objekt der Klasse DefaultStdLayoutCache, das von StdLayoutCache abgeleitet ist (vorgestellt in unseren vorangegangenen Artikeln). StdLayoutCache ermöglicht das Auffinden des Objekts anhand des Namens mit der Methode get. Wenn es nicht existiert, bedeutet dies, dass es ein neues Objekt gibt und der Designer versucht, den vom Nutzer ausgewählten aktuellen Container zu finden. Zu diesem Zweck ist die Methode getSelected in der neuen Klasse DefaultStdLayoutCache implementiert. Wie genau die Auswahl durchgeführt wird, werden wir etwas später sehen. Es ist hier wichtig zu beachten, dass ein Ort zur Implementierung des neuen Elements nur ein Container sein kann (in unserem Fall werden CBox-Container verwendet). Wenn zu einem bestimmten Zeitpunkt kein Container ausgewählt wurde, ruft der Algorithmus findParent auf, um den Elterncontainer zu erkennen und ihn als Ziel zu verwenden. Wenn der Ort der Einfügung definiert ist, beginnt ein konventionelles Markup-Schema mit verschachtelten Blöcken zu funktionieren. Im externen Block wird das Objekt _layout mit dem Zielcontainer erstellt, und dann wird im Inneren ein Objekt in Zeichenfolge erzeugt:

  AutoPtr<StdLayoutBase> base(getPtr(props));

Alle Eigenschaften werden an die Hilfsmethode getPtr übergeben. Sie kann die Objekte aller unterstützten Typen erzeugen, aber der Einfachheit halber werden wir nur zeigen, wie sie für einige davon aussieht.

    StdLayoutBase *getPtr(const Properties &props)
    {
      switch(props.type)
      {
        case _BoxH:
          {
            _layout<CBoxH> *temp = applyProperties(new _layout<CBoxH>(props.name, props.width, props.height), props);
            temp <= (HORIZONTAL_ALIGN)props.style;
            return temp;
          }
        case _Button:
          return applyProperties(new _layout<CButton>(props.name, props.width, props.height), props);
        case _Edit:
          {
            _layout<CEdit> *temp = applyProperties(new _layout<CEdit>(props.name, props.width, props.height), props);
            temp <= (ENUM_ALIGN_MODE)LayoutConverters::style2textAlign(props.style);
            return temp;
          }
        case _SpinEdit:
          {
            _layout<SpinEditResizable> *temp = applyProperties(new _layout<SpinEditResizable>(props.name, props.width, props.height), props);
            temp["min"] <= 0;
            temp["max"] <= DUMMY_ITEM_NUMBER;
            temp["value"] <= 1 <= 0;
            return temp;
          }
        ...
      }
    }

Die Vorlagen der Objekte _Layout werden vom vordefinierten Typ des GUI-Elements mittels Konstruktoren erstellt, die uns aus den statischen Beschreibungen von MQL-Markups bekannt sind. Die Objekte _Layout ermöglichen die Verwendung von überladenen Operatoren <= zur Definition von Eigenschaften, insbesondere wird auf diese Weise der Stil HORIZONTAL_ALIGN für CBoxH, ENUM_ALIGN_MODE für ein Textfeld oder Spinner-Bereiche gefüllt. Einstellungen einiger anderer allgemeiner Eigenschaften, wie Einzüge, Text und Farbe, werden an die Hilfsmethode applyProperties delegiert (weitere Einzelheiten dazu finden Sie in den Quelltexten).

    template<typename T>
    T *applyProperties(T *ptr, const Properties &props)
    {
      static const string sides[4] = {"left", "top", "right", "bottom"};
      for(int i = 0; i < 4; i++)
      {
        ptr[sides[i]] <= (int)props.margins[i];
      }
      
      if(StringLen(props.text))
      {
        ptr <= props.text;
      }
      else
      {
        ptr <= props.name;
      }
      ...
      return ptr;
    }

Wenn das Objekt namentlich im Cache gefunden wird, geschieht Folgendes (in vereinfachter Form):

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        CWnd *sel = cache.getSelected();
        if(ptr == sel)
        {
          update(ptr, props);
          Rebound(Rect());
        }
      }
      ...
    }

Die Hilfsmethode update überträgt die Eigenschaften von der Struktur props in das gefundene ptr-Objekt.

    void update(CWnd *ptr, const Properties &props)
    {
      ptr.Width(props.width);
      ptr.Height(props.height);
      ptr.Alignment(convert(props.align));
      ptr.Margins(props.margins[0], props.margins[1], props.margins[2], props.margins[3]);
      CWndObj *obj = dynamic_cast<CWndObj *>(ptr);
      if(obj)
      {
        obj.Text(props.text);
      }
      
      CBoxH *boxh = dynamic_cast<CBoxH *>(ptr);
      if(boxh)
      {
        boxh.HorizontalAlign((HORIZONTAL_ALIGN)props.style);
        boxh.Pack();
        return;
      }
      CBoxV *boxv = dynamic_cast<CBoxV *>(ptr);
      if(boxv)
      {
        boxv.VerticalAlign((VERTICAL_ALIGN)props.style);
        boxv.Pack();
        return;
      }
      CEdit *edit = dynamic_cast<CEdit *>(ptr);
      if(edit)
      {
        edit.TextAlign(LayoutConverters::style2textAlign(props.style));
        return;
      }
    }

Kehren wir nun zu dem Problem der Auswahl der GUI-Elemente im Formular zurück. Es wird durch das Cache-Objekt gelöst, aufgrund der Behandlung der vom Benutzer initiierten Ereignisse. onEvent ist in der Klasse StdLayoutCache reserviert, um über das Makro ON_EVENT_LAYOUT_ARRAY eine Verbindung zu den Diagrammereignissen auf der Karte herzustellen:

  EVENT_MAP_BEGIN(DesignerForm)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
    ...
  EVENT_MAP_END(AppDialogResizable)

Dies sendet Mausklicks für alle Cache-Elemente an die Funktion onEvent, den wir in unserer abgeleiteten Klasse, DefaultStdLayoutCache, definieren. In der Klasse wird der Zeiger selected des universellen Fenstertyps CWnd erzeugt; er muss von der Funktion onEvent gefüllt werden.

  class DefaultStdLayoutCache: public StdLayoutCache
  {
    protected:
      CWnd *selected;
      
    public:
      CWnd *getSelected(void) const
      {
        return selected;
      }
      
      ...
      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(element); // get actual GUI element
          }
          ...
          
          selected = element;
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", element.Id());
          EventChartCustom(CONTROLS_SELF_MESSAGE, ON_LAYOUT_SELECTION, 0, 0.0, NULL);
          return true;
        }
        return false;
      }
  };

Ein Element wird visuell im Formular durch einen roten Rahmen in der trivialen Methode highlight (Aufruf von ColorBorder) ausgewählt. Die Methode deselektiert zuerst das vorhergehende ausgewählte Element (setzt die Rahmenfarbe, CONTROLS_BUTTON_COLOR_BORDER), findet dann ein Cache-Element, das dem angeklickten Diagrammobjekt entspricht und speichert den Zeiger darauf in der Variablen selected. Schließlich wird das neu ausgewählte Objekt durch einen roten Rahmen markiert und das Ereignis ON_LAYOUT_SELECTION an das Diagramm gesendet. Es informiert den Inspektor darüber, dass ein neues Element im Formular ausgewählt wurde und sollte daher seine Eigenschaften im Inspektor-Dialog anzeigen.

Im Inspektor wird dieses Ereignis in OnRemoteSelection abgefangen, der vom Designer eine Verknüpfung zum ausgewählten Objekt anfordert und alle Attribute daraus über die Standard-API der Bibliothek liest.

  EVENT_MAP_BEGIN(InspectorDialog)
    ...
    ON_NO_ID_EVENT(ON_LAYOUT_SELECTION, OnRemoteSelection)
  EVENT_MAP_END(AppDialogResizable)

Unten sehen Sie den Beginn der Methode OnRemoteSelection.

  bool InspectorDialog::OnRemoteSelection()
  {
    DefaultStdLayoutCache *remote = designer.getCache();
    CWnd *ptr = remote.getSelected();
    
    if(ptr)
    {
      string purename = StringSubstr(ptr.Name(), 5); // cut instance id prefix
      CWndObj *x = dynamic_cast<CWndObj *>(props.name.backlink());
      if(x) x.Text(purename);
      props.name = purename;
      
      int t = -1;
      ComboBoxResizable *types = dynamic_cast<ComboBoxResizable *>(props.type.backlink());
      if(types)
      {
        t = GetTypeByRTTI(ptr._rtti);
        types.Select(t);
        props.type = t;
      }
      
      // width and height
      SpinEditResizable *w = dynamic_cast<SpinEditResizable *>(props.width.backlink());
      w.Value(ptr.Width());
      props.width = ptr.Width();
      
      SpinEditResizable *h = dynamic_cast<SpinEditResizable *>(props.height.backlink());
      h.Value(ptr.Height());
      props.height = ptr.Height();
      ...
    }
  }

Nachdem der Algorithmus vom Designer-Cache den ptr-Link zum ausgewählten Objekt erhalten hat, ermittelt er dessen Namen, löscht ihn aus dem Fensterbezeichner (dieses Feld, m_instance_id, in der Klasse CAppDialog ist ein Präfix in allen Namen, um Konflikte zwischen Objekten aus verschiedenen Fenstern zu verhindern, von denen wir 2 haben) und schreibt ihn in das mit dem Namen verbundene Steuerelement. Sie sollten beachten, dass wir hier einen Rückverweis auf das Steuerelement (backlink()) von der Eigenschaft StdValue <string> name verwenden. Da wir das Feld von innen modifizieren, wird außerdem das Ereignis, das sich auf seine Änderung bezieht, nicht erzeugt (wie es manchmal der Fall ist, wenn die Änderung vom Benutzer initiiert wird); daher muss der neue Wert zusätzlich in die entsprechende Eigenschaft von PropertySet (props.name) geschrieben werden.

Technisch gesehen wäre es aus der Sicht von OOP korrekter, für jeden Eigenschaftstyp des Steuerelements seine virtuelle Änderungsmethode zu überschreiben und die mit ihr verknüpfte Instanz StdValue automatisch zu aktualisieren. Hier ist zum Beispiel, wie dies für CEdit geschehen könnte.

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      ...
      virtual bool OnSetText(void) override
      {
        if(CEdit::OnSetText())
        {
          if(CheckPointer(property) != POINTER_INVALID) property = m_text;
          return true;
        }
        return false;
      }    
  };

Dann würde die Änderung des Feldinhalts mit der Methode Text() zum nachfolgenden Aufruf von OnSetText und zur automatischen Aktualisierung der Eigenschaft führen. Bei zusammengesetzten Steuerelementen, wie z.B. CCheckGroup, ist dies jedoch nicht so einfach; daher haben wir eine praktischere Implementierung bevorzugt.

In ähnlicher Weise aktualisieren wir mit Backlinks zu Steuerelementen den Inhalt in den Feldern Höhe, Breite, Typ und anderen Eigenschaften des im Designer ausgewählten Objekts.

Um die unterstützten Typen zu identifizieren, verfügen wir über eine Enumeration, deren Element auf der Grundlage der speziellen Variablen _rtti, die wir in unseren vorangegangenen Artikeln auf der untersten Ebene in der Klasse CWnd hinzugefügt haben, erkannt werden kann, und füllen sie mit dem Namen einer bestimmten Klasse in allen abgeleiteten Klassen.

Schnellstartanleitung

Der Inspektor-Dialog enthält das Eingabefeld verschiedener Typen mit den Eigenschaften des aktuellen Objekts (im Designer ausgewählt) oder des zu erstellenden Objekts.

Pflichtfelder sind Name (string) und Typ (in der Dropdown-Liste zu wählen).

In den Feldern Breite und Höhe kann die Objektgröße in Pixeln definiert werden. Diese Einstellungen werden jedoch nicht berücksichtigt, wenn unten ein bestimmter Dehnungsmodus angegeben ist: Zum Beispiel: Bindung an den linken und rechten Rand bedeutet die an den Container angepasste Breite. Wenn Sie mit der Maus bei gedrückter Umschalttaste in das Höhen- oder Breitenfeld klicken, kann die Eigenschaft auf den Standardwert (Breite 100 und Höhe 20) zurückgesetzt werden.

Alle Steuerelemente vom Typ SpinEdit (nicht nur in den Größeneigenschaften) wurden so verbessert, dass das Bewegen der Maus innerhalb des Steuerelements nach links oder rechts mit gedrückter Maustaste (Ziehen, aber nicht Fallenlassen) die Werte von Spinner proportional zum zurückgelegten Abstand in Pixeln schnell ändert. Dies wurde getan, um das Editieren zu erleichtern, was durch das Drücken kleiner Pumptasten nicht sehr bequem ist. Änderungen sind für alle Programme verfügbar, die Steuerelemente aus dem Ordner ControlsPlus verwenden werden.

Die Dropdown-Liste mit dem Inhaltsausrichtungsstil (Style) ist nur für die Elemente von CBoxV, CBoxH und CEdit verfügbar (für alle anderen Typen ist sie blockiert). Für den Container CBox sind alle Ausrichtungsmodi ("center", "justify", "left/top", "right/bottom" und "stack") aktiviert. Für CEdit funktionieren nur diejenigen, die mit ENUM_ALIGN_MODE ("center", "left" und "right") korrespondieren.

Im Feld Text kann die Kopfzeile von CButton, CLabel oder der Inhalt von CEdit definiert werden. Bei anderen Typen ist das Feld deaktiviert.

Dropdown-Liste Color dient zur Auswahl der Hintergrundfarbe aus der Liste der Web-Farben. Sie ist nur für CBoxH, CBoxV, CButton und CEdit verfügbar. Die anderen Typen der Steuerelemente, die zusammengesetzte Typen sind, erfordern eine ausgeklügeltere Technik zur Aktualisierung der Farbe in allen ihren Komponenten, daher haben wir uns entschieden, sie noch nicht zu unterstützen. Um Farben auszuwählen, wurde die Klasse CListView modifiziert. Ihr wurde ein spezieller Farbmodus hinzugefügt, in dem die Werte der Listenelemente als Farbcodes interpretiert werden und der Hintergrund jedes Elements in der entsprechenden Farbe gezeichnet wird. Dieser Modus wird durch die Methode SetColorMode aktiviert und in der neuen Klasse ComboBoxWebColors (eine Spezialisierung von ComboBoxResizable aus dem Ordner Layouts) verwendet.

Die Standardfarben der Bibliotheks-GUI können zur Zeit nicht ausgewählt werden, da es ein Problem mit der Definition der Standardfarben gibt. Es ist für uns wichtig, die Standardfarbe für jeden Typ der Steuerelemente zu kennen, um sie nicht als ausgewählt in der Liste anzuzeigen, wenn der Nutzer keine bestimmte Farbe ausgewählt hat. Der einfachste Ansatz besteht darin, ein leeres Steuerelement eines bestimmten Typs zu erstellen und darin die Eigenschaft von ColorBackground einzulesen, aber das funktioniert nur bei einer sehr begrenzten Anzahl von Steuerelementen. Die Sache ist die, dass Farbe in der Regel nicht im Klassenkonstruktor zugewiesen wird, sondern in der Methode Create, die viel unnötige Initialisierung erzeugt, einschließlich der Erzeugung von realen Objekten im Diagramm. Natürlich brauchen wir keine unnötigen Objekte. Darüber hinaus ergibt sich die Hintergrundfarbe vieler zusammengesetzter Objekte aus dem Basishintergrund und nicht aus dem grundlegenden Steuerelement. Aufgrund der Kompliziertheit bei der Berücksichtigung dieser Nuancen haben wir beschlossen, alle Standardfarben in allen Klassen der Steuerelemente der Standardbibliothek als nicht ausgewählt zu betrachten. Das bedeutet, dass sie nicht in die Liste aufgenommen werden können, da der Nutzer sonst zwar eine solche Farbe auswählen kann, aber keine Bestätigung seiner Auswahl im Inspektor sehen wird. Die Listen der Web-Farben und der Farben der Standard-GUI werden in der Datei LayoutColors.mqh präsentiert.

Um die Farbe auf den Standardwert zurückzusetzen (unterschiedlich für jeden Steuerelementtyp), sollte der erste "leere" Punkt in der Liste ausgewählt werden, der für clrNONE relevant ist.

Flaggen in der Gruppe der unabhängigen Schalter, Ausrichtung, entsprechen den Ausrichtungsmodi durch Seiten aus der Enumeration ENUM_WND_ALIGN_FLAGS, plus ein spezieller Modus, WND_ALIGN_CONTENT, wird ihnen hinzugefügt, der im zweiten Artikel beschrieben wird und nur für Container funktioniert. Wenn Sie beim Drücken eines Schalters die Umschalttaste gedrückt halten, schaltet das Programm synchron alle 4 Flags von ENUM_WND_ALIGN_FLAGS. Wenn die Option aktiviert ist, werden auch andere Flags aktiviert, und umgekehrt, wenn die Option deaktiviert ist, werden andere zurückgesetzt. Auf diese Weise kann die gesamte Gruppe mit einem Klick umgeschaltet werden, mit Ausnahme von WND_ALIGN_CONTENT (WND_ALIGN_CONTENT).

Die Ränder der Spinner definieren die Einzüge des Elements in Bezug auf die Seiten des Container-Rechtecks, in dem sich dieses Element befindet. Reihenfolge der Felder: Links, oben, rechts und unten. Alle Felder können schnell auf Null zurückgesetzt werden, indem Sie mit gedrückter Umschalttaste-Taste in ein beliebiges Feld klicken. Alle Felder können einfach als gleich gesetzt werden, indem man mit gedrückter Strg-Taste auf das Feld mit dem gewünschten Wert klickt — dies führt dazu, dass der Wert in 3 andere Felder kopiert wird.

Die Schaltfläche Apply ist uns bereits bekannt — sie wendet die vorgenommenen Änderungen an, was dazu führt, dass entweder ein neues Steuerelement im Designer erstellt oder das vorhandene modifiziert wird.

Das neue Objekt wird in das ausgewählte Containerobjekt oder den Container mit dem ausgewählten Steuerelement eingefügt (wenn das Steuerelement ausgewählt ist).

Um ein Element im Designer auszuwählen, muss man darauf mit der Maus klicken. Das gewählte Element wird mit einem roten Rahmen hervorgehoben. Die einzige Ausnahme ist CLabel — diese Funktion wird darin nicht unterstützt.

Das neue Element wird sofort nach dem Einfügen automatisch ausgewählt.

In einen leeren Dialog kann nur der Container CBoxV oder CBoxH eingefügt werden, wobei es nicht notwendig ist, den Client-Bereich vorher auszuwählen. Dieser erste und größte Container wird standardmäßig über das gesamte Fenster gestreckt.

Wiederholtes Klicken auf ein bereits ausgewähltes Element ruft den Löschauftrag auf. Die Löschung erfolgt erst nach Bestätigung durch den Nutzer.

Die Zwei-Positionen-Schaltfläche TestMode schaltet zwischen den beiden Betriebsmodi des Designers um. Standardmäßig ist er nicht gedrückt, der Testmodus ist deaktiviert, und die Bearbeitung der Designer-Oberfläche funktioniert — der Nutzer kann Elemente durch Anklicken mit der Maus auswählen und löschen. Wenn sie gedrückt ist, ist der Testmodus aktiviert. Gleichzeitig funktioniert der Dialog in etwa wie im realen Programm, während die Bearbeitung des Layouts und die Auswahl der Elemente deaktiviert sind.

Die Schaltfläche Export ermöglicht das Speichern der aktuellen Konfiguration der Designer-Schnittstelle als MQL-Layout. Der Dateiname beginnt mit Präfix layout und enthält die aktuelle Zeitmaske und die Erweiterung txt. Wenn Sie beim Drücken von Export die Umschalttaste gedrückt halten, wird die Konfiguration des Formulars nicht als Text, sondern in binärer Form in einer Datei mit eigenem Format und der Erweiterung mql gespeichert. Das ist praktisch, denn Sie können den Entwurfsprozess des Layouts unterbrechen und nach einer Weile wiederherstellen. Um die binäre Layout mql-Datei hochzuladen, wird die gleiche Export-Schaltfläche verwendet, vorausgesetzt, dass das Formular und der Cache der Elemente leer sind, was unmittelbar beim Start des Programms geschieht. Die aktuelle Version versucht immer, die Datei layout.mql zu importieren. Auf Wunsch können Sie die Dateiauswahl in den Eingaben oder in MQL implementieren.

Im oberen Teil des Inspektor-Dialogs finden Sie ein Drop-Down-Menü mit allen im Designer erstellten Elementen. Die Auswahl eines Elements in der Liste führt zur automatischen Auswahl und Hervorhebung dieses Elements im Designer. Umgekehrt führt die Auswahl eines Elements im Formular dazu, dass es in der Liste aktuell ist.

Jetzt, beim Bearbeiten, können die Fehler von 2 Kategorien auftreten: Solche, die durch die Analyse des MQL-Layouts behoben werden können, und schwerwiegendere. Zu den ersteren gehören solche Kombinationen von Einstellungen, bei denen Steuerelemente oder Container über die Grenzen des Fensters oder des Elterncontainers hinausgehen. In diesem Fall werden sie in der Regel nicht mehr mit der Maus ausgewählt, und Sie können sie nur über den Selektor im Inspektor aktivieren. Welche Eigenschaften genau falsch sind, können Sie herausfinden, indem Sie das MQL-Markup in Texteform analysieren — es genügt, Export zu drücken, um den aktuellen Zustand zu erhalten. Nachdem Sie das Markup analysiert haben, sollten Sie die Eigenschaften im Inspektor korrigieren und dabei die korrekte Ansicht des Formulars wiederherstellen.

Diese Version des Programms dient der Überprüfung des Konzepts, und im Quellcode gibt es keine Prüfungen für alle Kombinationen von Parametern, die bei der Neuberechnung der Größen adaptiver Container auftreten können.

Zur zweiten Kategorie von Fehlern gehört insbesondere die Situation, dass ein Element versehentlich in einen falschen Container eingefügt wurde. In diesem Fall können Sie das Element nur löschen und wieder hinzufügen, allerdings an einer anderen Stelle.

Es wird empfohlen, das Formular regelmäßig im Binärformat zu speichern (drücken Sie die Export-Taste und halten Sie dabei die Umschalttaste gedrückt), so dass Sie im Falle unlösbarer Probleme mit der letzten guten Konfiguration weiterarbeiten können.

Betrachten wir einige Beispiele für die Arbeit mit dem Programm.

Beispiele

Versuchen wir zunächst, die Struktur des Inspektors in Designer nachzubilden. In der Animation unten sehen Sie den Prozess, der mit dem Hinzufügen von vier oberen Zeichenketten und Feldern beginnt, um den Namen, den Typ und die Breite festzulegen. Es werden verschiedene Arten von Steuerelementen, Ausrichtungen und Farbschemata verwendet. Labels, die die Feldnamen enthalten, werden mit den Eingabefeldern von CEdit gebildet, da CLabel eine sehr eingeschränkte Funktionalität hat (insbesondere werden Textausrichtung und Hintergrundfarbe nicht unterstützt). Die Einstellung des Attributs schreibgeschützt ist in Inspektor jedoch nicht verfügbar. Daher besteht die einzige Möglichkeit, eine Kennzeichnung als nicht editierbar zu kennzeichnen, darin, ihm einen grauen Hintergrund zuzuweisen (dies ist ein rein visueller Effekt). Im MQL-Code müssen solche CEdit-Objekte sicherlich zusätzlich entsprechend angepasst, d.h. in den Modus schreibgeschützt geschaltet werden. Genau das haben wir im Inspektor selbst getan.

Bearbeitung des Formulars

Bearbeitung des Formulars

Die Bearbeitung des Formulars zeigt deutlich den anpassungsfähigen Charakter der Markup-Technologie und ist als externe Darstellung eindeutig an MQL-Markup gebunden. Sie können jederzeit die Schaltfläche Export drücken und den resultierenden MQL-Code sehen.

In der endgültigen Version erhalten wir einen Dialog, der praktisch vollständig mit dem Inspektor-Fenster übereinstimmt (bis auf einige Details).

Inspektor-Dialog-Markup, wiederhergestellt im Designer

Inspektor-Dialog-Markup, wiederhergestellt im Designer

Es ist jedoch zu beachten, dass innerhalb des Inspektors viele Klassen von Steuerelementen nicht dem Standard entsprechen, da sie von einer bestimmten x-Eigenschaft abgeleitet werden und einen zusätzlichen algorithmischen Bündelung darstellen. In unserem Beispiel werden jedoch nur Standardklassen von Steuerelementen (ControlsPlus) verwendet. Mit anderen Worten: Das resultierende Layout enthält immer nur die externe Repräsentation des Programms und das Standardverhalten von Steuerelementen. Die Verfolgung der Zustände von Elementen und die Kodierung der Reaktionen auf ihre Änderungen, einschließlich einer möglichen Anpassung der Klassen, ist das Vorrecht des Programmierers. Das geschaffene System erlaubt es, die Artefakte im MQL-Markup wie in der normalen MQL zu ändern. Das heißt, Sie können z.B. ComboBox durch ComboBoxWebColors ersetzen. In jedem Fall müssen aber alle im Layout erwähnten Klassen mit den Direktiven von #include in das Projekt aufgenommen werden.

Das obige Dialogfeld (Inspektor-Duplikat) wurde mit dem Befehl Export in die Text- und Binärdateien gespeichert — beide sind hiermit unter den Namen layout-inspector.txt bzw. layout-inspector.mql verbunden.

Nachdem Sie die Textdatei analysiert haben, können Sie das Inspektor-Markup ohne Bindung an Algorithmen oder Daten sinnvoll nutzen.

Grundsätzlich kann der Inhalt des Markups nach dem Export in die Datei in jedes Projekt eingefügt werden, zu dem auch die Header-Dateien des Layoutsystems und alle verwendeten GUI-Klassen gehören. Als Ergebnis erhalten wir eine funktionierende Schnittstelle. Insbesondere wird ein Projekt mit dem leeren DummyForm-Dialog angehängt. Wenn Sie möchten, können Sie darin das CreateLayout finden und darin das MQL-Markup einfügen, das im Designer vorläufig vorbereitet werden muss.

Dies kann auch leicht für den Layout-Inspektor.txt gemacht werden. Wir kopieren den gesamten Inhalt dieser Datei in die Zwischenablage und fügen ihn in die Datei DummyForm.mqh innerhalb der Methode CreateLayout ein, wo der Kommentar // exportiertes MQL-Layout hier eingefügt wird.

Bitte beachten Sie, dass die Dialoggröße in der Textdarstellung des Layouts (in diesem Fall 200*350), für das es erstellt wurde, angegeben ist. Daher sollten die folgenden Strings in den Quelltext CreateLayout nach dem String der Erstellung des Objekts mit dem Formular _Layout<DummyForm> dialog(this...) und vor dem kopierten Layout eingefügt werden:

  Width(200);
  Height(350);
  CSize sz = {200, 350};
  SetSizeLimit(sz);

Dies wird ausreichend Platz für alle Steuerelemente bieten und eine Verkleinerung des Dialogs nicht zulassen.

Wir erzeugen das relevante Fragment nicht automatisch beim Export, da das Layout möglicherweise nur einen Teil des Dialogs darstellt oder eventuell für andere Klassen von Fenstern und Containern dient, wo es diese Methoden nicht geben wird.

Wenn wir das Beispiel jetzt kompilieren und ausführen, erhalten wir eine sehr ähnliche Kopie vom Inspektor. Aber es gibt immer noch Unterschiede.

Die wiederhergestellte Schnittstelle des Inspektors

Die wiederhergestellte Schnittstelle des Inspektors

Erstens sind alle Dropdown-Listen leer und funktionieren daher nicht. Es sind keine Spinner eingestellt, also funktionieren sie auch nicht. Die Gruppe der Ausrichtungsflags ist visuell leer, weil wir kein Kontrollkästchen im Layout erzeugt haben, aber das entsprechende Steuerelement existiert, und es hat sogar 5 versteckte Kontrollkästchen, die von der Bibliothek der Standardkomponenten erzeugt werden, basierend auf der Anfangsgröße des Steuerelements (Sie können all diese Objekte in der Liste der Diagrammobjekte sehen, Befehl Objektliste).

Zweitens ist die Gruppe der Spinner mit den Abstandswerten wirklich nicht vorhanden: Wir haben sie nicht in das Formular übertragen, weil sie von einem Layout-Objekt als Array im Inspektor erzeugt wird. Unser Editor kann nichts dergleichen tun. Wir könnten 4 unabhängige Elemente erstellen, aber dann müssten wir sie im Code ähnlich wie einander anpassen.

Wenn ein Steuerelement gedrückt wird, druckt das Formular seinen Namen, seine Klasse und seinen Identifikator in das Protokoll.

Wir können auch die binäre Datei layout-inspector.mql (nachdem wir sie vorläufig in layout.mql umbenannt haben) zurück in Inspektor hochladen und sie weiter bearbeiten. Zu diesem Zweck genügt es, das Hauptprojekt anzuwählen und auf Export zu drücken, sobald das Formular noch leer ist.

Bitte beachten Sie, dass der Designer zu Illustrationszwecken eine gewisse Datenmenge für alle Steuerelemente mit Listen oder Gruppen generiert und auch den Bereich für Spinner festlegt. Daher können wir mit Elementen spielen, wenn wir in den TestModus wechseln. Diese Größe der Pseudodaten wird im Designer-Formular durch das Makro DUMMY_ITEM_NUMBER definiert und beträgt standardmäßig 11.

Nun wollen wir sehen, wie das Handelspanel im Designer erscheinen könnte.

Layout des Handelspanels: Farb-Würfel-Handelspanel

Layout des Handelspanels: Farb-Würfel-Handelspanel

Er erhebt keinen Anspruch auf Superfunktionalität, aber es geht darum, dass er leicht entsprechend den Präferenzen des jeweiligen Händlers redaktionell verändert werden kann. Dieses Formular, wie auch das vorhergehende, verwendet farbige Container, um ihre Anordnung leichter erkennen zu können.

Wir sollten nochmals darauf hinweisen, dass wir hier nur das Aussehen meinen. Bei der Ausgabe des Designers erhalten wir den MQL-Code, der nur für die Erzeugung des Fensters und den Anfangszustand der Steuerelemente zuständig ist. Wie üblich müssen alle Berechnungsalgorithmen, die Reaktionen auf die Aktionen des Benutzers, der Schutz vor falsch eingegebenen Daten und das Senden von Handelsaufträgen manuell programmiert werden.

In diesem Layout sollten einige Arten von Steuerelementen durch etwas Passenderes ersetzt werden. So werden Verfallsdaten von Pending-Orders darin mit Kalender bezeichnet, der die Eingabe der Uhrzeit nicht unterstützt. Alle Dropdown-Listen müssen mit den entsprechenden Optionen gefüllt werden. So können z.B. Stop-Levels in verschiedenen Einheiten eingegeben werden, wie Preis, Abstand in Pips, Risiko/Verluste als Einlagenprozentsatz oder mit absolutem Wert, während das Volumen als fest, in Geld oder als Prozentsatz der freien Marge festgelegt werden kann, und das Trailing ist einer von mehreren Algorithmen.

Dieser Markup wird hier als zwei Dateien mit einem Layout-Farb-Würfel-Handelspanel angehängt: als Text und binär. Ersteres kann in das leere Formular eingefügt werden, z.B. DummyForm, und mit Daten und Ereignisbehandlung vervollständigt werden. Die zweite kann in den Designer geladen und bearbeitet werden. Aber denken Sie daran, dass der grafische Editor nicht zwingend erforderlich ist. Das Markup kann auch in seiner textlichen Darstellung korrigiert werden. Der einzige Vorteil des Editors besteht darin, dass wir mit den Einstellungen spielen und die laufenden Änderungen sofort sehen können. Er unterstützt jedoch nur die grundlegendsten Funktionen.

Schlussfolgerungen

In diesem Aufsatz haben wir einen einfachen Editor zur interaktiven Entwicklung der grafischen Oberfläche von Programmen, die auf der MQL-Markup-Technologie basieren, vorgestellt. Die vorgestellte Implementierung enthält nur grundlegende Funktionen, die noch ausreichend sind, um die Funktionsfähigkeit des Konzepts und die weitere Ausdehnung auf andere Arten von Steuerelementen, eine vollständigere Unterstützung verschiedener Eigenschaften, andere Bibliotheken von GUI-Komponenten und Editiermechanismen zu demonstrieren. Insbesondere fehlt dem Editor noch die Funktion des Abbrechens von Operationen, des Einfügens von Elementen an einer beliebigen Position im Container (d.h. nicht nur das Hinzufügen an das Ende der Liste der bereits vorhandenen Steuerelement), der Gruppenoperationen, des Kopierens in die Zwischenablage und des Einfügens aus der Zwischenablage und vieles mehr. Open Source Codes erlauben es Ihnen jedoch, die Technologie zu ergänzen und an Ihre Bedürfnisse anzupassen.

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

Beigefügte Dateien |
MQL5GUI3.zip (112.66 KB)
Zeitreihen in der Bibliothek DoEasy (Teil 43): Klassen der Objekte von Indikatorpuffern Zeitreihen in der Bibliothek DoEasy (Teil 43): Klassen der Objekte von Indikatorpuffern

Der Artikel beschäftigt sich mit der Entwicklung von Indikatorpuffer-Objektklassen, abgeleitet vom abstrakten Pufferobjekt, um die Deklaration zu vereinfachen und mit Indikatorpuffern zu arbeiten, während gleichzeitig nutzerdefinierte Indikatorprogramme auf der Grundlage der Bibliothek DoEasy erstellt werden.

Die Handelssignale mehrerer Währungen überwachen (Teil 5): Signalkombinationen Die Handelssignale mehrerer Währungen überwachen (Teil 5): Signalkombinationen

Im fünften Artikel, der sich auf die Schaffung eines Handelssignalmonitors bezieht, werden wir zusammengesetzte Signale betrachten und die notwendige Funktionalität implementieren. In früheren Versionen verwendeten wir einfache Signale, wie RSI, WPR und CCI, und wir führten auch die Möglichkeit ein, nutzerdefinierte Indikatoren zu verwenden.

Nativer Twitter-Client für MT4 und MT5 ohne DLL Nativer Twitter-Client für MT4 und MT5 ohne DLL

Wollten Sie schon immer auf Tweets zugreifen und/oder Ihre Handelssignale auf Twitter posten? Suchen Sie nicht mehr, diese fortlaufenden Artikelserien zeigen Ihnen, wie Sie es ohne die Verwendung einer DLL machen können. Genießen Sie die Reise der Implementierung der Tweeter-API mit MQL. In diesem ersten Teil werden wir dem glorreichen Weg der Authentifizierung und Autorisierung beim Zugriff auf die Twitter-API folgen.

Nativer Twitter-Client: Teil 2 Nativer Twitter-Client: Teil 2

Ein als MQL-Klasse implementierter Twitter-Client, mit dem Sie Tweets mit Fotos versenden können. Alles, was Sie brauchen, ist eine einzige, in sich geschlossene Include-Datei und schon können Sie all Ihre wunderbaren Charts und Signale twittern.