Anwendung von OLAP im Handel (Teil 2): Die Visualisierung der Ergebnisse der interaktiven, mehrdimensionalen Datenanalyse

Stanislav Korotky | 11 Juli, 2019

Im ersten Artikel über den Einsatz von OLAP-Techniken im Handel haben wir die allgemeinen, mehrdimensionalen Datenverarbeitungsprinzipien berücksichtigt und fertige MQL-Klassen bereitgestellt, die die praktische Anwendung von OLAP für die Kontenhistorie oder die Verarbeitung von Handelsberichten ermöglichen. Allerdings hatten wir eine vereinfachte Ausgabe der Ergebnisse als Text in den Expertenprotokollen implementiert. Für eine effizientere, visuelle Darstellung müssen wir eine neue Klasse, eine Ableitung der Display-Schnittstelle, anlegen, die OLAP-Daten grafisch darstellen kann. Diese Aufgabe erfordert viel Vorarbeit und betrifft viele verschiedene Aspekte, die nichts mit dem OLAP zu tun haben. Lassen wir also die Datenverarbeitung beiseite und konzentrieren wir uns auf die grafische Oberfläche des MQL-Programms.

Für die GUI-Implementierung stehen mehrere MQL-Bibliotheken zur Verfügung, darunter die Standardbibliothek des Steuerelements Include/Controls). Einer der bemerkenswerten Nachteile in fast allen Bibliotheken liegt darin, dass es keine Möglichkeit gibt, das Layout der Elemente im Fenster automatisch zu steuern. Mit anderen Worten, die Positionierung und Ausrichtung der Elemente erfolgt statisch über fest programmierte Konstanten mit den Koordinaten X und Y. Es gibt noch ein weiteres Problem, das eng mit dem ersten zusammenhängt: Es gibt keine visuelle Gestaltung für Bildschirmformen. Das ist eine noch schwierigere Aufgabe, die jedoch nicht unmöglich ist. Da die Schnittstelle nicht das Hauptthema innerhalb dieses Projekts ist, wurde beschlossen, sich nicht auf den Editor des Bildschirmformulars zu konzentrieren, sondern einen einfacheren adaptiven Interface-Ansatz zu implementieren. Elemente in dieser Schnittstelle müssen speziell in Gruppen angeordnet sein, die automatisch korrelierte Positionierungs- und Skalierungsregeln unterstützen können.

Das Problem mit der Standardbibliothek ist, dass ihre Dialogfenster eine feste Größe haben. Bei der Darstellung großer OLAP-Hyperwürfel wäre es jedoch für den Benutzer bequemer, das Fenster zu maximieren oder zumindest so weit zu dehnen, dass die Zellenbeschriftungen ohne Überlappung auf die Achsen passen.

Separate offene GUI-bezogene Entwicklungen sind auf der Website mql5.com verfügbar: Sie behandeln verschiedene Probleme, aber ihr Verhältnis von Komplexität zu Fähigkeit ist bei weitem nicht optimal. Entweder sind die Möglichkeiten begrenzt (z.B. verfügt eine Lösung über einen Layoutmechanismus, bietet aber keine Skalierungsmöglichkeiten), oder die Integration erfordert viel Aufwand (Sie müssen eine umfangreiche Dokumentation lesen, Nicht-Standard-Methoden lernen, etc.) Da alle anderen Dinge gleich sind, ist es besser, eine Lösung zu verwenden, die auf Standardelementen basiert, die häufiger und verbreiteter sind (d.h. die in einer größeren Anzahl von MQL-Anwendungen verwendet werden und daher einen höheren Nutzwert haben).

Daher habe ich eine scheinbar einfache, technologische Lösung ausgewählt, die in den Artikeln Verwendung von Layouts und Containern für GUI Controls: Die CBox Klasse und Die Verwendung von Layout und Containern für GUI Controls: Die CGrid Klasse von Enrico Lambino.

Im ersten Artikel werden die Kontrollen den Containern mit der horizontalen oder vertikalen Anordnung hinzugefügt. Sie können verschachtelt werden und bieten so ein beliebiges Interface-Layout. Der zweite Artikel stellt Container mit dem tabellarischen Layout vor. Beide können mit allen Standardsteuerungen sowie mit allen ordnungsgemäß entwickelten Steuerungen arbeiten, die auf der CWnd-Klasse basieren.

Der Lösung fehlt nur die dynamische Größenänderung von Fenstern und Containern. Dies wird der erste Schritt zur Lösung des allgemeinen Problems sein.

"Gummi"-Fenster

Die Klassen CBox und CGrid sind mit Projekten durch die Header-Dateien Box.mqh, Grid.mqh und GridTk.mqh verbunden. Wenn Sie Archive aus den Artikeln verwenden, installieren Sie diese Dateien im Verzeichnis Include/Layouts.

Achtung! Die Standardbibliothek enthält bereits die CGrid-Struktur. Es ist für das Zeichnen des Rasters auf dem Chart vorgesehen. Die Containerklasse CGrid ist damit nicht verbunden. Die Übereinstimmung der Namen ist unangenehm, aber nicht kritisch.

Wir werden einen kleinen Fehler in der Datei GridTk.mqh beheben und einige Ergänzungen in der Datei Box.mqh vornehmen, woraufhin wir direkt mit der Verbesserung der Standard-Dialogklasse CAppDialog beginnen können. Natürlich werden wir die vorhandene Klasse nicht auflösen, sondern eine neue von CAppDialog abgeleitete Klasse erstellen.

Die wichtigsten Änderungen betreffen die Methode CBox::GetTotalControlsSize (die entsprechenden Zeilen sind mit Kommentaren versehen). Sie können die Dateien aus den Originalprojekten mit denen im Anhang vergleichen.

  void CBox::GetTotalControlsSize(void)
  {
    m_total_x = 0;
    m_total_y = 0;
    m_controls_total = 0;
    m_min_size.cx = 0;
    m_min_size.cy = 0;
    int total = ControlsTotal();
    
    for(int i = 0; i < total; i++)
    {
      CWnd *control = Control(i);
      if(control == NULL) continue;
      if(control == &m_background) continue;
      CheckControlSize(control);
      
      // added: invoke itself recursively for nested containers
      if(control.Type() == CLASS_LAYOUT)
      {
        ((CBox *)control).GetTotalControlsSize();
      }
      
      CSize control_size = control.Size();
      if(m_min_size.cx < control_size.cx)
        m_min_size.cx = control_size.cx;
      if(m_min_size.cy < control_size.cy)
        m_min_size.cy = control_size.cy;
      
      // edited: m_total_x and m_total_y are incremeted conditionally according to container orientation
      if(m_layout_style == LAYOUT_STYLE_HORIZONTAL) m_total_x += control_size.cx;
      else m_total_x = MathMax(m_min_size.cx, m_total_x);
      if(m_layout_style == LAYOUT_STYLE_VERTICAL) m_total_y += control_size.cy;
      else m_total_y = MathMax(m_min_size.cy, m_total_y);
      m_controls_total++;
    }
    
    // added: adjust container size according to new totals
    CSize size = Size();
    if(m_total_x > size.cx && m_layout_style == LAYOUT_STYLE_HORIZONTAL)
    {
      size.cx = m_total_x;
    }
    if(m_total_y > size.cy && m_layout_style == LAYOUT_STYLE_VERTICAL)
    {
      size.cy = m_total_y;
    }
    Size(size);
  }

Kurz gesagt, die modifizierte Version berücksichtigt die mögliche dynamische Größenänderung von Elementen.

Zu den Testbeispielen in den Originalartikeln gehörten der Controls2 Expert Advisor (ein Analogon zum Standard Controls Projekt, das im Standard MetaTrader Lieferumfang unter dem Ordner Experts\Examples\Controls\) und das SlidingPuzzle2 Spiel. Beide Containerbeispiele befinden sich standardmäßig im Ordner Experts\Examples\Layouts\. Basierend auf diesen Containern werden wir versuchen, die "Gummi"-Fenster zu implementieren.

Erstellen wir MaximizableAppDialog.mqh unter Include\Layouts\. Die Fensterklasse wird von CAppDialog übernommen.

  #include <Controls\Dialog.mqh>
  #include <Controls\Button.mqh>
  
  class MaximizableAppDialog: public CAppDialog
  {

Wir benötigen 2 neue Schaltflächen mit Bildern: eine, um das Fenster zu maximieren (es befindet sich in der Kopfzeile neben der Schaltfläche Minimieren) und die andere für eine beliebige Größenänderung (in der unteren rechten Ecke).

  protected:
    CBmpButton m_button_truemax;
    CBmpButton m_button_size;

Die Anzeige des aktuellen Maximalzustandes oder des Größenänderungsprozesses wird in den entsprechenden logischen Variablen gespeichert.

    bool m_maximized;
    bool m_sizing;

Fügen wir auch ein Rechteck hinzu, in dem wir ständig die Diagrammgröße für den maximierten Zustand überwachen (so dass auch die Diagrammgröße angepasst werden muss), sowie eine bestimmte Mindestgröße festlegen, die das Fenster nicht unterschreiten darf (der Benutzer kann diese Einschränkung mit der öffentlichen Methode SetSizeLimit festlegen).

    CRect m_max_rect;
    CSize m_size_limit;

Die neu hinzugefügten Maximierungs- und Größenanpassungsmodi sollten mit den Standardmodi interagieren: die Standardgröße und die Minimierung eines Dialogs. Wenn das Fenster also maximiert ist, sollte es nicht durch Halten der Titelleiste gezogen werden, was in der Standardgröße erlaubt ist. Außerdem sollte der Zustand der Minimierungsschaltfläche zurückgesetzt werden, wenn das Fenster maximiert wird. Dazu benötigen wir Zugriff auf die Variablen CEdit m_caption in der Klasse CDialog und CBmpButton m_button_minmax in CAppDialog. Leider sind sie, wie auch viele andere Mitglieder dieser Klassen, im 'private' Bereich deklariert. Das sieht etwas seltsam aus, während diese Basisklassen Teil der 'public' Bibliothek sind, die für den breiten Gebrauch bestimmt ist. Eine bessere Lösung wäre es, alle Mitglieder als 'protected' zu deklarieren oder zumindest Methoden für den Zugriff auf sie bereitzustellen. Aber in unserem Fall sind sie 'private'. Das Einzige, was wir also tun können, ist, die Standardbibliothek durch einen "Patch" zu reparieren. Das Problem mit dem Patch ist, dass Sie nach dem Bibliotheksupdate den Patch erneut anwenden müssen. Aber die einzig mögliche Alternativlösung, doppelte Klassen CDialog und CAppDialog zu erstellen, erscheint aus Sicht der OOP-Ideologie nicht angemessen.

Dies ist nicht der letzte Fall, wenn die private Deklaration der Klassenmitglieder die Erweiterung der Funktionalität der abgeleiteten Klassen verhindert. Daher schlage ich vor, eine Kopie des Ordners Include/Controls zu erstellen, und wenn der Fehler "privater Mitgliederzugriff" während der Kompilierung auftritt, können Sie geeignete Teile bearbeiten, z.B. das entsprechende Element in den Abschnitt 'protected' verschieben oder 'private' durch 'protected' ersetzen.

Wir müssen einige der virtuellen Methoden der Basisklassen neu schreiben:

    virtual bool CreateButtonMinMax(void) override;
    virtual void OnClickButtonMinMax(void) override;
    virtual void Minimize(void) override;
  
    virtual bool OnDialogDragStart(void) override;
    virtual bool OnDialogDragProcess(void) override;
    virtual bool OnDialogDragEnd(void) override;

Die ersten drei Methoden sind mit der Schaltfläche Minimieren verknüpft und die anderen drei mit dem Größenänderungsprozess, der auf der Drag'n'Drop-Technologie basiert.

Es werden auch die virtuellen Methoden zur Erstellung des Dialogs und der Reaktion auf Ereignisse behandelt (letzteres wird immer implizit in den Makrodefinitionen der Ereignisbehandlungszuordnung verwendet, auf die wir später noch eingehen werden).

    virtual bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2) override;
    virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) override;

Die Schaltfläche Maximize wird zusammen mit der Standard-Schaltfläche Minimize in der vordefinierten Version von CreateButtonMinMax erstellt. Zuerst wird die Basisimplementierung aufgerufen, um die Standard-Kopftasten zu erhalten. Dann wird zusätzlich die neue Schaltfläche Maximize gezeichnet. Der Quellcode enthält einen gemeinsamen Satz von Anweisungen, die Anfangskoordinaten und Ausrichtung sowie die Verbindung von Bildressourcen festlegen. Daher wird dieser Code hier nicht angezeigt. Der vollständige Quellcode ist unten angehängt. Die Ressourcen der beiden Schaltflächen befinden sich im Unterverzeichnis "res":

  #resource "res\\expand2.bmp"
  #resource "res\\size6.bmp"
  #resource "res\\size10.bmp"

Die folgende Methode ist für die Verarbeitung von Maximieren-Schaltflächenklicks verantwortlich:

    virtual void OnClickButtonTrueMax(void);

Darüber hinaus werden wir Hilfsmethoden hinzufügen, um das Fenster im gesamten Diagramm zu maximieren und seine ursprüngliche Größe wiederherzustellen: Diese Methoden können von OnClickButtonTrueMax aufgerufen werden und führen die gesamte Arbeit aus, je nachdem, ob das Fenster maximiert ist oder nicht.

    virtual void Expand(void);
    virtual void Restore(void);

Das Erstellen der Schaltfläche Resize und der Start des Skalierungsprozesses sind in den folgenden Methoden implementiert:

    bool CreateButtonSize(void);
    bool OnDialogSizeStart(void);

Die Ereignisbehandlung wird durch ähnliche Makros bestimmt:

  EVENT_MAP_BEGIN(MaximizableAppDialog)
    ON_EVENT(ON_CLICK, m_button_truemax, OnClickButtonTrueMax)
    ON_EVENT(ON_DRAG_START, m_button_size, OnDialogSizeStart)
    ON_EVENT_PTR(ON_DRAG_PROCESS, m_drag_object, OnDialogDragProcess)
    ON_EVENT_PTR(ON_DRAG_END, m_drag_object, OnDialogDragEnd)
  EVENT_MAP_END(CAppDialog)

Die Objekte m_button_truemax und m_button_size wurden von uns selbst erstellt, während m_drag_object von der Klasse CWnd übernommen wird. Das Objekt wird in dieser Klasse verwendet, um das Ziehen von Fenstern über die Titelleiste zu ermöglichen. In unserer Klasse wird dieses Objekt an der Größenänderung beteiligt sein.

Aber das ist nicht die ganze notwendige Arbeit mit Ereignissen. Um die Größenänderung des Charts abzufangen, müssen wir das Ereignis CHARTEVENT_CHART_CHART_CHANGE behandeln. Zu diesem Zweck beschreiben wir die Methode ChartEvent in unserer Klasse: Sie wird sich mit der ähnlichen Methode in CAppDialog überschneiden. Daher müssen wir die grundlegende Implementierung aufrufen. Darüber hinaus werden wir den Ereigniscode überprüfen und eine spezifische Verarbeitung für CHARTEVENT_CHART_CHART_CHANGE durchführen.

  void MaximizableAppDialog::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
    if(id == CHARTEVENT_CHART_CHANGE)
    {
      if(OnChartChange(lparam, dparam, sparam)) return;
    }
    CAppDialog::ChartEvent(id, lparam, dparam, sparam);
  }

Die Methode OnChartChange verfolgt die Größe des Charts, und wenn die Chartgröße geändert wird, während sie der aktive Maximierungsmodus ist, wird ein neues Layout der Elemente initiiert. Dies geschieht mit der Methode SelfAdjustment.

  bool MaximizableAppDialog::OnChartChange(const long &lparam, const double &dparam, const string &sparam)
  {
    m_max_rect.SetBound(0, 0,
                        (int)ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS) - 0 * CONTROLS_BORDER_WIDTH,
                        (int)ChartGetInteger(ChartID(), CHART_HEIGHT_IN_PIXELS) - 1 * CONTROLS_BORDER_WIDTH);
    if(m_maximized)
    {
      if(m_rect.Width() != m_max_rect.Width() || m_rect.Height() != m_max_rect.Height())
      {
        Rebound(m_max_rect);
        SelfAdjustment();
        m_chart.Redraw();
      }
      return true;
    }
    return false;
  }

Diese Methode wird in der Klasse MaximizableAppDialog als abstrakt und virtuell deklariert, was bedeutet, dass die Unterklasse ihre Steuerelemente an die neue Größe anpassen muss.

    virtual void SelfAdjustment(const bool minimized = false) = 0;

Die gleiche Methode wird von anderen Stellen der Fensterklasse "rubber" (Gummi) aufgerufen, in der die Größenänderung durchgeführt wird. Zum Beispiel aus OnDialogDragProcess (wenn der Benutzer den unteren rechten Winkel zieht) und OnDialogDragEnd (der Benutzer hat die Skalierung abgeschlossen).

Das Verhalten des erweiterten Dialogs ist wie folgt: Nachdem er mit der Standardgröße im Chart angezeigt wird, kann der Benutzer ihn über die Titelleiste ziehen (Standardverhalten), minimieren (Standardverhalten) und maximieren (hinzugefügtes Verhalten). Der maximierte Zustand wird gespeichert, wenn die Größe des Charts geändert wird. Die gleiche Schaltfläche kann im maximierten Zustand verwendet werden, um das Fenster auf die Originalgröße zurückzusetzen oder zu minimieren. Das Fenster kann auch sofort aus dem minimierten Zustand maximiert werden. Wenn das Fenster weder minimiert noch maximiert ist, wird der aktive Bereich für die beliebige Skalierung (Dreieckstaste) in der unteren rechten Ecke angezeigt. Wenn das Fenster minimiert oder maximiert wird, wird dieser Bereich deaktiviert und ausgeblendet.

Damit könnte die Implementierung von MaximizableAppDialog abgeschlossen werden. Beim Testen zeigte sich jedoch noch ein weiterer Aspekt, der einer Weiterentwicklung bedurfte.

Im minimierten Zustand überlappt der aktive Größenänderungsbereich die Schaltfläche zum Schließen des Fensters und fängt dessen Mausereignisse ab. Dies ist der offensichtliche Bibliotheksfehler, da die Schaltfläche zum Ändern der Größe im minimierten Zustand ausgeblendet ist und sie inaktiv wird. Das Problem betrifft die Methode CWnd::OnMouseEvent. Sie benötigt die folgende Überprüfung:

  // if(!IS_ENABLED || !IS_VISIBLE) return false; - this line is missing

Dadurch werden auch deaktivierte und unsichtbare Steuerelemente Ereignisse abfangen. Natürlich könnte das Problem durch die Einstellung der entsprechenden Z-Reihenfolge für die Bedienelemente gelöst werden. Das Problem mit der Bibliothek ist jedoch, dass sie die Z-Reihenfolge der Kontrollen nicht berücksichtigt. Insbesondere enthält die Methode CWndContainer::OnMouseEvent eine einfache Schleife durch alle untergeordneten Elemente in umgekehrter Reihenfolge, so dass sie nicht versucht, ihre Priorität in der Z-Reihenfolge zu bestimmen.

Daher brauchen wir entweder einen neuen Patch für die Bibliothek oder eine Art "Trick" in der abgeleiteten Klasse. Hier wird die zweite Variante verwendet. Der "Trick" ist folgender: Im minimierten Zustand ist der Klick auf die Schaltfläche Resize als der Klick auf die Schaltfläche Schließen zu interpretieren (da dies die Schaltfläche ist, die sich überschneidet). Zu diesem Zweck wurde MaximizableAppDialog um die folgende Methode erweitert:

  void MaximizableAppDialog::OnClickButtonSizeFixMe(void)
  {
    if(m_minimized)
    {
      Destroy();
    }
  }

Die Methode wurde hinzugefügt, um das Ereignis abzubilden:

  EVENT_MAP_BEGIN(MaximizableAppDialog)
    ...
    ON_EVENT(ON_CLICK, m_button_size, OnClickButtonSizeFixMe)
    ...
  EVENT_MAP_END(CAppDialog)

Nun ist die Klasse MaximizableAppDialog einsatzbereit. Bitte beachten Sie, dass es für den Einsatz im Bereich der Hauptdiagramme konzipiert ist.

Erstens, lassen Sie uns versuchen, es dem Spiel SlidingPuzzle hinzuzufügen. Kopieren Sie SlidingPuzzle2.mq5 und SlidingPuzzle2.mqh als SlidingPuzzle3.mq5 und SlidingPuzzle3.mqh, bevor Sie mit der Bearbeitung beginnen. In der mq5-Datei gibt es fast nichts zu ändern: Ändern Sie nur den Verweis auf die Include-Datei auf SlidingPuzzle3.mqh.

Fügen wir in der Datei SlidingPuzzle3.mqh die neu erstellte Klasse anstelle der Standard-Dialogklasse ein:

  #include <Controls\Dialog.mqh>

einzufügen:

  #include <Layouts\MaximizableAppDialog.mqh>

Die Klassenbeschreibung muss die neue übergeordnete Klasse verwenden:

  class CSlidingPuzzleDialog: public MaximizableAppDialog // CAppDialog

Die ähnliche Ersetzung von Klassennamen sollte in der Ereigniszuordnung durchgeführt werden:

  EVENT_MAP_END(MaximizableAppDialog) // CAppDialog

Auch diese Ersetzung sollte in Create durchgeführt werden:

  bool CSlidingPuzzleDialog::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    if(!MaximizableAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) // CAppDialog
      return (false);
    ...

Schließlich erfordert der neue Dialog die Implementierung der Methode SelfAdjustment, die auf Größenänderungen reagiert.

  void CSlidingPuzzleDialog::SelfAdjustment(const bool minimized = false)
  {
    CSize size;
    size.cx = ClientAreaWidth();
    size.cy = ClientAreaHeight();
    m_main.Size(size);
    m_main.Pack();
  }

Die entsprechende Arbeit wird vom Container m_main ausgeführt: Seine Methode 'Pack' wird für die zuletzt bekannte Größe des Clientbereichs des Fensters aufgerufen.

Das ist absolut ausreichend, um dem Spiel ein adaptives Layout zu geben. Um die Lesbarkeit und Effizienz des Codes zu verbessern, habe ich jedoch das Prinzip der Tastenbedienung in der Anwendung leicht geändert: Jetzt sind sie alle in einem einzigen Array CButton m_buttons[16] zusammengefasst, können anstelle des Operators 'switch' über einen Index aufgerufen werden und werden in einer einzigen Zeile (nach der OnClickButton-Methode) in der Event-Map verarbeitet:

  ON_INDEXED_EVENT(ON_CLICK, m_buttons, OnClickButton)

Sie können den Quellcode des Originalspiels mit dem modifizierten Code vergleichen.

Das Verhalten des adaptiven Fensters ist nachfolgend dargestellt.

Das Spiel SlidingPuzzle

Das Spiel SlidingPuzzle

Ebenso müssen wir die Demo Expert Advisor Experts\Examples\Layouts\Controls2.mq5 ändern: Ihre Hauptdatei mq5 und die Include-Kopfdatei mit der Dialogbeschreibung, die hier unter den neuen Namen Controls3.mq5 und ControlsDialog3.mqh präsentiert werden. Beachten Sie, dass das Spiel einen Container vom Typ Gitter verwendet hat, während der Dialog mit den Steuerelementen basierend auf dem Typ 'Box' aufgebaut ist.

Wenn wir im modifizierten Projekt die gleiche Implementierung der Methode SelfAdjustment, ähnlich der im Spiel verwendeten, belassen, können wir den bisher unbemerkten Fehler leicht erkennen: Die adaptive Fenstergrößenänderung funktioniert nur für das Fenster selbst, hat aber keinen Einfluss auf die Steuerelemente. Wir brauchen die Möglichkeit, die Größe der Steuerelemente an die dynamische Fenstergröße anzupassen.

" Gummi"-Steuerelemente

Verschiedene Standardsteuerelemente haben eine unterschiedliche Anpassung an die dynamische Größenänderung. Einige von ihnen, wie z.B. die Tasten CButton, können auf den Methodenaufruf 'Width' richtig reagieren. Für andere, wie z.B. für die Listen CListView, können wir die Ausrichtung einfach über 'Alignment' einstellen, und das System speichert automatisch den Abstand zwischen dem Steuerelement und dem Fensterrand, was dem Verhalten eines "Gummibandes" entspräche. Einige der Steuerelemente unterstützen jedoch keine der Varianten. Dazu gehören unter anderem CSpinEdit und CComboBox. Um ihnen die neue Fähigkeit hinzuzufügen, müssen wir Unterklassen erstellen.

Für CSpinEdit würde es ausreichen, die virtuelle Methode OnResize zu überschreiben:

  #include <Controls/SpinEdit.mqh> // patch required: private: -> protected:
  
  class SpinEditResizable: public CSpinEdit
  {
    public:
      virtual bool OnResize(void) override
      {
        m_edit.Width(Width());
        m_edit.Height(Height());
        
        int x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_SPIN_BUTTON_X_OFF);
        int y1 = (Height() - 2 * CONTROLS_SPIN_BUTTON_SIZE) / 2;
        m_inc.Move(Left() + x1, Top() + y1);
        
        x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_SPIN_BUTTON_X_OFF);
        y1 = (Height() - 2 * CONTROLS_SPIN_BUTTON_SIZE) / 2 + CONTROLS_SPIN_BUTTON_SIZE;
        m_dec.Move(Left() + x1, Top() + y1);
  
        return CWndContainer::OnResize();
      }
  };

Da CSpinEdit tatsächlich aus 3 Elementen, einem Eingabefeld und zwei Schaltflächen besteht, müssen wir als Reaktion auf eine Größenänderung (nach der OnResize-Methode) das Eingabefeld entsprechend der neuen Größe erhöhen oder verringern und die Schaltflächen an den rechten Rand des Feldes verschieben. Das einzige Problem ist, dass die untergeordneten Elemente m_edit, m_inc und m_dec im privaten Bereich beschrieben sind. Daher müssen wir die Standardbibliothek wieder reparieren. CSpinEdit wurde hier nur zur Demonstration des Ansatzes verwendet, der in diesem Fall einfach zu implementieren ist. Für die echte OLAP-Schnittstelle benötigen wir eine angepasste Dropdown-Liste.

Ein ähnliches Problem kann jedoch bei der Anpassung der Klasse CComboBox auftreten. Bevor wir eine abgeleitete Klasse implementieren, müssen wir einen Patch auf die Basisklasse der CComboBox anwenden, der den Bereich 'private' durch 'protected' ersetzen sollte. Beachten Sie, dass alle diese Patches die Kompatibilität mit anderen Projekten, die die Standardbibliothek verwenden, nicht beeinträchtigen.

Etwas mehr Aufwand ist erforderlich, um die "Gummi"-Combobox zu implementieren. Wir müssen nicht nur OnResize, sondern auch OnClickButton, Enable und Disable überschreiben sowie eine Ereigniszuordnung hinzufügen. Wir verwalten alle untergeordneten Objekte m_edit, m_list und m_drop, d.h. alle Objekte, aus denen die Combobox besteht.

  #include <Controls/ComboBox.mqh> // patch required: private: -> protected:
  
  class ComboBoxResizable: public CComboBox
  {
    public:
      virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) override;
  
      virtual bool OnResize(void) override
      {
        m_edit.Width(Width());
        
        int x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_COMBO_BUTTON_X_OFF);
        int y1 = (Height() - CONTROLS_BUTTON_SIZE) / 2;
        m_drop.Move(Left() + x1, Top() + y1);
        
        m_list.Width(Width());
  
        return CWndContainer::OnResize();
      }
      
      virtual bool OnClickButton(void) override
      {
        // this is a hack to trigger resizing of elements in the list
        // we need it because standard ListView is incorrectly coded in such a way
        // that elements are resized only if vscroll is present
        bool vs = m_list.VScrolled();
        if(m_drop.Pressed())
        {
          m_list.VScrolled(true);
        }
        bool b = CComboBox::OnClickButton();
        m_list.VScrolled(vs);
        return b;
      }
      
      virtual bool Enable(void) override
      {
        m_edit.Show();
        m_drop.Show();
        return CComboBox::Enable();
      }
      
      virtual bool Disable(void) override
      {
        m_edit.Hide();
        m_drop.Hide();
        return CComboBox::Disable();
      }
  };
  
  #define EXIT_ON_DISABLED \
        if(!IsEnabled())   \
        {                  \
          return false;    \
        }
  
  EVENT_MAP_BEGIN(ComboBoxResizable)
    EXIT_ON_DISABLED
    ON_EVENT(ON_CLICK, m_drop, OnClickButton)
  EVENT_MAP_END(CComboBox)

Nun können wir diese "Gummi"-Steuerelemente mit dem Demoprojekt Controls3 überprüfen. Ersetzen wir die Klassen CSpinEdit und CComboBox durch SpinEditResizable bzw. ComboBoxResizable. Ändern Sie die Größen der Steuerelemente in der Methode SelfAdjustment.

  void CControlsDialog::SelfAdjustment(const bool minimized = false)
  {
    CSize min = m_main.GetMinSize();
    CSize size;
    size.cx = ClientAreaWidth();
    size.cy = ClientAreaHeight();
    if(minimized)
    {
      if(min.cx > size.cx) size.cx = min.cx;
      if(min.cy > size.cy) size.cy = min.cy;
    }
    m_main.Size(size);
    int w = (m_button_row.Width() - 2 * 2 * 2 * 3) / 3;
    m_button1.Width(w);
    m_button2.Width(w);
    m_button3.Width(w);
    m_edit.Width(w);
    m_spin_edit.Width(w);
    m_combo_box.Width(m_lists_row.Width() / 2);
    m_main.Pack();
  }

Die Methode SelfAdjustment wird nach der Größenänderung des Fensters automatisch von der übergeordneten Klasse MaximizableAppDialog aufgerufen. Darüber hinaus werden wir diese Methode selbst einmalig, zum Zeitpunkt der Fensterinitialisierung, aus der Methode CreateMain heraus aufrufen.

So kann das in der Realität aussehen (der Einfachheit halber füllen Steuerungen den Arbeitsbereich nur horizontal aus, aber der gleiche Effekt kann vertikal angewendet werden).

Demonstration von Kontrollen

Demonstration von Kontrollen

Die roten Boxen werden hier zum Debuggen angezeigt und können mit dem Makro LAYOUT_BOX_DEBUG deaktiviert werden.

Zusätzlich zu den oben genannten Änderungen habe ich auch das Prinzip der Initialisierung der Steuerelemente leicht modifiziert. Beginnend mit dem Hauptkundenbereich des Fensters wird jeder Block vollständig in einer speziellen Methode initialisiert (z.B. CreateMain, CreateEditRow, CreateButtonRow, etc.), die bei Erfolg eine Referenz auf den erstellten Containertyp (CWnd *) zurückgibt. Der übergeordnete Container fügt eine abgeleitete Klasse hinzu, indem er CWndContainer::Add aufruft. So sieht der Initialisierungsdialog des Hauptdialogs jetzt aus:

  bool CControlsDialog::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
      if(MaximizableAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)
      && Add(CreateMain(chart, name, subwin)))
      {
          return true;
      }
      return false;
  }
  
  CWnd *CControlsDialog::CreateMain(const long chart, const string name, const int subwin)
  {
      m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
      if(m_main.Create(chart, name + "main", subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight())
      && m_main.Add(CreateEditRow(chart, name, subwin))
      && m_main.Add(CreateButtonRow(chart, name, subwin))
      && m_main.Add(CreateSpinDateRow(chart, name, subwin))
      && m_main.Add(CreateListsRow(chart, name, subwin))
      && m_main.Pack())
      {
          SelfAdjustment();
          return &m_main;
      }
      return NULL;
  }

Hier ist die Initialisierung einer Zeile mit Schaltflächen:

  CWnd *CControlsDialog::CreateButtonRow(const long chart, const string name, const int subwin)
  {
      if(m_button_row.Create(chart, name + "buttonrow", subwin, 0, 0, ClientAreaWidth(), BUTTON_HEIGHT * 1.5)
      && m_button_row.Add(CreateButton1())
      && m_button_row.Add(CreateButton2())
      && m_button_row.Add(CreateButton3()))
      {
        m_button_row.Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, 2, 0, 2, 0);
        return &m_button_row;
      }
      return NULL;
  }

Diese Syntax scheint logischer und kompakter zu sein als die bisher verwendete. Allerdings kann der Kontextvergleich von alten und neuen Projekten mit einer solchen Implementierung schwierig sein.

Bei den Steuerelementen gibt es noch mehr zu tun. Vergessen wir nicht, dass der Zweck des Projekts darin besteht, eine grafische Oberfläche für das OLAP zu implementieren. Das zentrale Steuerelement ist daher das "Chart". Das Problem ist, dass es in der Standardbibliothek kein solches Steuerelement gibt. Wir müssen es erstellen.

Die Steuerung "Chart" (CPlot)

Die MQL-Bibliothek bietet mehrere grafische Primitive. Dazu gehören der Hintergrund (CCanvas), Hintergrund-basierte Grafiken (CGraphic) und Grafikobjekte zur Darstellung vorgefertigter Bilder (CChartObjectBitmap, CPicture), die jedoch nicht mit den erforderlichen Grafiken zusammenhängen. Um eines der oben genannten Primitive in eine Fenster-Schnittstelle einzufügen, müssen wir es in die abgeleitete Klasse des entsprechenden Steuerelements umbrechen, die plotten kann. Glücklicherweise gibt es keine Notwendigkeit, diese Aufgabe von Grund auf neu zu lösen. Bitte beachten Sie den auf CGraphic basierenden Artikel PairPlot basiert auf CGraphic dient der Analyse von Korrelationen zwischen Datenarrays (Zeitreihen), der auf dieser Seite veröffentlicht wurde. Es bietet eine gebrauchsfertige Steuerelementklasse, die einen Satz von Charts zur Analyse von Korrelationen zwischen Symbolen enthält. So müssen wir es nur für die Arbeit mit einem einzigen Diagramm in der Kontrolle ändern und erhalten so das gewünschte Ergebnis.

Die Dateien aus dem Artikel werden in das Verzeichnis Include\PairPlot\ installiert. Die Datei, in der sich die Klasse von Interesse befindet, heißt PairPlot.mqh. Basierend auf dieser Datei werden wir unsere Variante unter dem Namen Plot.mqh erstellen. Die wichtigsten Unterschiede:

Wir brauchen die Klasse CTimeserie nicht, also löschen wir sie. Die Steuerungsklasse CPairPlot, die vom CWndClient abgeleitet ist, wird in CPlot transformiert, während ihr Betrieb mit symbolübergreifenden Charts durch ein einziges Diagramm ersetzt wird. Die Charts in den oben genannten Projekten werden mit Hilfe einer speziellen Histogrammklasse (CHistogramm) und der Streudiagrammklasse (CScatter) dargestellt, die von der gemeinsamen CPlotBase-Klasse (die wiederum von CGraphic abgeleitet ist) abgeleitet sind. Wir werden CPlotBase in unsere eigene Klasse CGraphicInPlot konvertieren, die ebenfalls von CGraphic abgeleitet ist. Wir benötigen keine speziellen Diagramme oder Streudiagramme. Stattdessen verwenden wir Standardzeichenstile (CURVE_POINTS, CURVE_LINES, CURVE_POINTS_AND_LINES, CURVE_STEPS, CURVE_HISTOGRAM), die von der CGraphic-Klasse (nämlich der benachbarten Klasse CCurve) bereitgestellt werden. Das vereinfachte Diagramm der Beziehungen zwischen den Klassen ist im Folgenden dargestellt.

Das Diagramm der Beziehungen zwischen den grafischen Klassen

Das Diagramm der Beziehungen zwischen den grafischen Klassen

Die graue Farbe wird für neu hinzugefügte Klassen verwendet, während alle anderen Klassen Standard sind.

Lassen Sie uns den Test-EA PlotDemo erstellen, um das neue Steuerelement zu überprüfen. Initialisierung, Bindung an Ereignisse und Start sind in der Datei PlotDemo.mq5 implementiert, während die Dialogbeschreibung in PlotDemo.mqh enthalten ist (beide Dateien sind angehängt).

Das EA akzeptiert den einzigen Eingabeparameter, den Zeichenstil.

  #include "PlotDemo.mqh"
  
  input ENUM_CURVE_TYPE PlotType = CURVE_POINTS;
  
  CPlotDemo *pPlotDemo;
  
  int OnInit()
  {
      pPlotDemo = new CPlotDemo;
      if(CheckPointer(pPlotDemo) == POINTER_INVALID) return INIT_FAILED;
  
      if(!pPlotDemo.Create(0, "Plot Demo", 0, 20, 20, 800, 600, PlotType)) return INIT_FAILED;
      if(!pPlotDemo.Run()) return INIT_FAILED;
      pPlotDemo.Refresh();
  
      return INIT_SUCCEEDED;
  }
  
  void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
      pPlotDemo.ChartEvent(id, lparam, dparam, sparam);
  }
  
  ...

Erstellen wir unser Kontrollobjekt in der Header-Datei des Dialogs und fügen zwei Testkurven hinzu.

  #include <Controls\Dialog.mqh>
  #include <PairPlot/Plot.mqh>
  #include <Layouts/MaximizableAppDialog.mqh>
  
  class CPlotDemo: public MaximizableAppDialog // CAppDialog
  {
    private:
      CPlot m_plot;
  
    public:
      CPlotDemo() {}
      ~CPlotDemo() {}
  
      bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2, const ENUM_CURVE_TYPE curveType = CURVE_POINTS);
      virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
      bool Refresh(void);
  
      virtual void SelfAdjustment(const bool minimized = false) override
      {
        if(!minimized)
        {
          m_plot.Size(ClientAreaWidth(), ClientAreaHeight());
          m_plot.Resize(0, 0, ClientAreaWidth(), ClientAreaHeight());
        }
        m_plot.Refresh();
      }
  };
  
  EVENT_MAP_BEGIN(CPlotDemo)
  EVENT_MAP_END(MaximizableAppDialog)
  
  bool CPlotDemo::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2, const ENUM_CURVE_TYPE curveType = CURVE_POINTS)
  {
      const int maxw = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      const int maxh = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
      int _x1 = x1;
      int _y1 = y1;
      int _x2 = x2;
      int _y2 = y2;
      if(x2 - x1 > maxw || x2 > maxw)
      {
        _x1 = 0;
        _x2 = _x1 + maxw - 0;
      }
      if(y2 - y1 > maxh || y2 > maxh)
      {
        _y1 = 0;
        _y2 = _y1 + maxh - 1;
      }
      
      if(!MaximizableAppDialog::Create(chart, name, subwin, _x1, _y1, _x2, _y2))
          return false;
      if(!m_plot.Create(m_chart_id, m_name + "Plot", m_subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight(), curveType))
          return false;
      if(!Add(m_plot))
          return false;
      double x[] = {-10, -4, -1, 2, 3, 4, 5, 6, 7, 8};
      double y[] = {-5, 4, -10, 23, 17, 18, -9, 13, 17, 4};
      m_plot.CurveAdd(x, y, "Example 1");
      m_plot.CurveAdd(y, x, "Example 2");
      return true;
  }
  
  bool CPlotDemo::Refresh(void)
  {
      return m_plot.Refresh();
  }

Die Arbeitsweise des Expert Advisors wird im Folgenden veranschaulicht:

Demonstration des Steuerelements mit Grafik

Demonstration des Steuerelements mit Grafik

Wir haben einen großen Teil der Arbeit abgeschlossen und jetzt reichen die Möglichkeiten zur Erstellung einer adaptiven Schnittstelle mit grafischer Unterstützung für das OLAP-Projekt aus. Um zusammenzufassen, werde ich ein Diagramm der wichtigsten Klassen im Zusammenhang mit der grafischen Benutzeroberfläche zeigen.

Diagramm der Steuerelementklassen

Diagramm der Steuerelementklassen

Die weiße Farbe wird für Standardklassen verwendet; die gelbe Farbe für Containerklassen; die rosa Farbe für Klassen von Dialogen und benutzerdefinierten Elementen, die eine Größenänderung unterstützen; die grüne Farbe für die Steuerelemente mit den integrierten Grafiken.

GUI für OLAP

Lassen Sie uns einen neuen Expert Advisor erstellen, der die interaktive Verarbeitung und Visualisierung der daten der Handelshistorie implementiert: OLAPGUI. Alle Operationen, die die Erstellung des Fensters und der Steuerelemente, die Reaktion auf die Benutzeraktion und OLAP-Funktionsaufrufe betreffen, sind in der Header-Datei OLAPGUI.mqh enthalten.

Lassen wir nur die EA-Eingaben, die sich auf den Datenimport aus HTML oder CSV beziehen. Dies betrifft zunächst die Variablen ReportFile, Prefix, Suffix, die Ihnen möglicherweise bereits aus dem ersten OLAPDEMO-Projekt bekannt sind. Wenn ReportFile leer ist, analysiert der EA die Handelshistorie des aktuellen Kontos.

Der Selektor, die Aggregatoren und der Chartstil werden über Steuerelemente ausgewählt. Wir behalten uns die Möglichkeit vor, 3 Dimensionen für den Hyperwürfel einzustellen, d.h. 3 Selektoren für die bedingten Achsen X, Y, Z. Zu diesem Zweck benötigen wir 3 Dropdown-Listen. Platzieren Sie sie in der oberen Reihe des Steuerelements. Näher am rechten Rand der gleichen Zeile, fügen Sie die Schaltfläche Process hinzu, mit der Sie die Analyse starten können.

Die Auswahl der Aggregatorfunktion und des Feldes, nach dem die Aggregation durchgeführt wird, erfolgt über zwei weitere Dropdown-Listen in der zweiten Reihe von Steuerelementen. Fügen wir dort eine Auswahlliste für die Sortierreihenfolge und den Chartstil hinzu. Die Filterung wird eliminiert, um die Benutzeroberfläche zu vereinfachen.

Die verbleibende Fläche wird durch ein Diagramm belegt.

Die Auswahllisten mit den Auswahlmöglichkeiten enthalten den gleichen Satz von Optionen. Es kombiniert die Arten von Selektoren und direkt ausgegebenen Datensätzen. Die nächste Tabelle zeigt die Namen der Steuerelemente und die entsprechenden Felder und/oder Selektortypen.

Die Auswahl der mit * markierten Selektoren wird durch den Aggregatortyp bestimmt: TradeSelector wird für IdentityAggregator verwendet, andernfalls wird QuantizationSelector verwendet.

Die Namen der Selektoren (Punkte 1 bis 9) in der Auswahlliste werden in Anführungszeichen angezeigt.

Die Auswahlschalter sollten nacheinander, von links nach rechts, von X nach Z ausgewählt werden. Die Kombinationsfelder für die nachfolgenden Achsen werden erst nach Auswahl des vorherigen Messwahlschalters ausgeblendet.

Unterstützte Aggregatfunktionen:

Alle Funktionen (außer der letzten) erfordern die Angabe des aggregierten Datensatzfeldes über die Auswahlliste rechts neben dem Aggregator.

Die Funktion "progressive total" bedeutet, dass das "Ordinal" als Selektor entlang der X-Achse gewählt wird (d.h. das sequentielle Durchlaufen von Datensätzen).

Das Kombinationsfeld mit Sortierung ist verfügbar, wenn der einzige Selektor (X) ausgewählt ist.

Die X- und Y-Achse befinden sich jeweils horizontal und vertikal auf dem Diagramm. Für dreidimensionale Hyperwürfel mit unterschiedlichen Koordinaten entlang der Z-Achse habe ich den primitivsten Ansatz gewählt: Mehrere Abschnitte in der Z-Ebene können mit der Schaltfläche Process durchgeblättert werden. Wenn es Z-Koordinaten gibt, ändert sich der Schaltflächenname in "i / n title >>", wobei "i" die Nummer der aktuellen Z-Koordinate ist, "n" die Gesamtzahl der Samples entlang der Z-Achse ist, "title" zeigt an, was entlang der Achse dargestellt wird (z.B. der Wochentag oder die Dealart in Abhängigkeit vom Z-Achsenwahlschalter). Wenn Sie die Konstruktionsbedingung des Hyperwürfels ändern, wird der Titel der Taste wieder auf "Process" gesetzt und beginnt im Normalmodus zu arbeiten. Bitte beachten Sie, dass sich die Verarbeitung für den Aggregator "identity" unterscheidet: In diesem Fall hat der Würfel immer 2 Dimensionen, während alle drei Kurven (für die Felder X, Y und Z) zusammen auf dem Diagramm dargestellt werden, ohne zu scrollen.

Zusätzlich zur grafischen Darstellung wird jeder Würfel auch in einem Protokoll als Text angezeigt. Dies ist besonders wichtig, wenn die Aggregation über einfache Felder und nicht über Selektoren erfolgt. Selektoren ermöglichen die Ausgabe von Labels entlang der Achsen, während das System bei der Quantisierung eines einfachen Feldes nur den Zellindex ausgeben kann. Um beispielsweise den Gewinn nach Losgröße zu analysieren, wählen Sie im X-Selektor das Feld "Lot" und im Feld "profit amount" den Aggregator "sum". Entlang der X-Achse können folgende Werte erscheinen: 0, 0,5, 1, 1,0, 1,0, 1,5 usw. bis zur Anzahl der verschiedenen gehandelten Volumina. Dies sind jedoch Zellzahlen, aber keine Loswerte, während letztere im Protokoll berücksichtigt werden. Das Protokoll enthält die folgende Meldung:

	Selectors: 1
	SumAggregator<TRADE_RECORD_FIELDS> FIELD_PROFIT_AMOUNT [6]
	X: QuantizationSelector(FIELD_LOT) [6]
	===== QuantizationSelector(FIELD_LOT) =====
	      [value] [title]
	[0] 365.96000 "0.01"
	[1]   0.00000 "0.0"
	[2]   4.65000 "0.03"
	[3]  15.98000 "0.06"
	[4]  34.23000 "0.02"
	[5]   0.00000 "1.0"

Hier ist 'value' der Gesamtgewinn,'title' der reale Loswert, der diesem Gewinn entspricht, während die Zahlen auf der linken Seite die Koordinaten entlang der X-Achse sind. Beachten Sie, dass auf dem Diagramm entlang der Achse gebrochene Werte erscheinen, obwohl nur ganzzahlige Indizes sinnvoll sind. Dieser Aspekt der Etikettendarstellung kann unter anderem sicherlich verbessert werden.

Um das GUI-Steuerelement mit dem OLAP-Core (die im ersten Artikel vorgestellte Idee wird unverändert übernommen) in der Header-Datei OLAPcube.mqh zu verbinden, muss die OLAPWrapper-Layer-Klasse implementiert werden. Es verfügt über die gleiche vorbereitende Operation mit Daten, die von der Funktion "process" im ersten Demo-Projekt OLAPDEMO durchgeführt wurde. Jetzt ist es eine Klassenmethode. Jetzt ist es eine Klassenmethode

  class OLAPWrapper
  {
    protected:
      Selector<TRADE_RECORD_FIELDS> *createSelector(const SELECTORS selector, const TRADE_RECORD_FIELDS field);
  
    public:
      void process(
          const SELECTORS &selectorArray[], const TRADE_RECORD_FIELDS &selectorField[],
          const AGGREGATORS AggregatorType, const TRADE_RECORD_FIELDS AggregatorField, Display &display,
          const SORT_BY SortBy = SORT_BY_NONE,
          const double Filter1value1 = 0, const double Filter1value2 = 0)
      {
        int selectorCount = 0;
        for(int i = 0; i < MathMin(ArraySize(selectorArray), 3); i++)
        {
          selectorCount += selectorArray[i] != SELECTOR_NONE;
        }
        ...
        HistoryDataAdapter<CustomTradeRecord> history;
        HTMLReportAdapter<CustomTradeRecord> report;
        CSVReportAdapter<CustomTradeRecord> external;
        
        DataAdapter *adapter = &history;
        
        if(ReportFile != "")
        {
          if(StringFind(ReportFile, ".htm") > 0 && report.load(ReportFile))
          {
            adapter = &report;
          }
          else
          if(StringFind(ReportFile, ".csv") > 0 && external.load(ReportFile))
          {
            adapter = &external;
          }
          else
          {
            Alert("Unknown file format: ", ReportFile);
            return;
          }
        }
        else
        {
          Print("Analyzing account history");
        }
        
        Selector<TRADE_RECORD_FIELDS> *selectors[];
        ArrayResize(selectors, selectorCount);
        
        for(int i = 0; i < selectorCount; i++)
        {
          selectors[i] = createSelector(selectorArray[i], selectorField[i]);
        }
  
        Aggregator<TRADE_RECORD_FIELDS> *aggregator;
        switch(AggregatorType)
        {
          case AGGREGATOR_SUM:
            aggregator = new SumAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
            break;
            ...
        }
        
        Analyst<TRADE_RECORD_FIELDS> *analyst;
        analyst = new Analyst<TRADE_RECORD_FIELDS>(adapter, aggregator, display);
        
        analyst.acquireData();
        ...
        analyst.build();
        analyst.display(SortBy, AggregatorType == AGGREGATOR_IDENTITY);
        ...
      }

Der vollständige Quellcode ist unten angehängt. Beachten Sie, dass alle Einstellungen, die im Projekt OLAPDEMO von den Eingabevariablen empfangen wurden, nun als Parameter der Methode "process" übergeben werden und natürlich basierend auf dem Zustand der Kontrollen gefüllt werden sollten.

Von besonderem Interesse ist der Parameter "display". Der OLAP-Kern deklariert diese spezielle 'Display'-Schnittstelle für die Datenvisualisierung. Jetzt müssen wir es im grafischen Teil des Programms implementieren. Indem wir ein Objekt mit dieser Schnittstelle erstellen, implementieren wir die "Dependency Injection", die im ersten Artikel diskutiert wurde. Dies ermöglicht die Verbindung der neuen Ergebnisanzeigemethode, ohne den OLAP-Kern zu verändern.

Erstellen Sie in der Datei OLAPGUI.mq5 einen Dialog und übergeben Sie das OLAPWrapper-Beispiel an ihn.

  #include "OLAPGUI.mqh"
  
  OLAPWrapper olapcore;
  OLAPDialog dialog(olapcore);
  
  int OnInit()
  {
      if(!dialog.Create(0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), 0,  0, 0, 584, 456)) return INIT_FAILED;
      if(!dialog.Run()) return INIT_FAILED;
      return INIT_SUCCEEDED;
  }
  ...

Die Dialog-Klasse OLAPDialog ist in OLAPGUI.mqh definiert.

  class OLAPDialog;
  
  // since MQL5 does not support multiple inheritence we need this delegate object
  class OLAPDisplay: public Display
  {
    private:
      OLAPDialog *parent;
  
    public:
      OLAPDisplay(OLAPDialog *ptr): parent(ptr) {}
      virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override;
  };
  
  class OLAPDialog: public MaximizableAppDialog
  {
    private:
      CBox m_main;
  
      CBox m_row_1;
      ComboBoxResizable m_axis[AXES_NUMBER];
      CButton m_button_ok;
  
      CBox m_row_2;
      ComboBoxResizable m_algo[ALGO_NUMBER]; // aggregator, field, graph type, sort by
  
      CBox m_row_plot;
      CPlot m_plot;
      ...
      OLAPWrapper *olapcore;
      OLAPDisplay *olapdisplay;
      ...
  
    public:
      OLAPDialog(OLAPWrapper &olapimpl)
      {
        olapcore = &olapimpl;
        olapdisplay = new OLAPDisplay(&this);
      }
      
      ~OLAPDialog(void);
      ...

Als Reaktion auf den Knopf "Process" füllt ein Dialog die notwendigen Parameter für die OLAPWrapper::process-Methode basierend auf der Position des Steuerelements aus und ruft diese Methode auf, während das Olapdisplay-Objekt als Anzeige übergeben wird:

  void OLAPDialog::OnClickButton(void)
  {
    SELECTORS Selectors[4];
    TRADE_RECORD_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    TRADE_RECORD_FIELDS af = (TRADE_RECORD_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
    ...
    
    olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

Der vollständige Code mit allen Einstellungen ist unten angehängt.

Die Hilfsklasse OLAPDisplay wird benötigt, da MQL keine Mehrfachvererbung unterstützt. Die Klasse OLAPDialog ist von MaximizableAppDialog abgeleitet und kann daher die Dialog-Schnittstelle nicht direkt implementieren. Stattdessen wird diese Aufgabe von der Klasse OLAPDisplay übernommen: Ihr Objekt wird innerhalb des Fensters erstellt und durch einen Link zum Entwickler über den Konstruktorparameter bereitgestellt.

Nach der Erstellung des Würfels ruft der OLAP-Kern die Methode OLAPDisplay::display auf:

  void OLAPDisplay::display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
  {
    int consts[];
    int selectorCount = metaData.getDimension();
    ArrayResize(consts, selectorCount);
    ArrayInitialize(consts, 0);
  
    Print(metaData.getMetaCubeTitle(), " [", metaData.getCubeSize(), "]");
    for(int i = 0; i < selectorCount; i++)
    {
      Print(CharToString((uchar)('X' + i)), ": ", metaData.getDimensionTitle(i), " [", metaData.getDimensionRange(i), "]");
    }
    
    if(selectorCount == 1)
    {
      PairArray *result;
      if(metaData.getVector(0, consts, result, sortby))
      {
        Print("===== " + metaData.getDimensionTitle(0) + " =====");
        ArrayPrint(result.array);
        parent.accept1D(result, metaData.getDimensionTitle(0));
      }
      parent.finalize();
      return;
    }
    ...

Der Zweck ist es, die anzuzeigenden Daten (getDimension(), getDimensionTitle(), getVector()) aus dem metaData-Objekt zu erhalten und an das Fenster zu übergeben. Das obige Fragment zeigt die Verarbeitung eines Falles mit einem einzigen Selektor. Spezielle Datenempfangsmethoden sind in der Dialogklasse reserviert:

  void OLAPDialog::accept1D(const PairArray *data, const string title)
  {
    m_plot.CurveAdd(data, title);
  }
  
  void OLAPDialog::accept2D(const double &x[], const double &y[], const string title)
  {
    m_plot.CurveAdd(x, y, title);
  }
  
  void OLAPDialog::finalize()
  {
    m_plot.Refresh();
    m_button_ok.Text("Process");
  }

Hier finden Sie Beispiele für analytische Profile, die mit OLAPGUI grafisch dargestellt werden können.

Gewinn je Symbol, in absteigender Reihenfolge

Gewinn je Symbol, in absteigender Reihenfolge

Gewinn je Symbol, in alphabetischer Reihenfolge

Gewinn je Symbol, in alphabetischer Reihenfolge

Gewinn je Symbol, Tag der Woche, an dem die Position geschlossen wurde, Geschäftsart "Kaufen".

Gewinn je Symbol, Tag der Woche, an dem die Position geschlossen wurde, Geschäftsart "Kaufen".

Gewinn je Symbol, Tag der Woche, an dem die Position geschlossen wurde, Geschäftsart "Verkaufen".

Gewinn je Symbol, Tag der Woche, an dem die Position geschlossen wurde, Geschäftsart "Verkaufen".

Gewinn nach Losgröße (Lose werden als Zellindizes gekennzeichnet, die Werte werden im Protokoll angezeigt)

Gewinn nach Losgröße (Lose werden als Zellindizes gekennzeichnet, die Werte werden im Protokoll angezeigt)

Gesamtsaldenkurve

Gesamtsaldenkurve

Salden für Kaufen und Verkaufen

Salden für Kaufen und Verkaufen

Saldenkurve einzeln für jedes Symbol

Saldenkurve einzeln für jedes Symbol

Swapkurve einzeln für jedes Symbol

Swapkurve einzeln für jedes Symbol

Gewinnabhängigkeit je nach Handelsdauer, einzeln für jedes Symbol

Gewinnabhängigkeit je nach Handelsdauer, einzeln für jedes Symbol

Anzahl der Deals je Symbol und Typ

Anzahl der Deals je Symbol und Typ

Abhängigkeit der Felder "Profit" und "Duration" (in Sekunden) für jeden Deal

Abhängigkeit der Felder "Profit" und "Duration" (in Sekunden) für jeden Deal

Abhängigkeiten von MFE (%) und MAE (%) für alle Deals

Abhängigkeiten von MFE (%) und MAE (%) für alle Deals

Leider sieht der standardmäßige Zeichenstil der Histogramme keine Anzeige mehrerer Arrays mit einem Offset von Spalten verschiedener Arrays mit dem gleichen Index vor. Mit anderen Worten, die Werte mit der gleichen Koordinate können sich vollständig überlappen. Dieses Problem kann durch die Implementierung einer nutzerdefinierten Histogramm-Visualisierungsmethode gelöst werden (dies kann mit der CGraphic-Klasse erfolgen). Aber diese Lösung geht über den Rahmen dieses Artikels hinaus.

Schlussfolgerungen

In diesem Artikel haben wir die allgemeinen Prinzipien der GUI-Erstellung für MQL-Programme überprüft, die Größenänderung und universelles Layout von Steuerelementen unterstützen. Auf Basis dieser Technologie haben wir eine interaktive Anwendung zur Analyse von Handelsberichten entwickelt, die die Entwicklungen aus dem ersten Artikel der OLAP-Serie nutzt. Die Visualisierung beliebiger Kombinationen verschiedener Indikatoren hilft bei der Identifizierung verborgener Muster und vereinfacht die Multikriterienanalyse, die zur Optimierung von Handelssystemen eingesetzt werden kann.

In der folgenden Tabelle finden Sie die Beschreibung der angehängten Dateien.

Das Projekt OLAPGUI

Das Projekt SlidingPuzzle3

Steuerelement Controls3

Das Projekt PlotDemo