English Русский 中文 Español 日本語 Português 한국어 Français Italiano Türkçe
preview
Implementierung eines Tabellenmodells in MQL5: Anwendung des MVC-Konzepts

Implementierung eines Tabellenmodells in MQL5: Anwendung des MVC-Konzepts

MetaTrader 5Beispiele |
16 9
Artyom Trishkin
Artyom Trishkin

Inhalt


Einführung

Bei der Programmierung spielt die Anwendungsarchitektur eine Schlüsselrolle bei der Gewährleistung von Zuverlässigkeit, Skalierbarkeit und einfacher Unterstützung. Einer der Ansätze, der hilft, solche Ziele zu erreichen, ist die Nutzung eines Architekturmusters namens MVC (Model-View-Controller).

Das MVC-Konzept ermöglicht es, eine Anwendung in drei miteinander verbundene Komponenten zu unterteilen: model (Daten- und Logikverwaltung), View (Datenanzeige) und controller (Verarbeitung von Nutzeraktionen). Diese Trennung vereinfacht die Entwicklung, das Testen und die Wartung des Codes und macht ihn strukturierter und flexibler.

In diesem Artikel betrachten wir, wie man MVC-Prinzipien anwendet, um ein Tabellenmodell in der Sprache MQL5 zu implementieren. Tabellen sind ein wichtiges Hilfsmittel zum Speichern, Verarbeiten und Anzeigen von Daten, und ihre richtige Organisation kann die Arbeit mit Informationen erheblich erleichtern. Wir werden Klassen für die Arbeit mit Tabellen erstellen: Tabellenzellen, Zeilen und Tabellenmodell. Um Zellen innerhalb von Zeilen und Zeilen innerhalb des Tabellenmodells zu speichern, werden wir die Klassen der verknüpften Listen aus der MQL5-Standardbibliothek verwenden, die eine effiziente Speicherung und Verwendung von Daten ermöglichen.


Ein wenig über das MVC-Konzept: Was ist es und warum wollen wir es?

Stellen Sie sich die Bewerbung wie eine Theateraufführung vor. Es gibt ein Szenario, das beschreibt, was passieren sollte (dies ist das Modell). Da ist die Bühne – das, was der Betrachter sieht (das ist die View). Und schließlich gibt es den Regisseur, der den gesamten Prozess steuert und andere Elemente miteinander verbindet (dies ist der Controller). Auf diese Weise funktioniert das Architekturmuster MVC – Model-View-Controller.

Dieses Konzept hilft, die Verantwortlichkeiten innerhalb der Anwendung zu trennen. Das Modell ist für die Daten und die Logik zuständig, die Ansicht für die Anzeige und das Erscheinungsbild, und der Controller ist für die Verarbeitung der Nutzeraktionen verantwortlich. Eine solche Trennung macht den Code übersichtlicher, flexibler und bequemer für die Teamarbeit.

Angenommen, Sie erstellen eine Tabelle. Das Modell weiß, welche Zeilen und Zellen es enthält, und weiß, wie es diese ändern kann. Die Ansicht zeichnet eine Tabelle auf dem Bildschirm. Und der Controller reagiert, wenn der Nutzer auf „Zeile hinzufügen“ klickt, und übergibt die Aufgabe an das Modell, um dann die Ansicht zu aktualisieren.

MVC ist besonders nützlich, wenn die Anwendung komplexer wird: neue Funktionen kommen hinzu, die Schnittstelle ändert sich, und mehrere Entwickler arbeiten daran. Mit einer klaren Architektur ist es einfacher, Änderungen vorzunehmen, Komponenten einzeln zu testen und den Code wiederzuverwenden.

Dieser Ansatz hat auch einige Nachteile. Bei sehr einfachen Projekten kann MVC überflüssig sein – man muss sogar das trennen, was in ein paar Funktionen passen könnte. Für skalierbare, ernsthafte Anwendungen macht sich diese Struktur jedoch schnell bezahlt.

Zusammengefasst:

MVC ist eine leistungsstarke architektonische Vorlage, die dabei hilft, den Code zu organisieren und ihn verständlicher, testbarer und skalierbarer zu machen. Sie ist besonders nützlich für komplexe Anwendungen, bei denen eine Trennung von Datenlogik, Nutzeroberfläche und Verwaltung wichtig ist. Bei kleinen Projekten ist seine Verwendung überflüssig.

Das Model-View-Controller-Paradigma passt sehr gut zu unserer Aufgabe. Die Tabelle wird aus unabhängigen Objekten erstellt.:

  • Tabellenzelle.
    Ein Objekt, das einen Wert eines der Typen – Real, Integer oder String – speichert, ist mit Werkzeugen ausgestattet, um den Wert zu verwalten, zu setzen und abzurufen;
  • Tabellenzeile.
    Ein Objekt, das eine Liste von Objekten in Tabellenzellen speichert, ist mit Werkzeugen zur Verwaltung von Zellen, ihrer Position, zum Hinzufügen und Löschen ausgestattet;
  • Ein Tabellenmodell.
    Ein Objekt, das eine Liste von Tabellen-String-Objekten speichert, ist mit Werkzeugen zum Verwalten von Tabellen-Strings und Spalten, ihrer Position, zum Hinzufügen und Löschen ausgestattet und hat außerdem Zugriff auf String- und Zellsteuerungen.

Die folgende Abbildung zeigt schematisch den Aufbau eines 4x4-Tabellenmodells:

Abb.1 4x4 Tabellenmodell

Nun wollen wir von der Theorie zur Praxis übergehen.


Schreiben von Klassen zum Aufbau eines Tabellenmodells

Wir werden die MQL5-Standardbibliothek verwenden, um alle Objekte zu erstellen.

Jedes Objekt wird von der Basisklasse der Bibliothek abgeleitet. Dies ermöglicht es Ihnen, diese Objekte in Objektlisten zu speichern.

Wir werden alle Klassen in eine einzige Testskriptdatei schreiben, sodass alles in einer Datei steht, sichtbar und schnell zugänglich ist. In Zukunft werden wir die geschriebenen Klassen in separate Include-Dateien aufteilen.


1. Verknüpfte Listen als Grundlage für die Speicherung tabellarischer Daten

Die verknüpfte Liste CList ist sehr gut für die Speicherung von Tabellendaten geeignet. Anders als das ähnliche Listen-Objekt CArrayObj implementiert es Zugriffsmethoden auf benachbarte Listenobjekte, die sich links und rechts vom aktuellen befinden. Dies erleichtert das Verschieben von Zellen in einer Zeile oder von Zeilen in einer Tabelle sowie das Hinzufügen und Löschen von Zellen. Gleichzeitig kümmert sich die Liste selbst um die korrekte Indizierung von verschobenen, hinzugefügten oder gelöschten Objekten in der Liste.

Allerdings gibt es hier eine Nuance. Wenn Sie die Methoden zum Laden und Speichern einer Liste in eine Datei betrachten, können Sie sehen, dass die Listenklasse beim Laden aus einer Datei ein neues Objekt in der virtuellen Methode CreateElement() erstellen muss.

Diese Methode in dieser Klasse gibt einfach NULL zurück:

   //--- method of creating an element of the list
   virtual CObject  *CreateElement(void) { return(NULL); }

Das bedeutet, dass wir, um mit verknüpften Listen zu arbeiten, und vorausgesetzt, dass wir Datei-Operationen benötigen, von der Klasse CList erben und diese Methode in unserer Klasse implementieren müssen.

Wenn Sie sich die Methoden zum Speichern von Objekten der Standardbibliothek in einer Datei ansehen, können Sie den folgenden Algorithmus zum Speichern von Objekteigenschaften erkennen:

  1. Die Datenanfangsmarkierung (-1) wird in die Datei geschrieben,
  2. Der Objekttyp wird in die Datei geschrieben,
  3. Alle Objekteigenschaften werden nacheinander in die Datei geschrieben.

Der erste und der zweite Punkt sind allen implementierten Speichern/Laden-Methoden inhärent, die Objekte der Standardbibliothek besitzen. Dementsprechend wollen wir nach der gleichen Logik den Typ des in der Liste gespeicherten Objekts kennen, damit wir beim Lesen aus einer Datei ein Objekt mit diesem Typ in der virtuellen Methode CreateElement() der von CList geerbten Listenklasse erstellen können.

Außerdem müssen alle Objekte, die in die Liste geladen werden können, deklariert oder erstellt werden, bevor die Listenklasse implementiert wird. In diesem Fall „weiß“ die Liste, welche Objekte „in Frage“ sind und welche erstellt werden müssen.

Erstellen Sie im Terminalverzeichnis \MQL5\Scripts\ einen neuen Ordner TableModel\, und darin eine neue Datei des Testskripts TableModelTest.mq5.

Verbinden wir die verknüpfte Listendatei und deklarieren die zukünftige Tabellenmodellklassen:

//+------------------------------------------------------------------+
//|                                               TableModelTest.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Include libraries                                                |
//+------------------------------------------------------------------+
#include <Arrays\List.mqh>

//--- Forward declaration of classes
class CTableCell;                   // Table cell class
class CTableRow;                    // Table row class
class CTableModel;                  // Table model class

Die Vorwärtsdeklaration zukünftiger Klassen ist hier notwendig, damit die Klasse der verknüpften Liste, die von CList erbt, über diese Arten von Klassen Bescheid weiß und auch über die Arten von Objekten, die sie erstellen muss. Zu diesem Zweck werden wir eine Enumeration von Objekttypen, Hilfsmakros und eine Enumeration von Möglichkeiten zur Sortierung von Listen schreiben:

//+------------------------------------------------------------------+
//| Include libraries                                                |
//+------------------------------------------------------------------+
#include <Arrays\List.mqh>

//--- Forward declaration of classes
class CTableCell;                   // Table cell class
class CTableRow;                    // Table row class
class CTableModel;                  // Table model class

//+------------------------------------------------------------------+
//| Macros                                                           |
//+------------------------------------------------------------------+
#define  MARKER_START_DATA    -1    // Data start marker in a file
#define  MAX_STRING_LENGTH    128   // Maximum length of a string in a cell

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_OBJECT_TYPE               // Enumeration of object types
  {
   OBJECT_TYPE_TABLE_CELL=10000,    // Table cell
   OBJECT_TYPE_TABLE_ROW,           // Table row
   OBJECT_TYPE_TABLE_MODEL,         // Table model
  };
  
enum ENUM_CELL_COMPARE_MODE         // Table cell comparison modes
  {
   CELL_COMPARE_MODE_COL,           // Comparison by column number
   CELL_COMPARE_MODE_ROW,           // Comparison by string number
   CELL_COMPARE_MODE_ROW_COL,       // Comparison by row and column
  };
  
//+------------------------------------------------------------------+
//| Functions                                                        |
//+------------------------------------------------------------------+
//--- Return the object type as a string
string TypeDescription(const ENUM_OBJECT_TYPE type)
  {
   string array[];
   int total=StringSplit(EnumToString(type),StringGetCharacter("_",0),array);
   string result="";
   for(int i=2;i<total;i++)
     {
      array[i]+=" ";
      array[i].Lower();
      array[i].SetChar(0,ushort(array[i].GetChar(0)-0x20));
      result+=array[i];
     }
   result.TrimLeft();
   result.TrimRight();
   return result;
  }
//+------------------------------------------------------------------+
//| Classes                                                          |
//+------------------------------------------------------------------+

Die Funktion, die die Beschreibung des Objekttyps zurückgibt, basiert auf der Annahme, dass alle Namen der Objekttypkonstanten mit der Teilzeichenkette „OBJECT_TYPE_“ beginnen. Dann können Sie die darauf folgende Teilzeichenkette nehmen, alle Zeichen der resultierenden Zeile in Kleinbuchstaben umwandeln, das erste Zeichen in Großbuchstaben umwandeln und alle Leer- und Steuerzeichen aus der endgültigen Zeichenkette links und rechts entfernen.

Wir wollen unsere eigene Klasse für verknüpfte Listen schreiben. Wir werden den Code in derselben Datei weiterschreiben:

//+------------------------------------------------------------------+
//| Classes                                                          |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Linked object list class                                         |
//+------------------------------------------------------------------+
class CListObj : public CList
  {
protected:
   ENUM_OBJECT_TYPE  m_element_type;   // Created object type in CreateElement()
public:
//--- Virtual method (1) for loading a list from a file, (2) for creating a list element
   virtual bool      Load(const int file_handle);
   virtual CObject  *CreateElement(void);
  };

Die CListObj-Klasse ist unsere neue verknüpfte Listenklasse, die von der Klasse CList der Standardbibliothek geerbt wurde.

Die einzige Variable in der Klasse wird diejenige sein, in die der Typ des zu erstellenden Objekts geschrieben wird. Da die Methode CreateElement() virtuell ist und genau dieselbe Signatur wie die Methode der übergeordneten Klasse haben muss, können wir den Typ des zu erstellenden Objekts nicht an sie übergeben. Aber wir können diesen Typ in eine deklarierte Variable schreiben und den Typ des zu erstellenden Objekts daraus lesen.

Wir müssen zwei virtuelle Methoden der übergeordneten Klasse umdefinieren: die Methode zum Hochladen aus einer Datei und die Methode zum Erstellen eines neuen Objekts. Betrachten wir sie.

Dier Methode zum Hochladen der Liste aus einer Datei:

//+------------------------------------------------------------------+
//| Load a list from the file                                        |
//+------------------------------------------------------------------+
bool CListObj::Load(const int file_handle)
  {
//--- Variables
   CObject *node;
   bool     result=true;
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Load and check the list start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Load and check the list type
   if(::FileReadInteger(file_handle,INT_VALUE)!=Type())
      return(false);
//--- Read the list size (number of objects)
   uint num=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Sequentially recreate the list elements by calling the Load() method of node objects
   this.Clear();
   for(uint i=0; i<num; i++)
     {
      //--- Read and check the object data start marker - 0xFFFFFFFFFFFFFFFF
      if(::FileReadLong(file_handle)!=MARKER_START_DATA)
         return false;
      //--- Read the object type
      this.m_element_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE);
      node=this.CreateElement();
      if(node==NULL)
         return false;
      this.Add(node);
      //--- Now the file pointer is offset relative to the beginning of the object marker by 12 bytes (8 - marker, 4 - type)
      //--- Set the pointer to the beginning of the object data and load the object properties from the file using the Load() method of the node element.
      if(!::FileSeek(file_handle,-12,SEEK_CUR))
         return false;
      result &=node.Load(file_handle);
     }
//--- Result
   return result;
  }

Hier wird zunächst der Anfang der Liste kontrolliert, ihr Typ und ihre Größe, d.h. die Anzahl der Elemente in der Liste; und dann werden in einer Schleife nach der Anzahl der Elemente die Anfangsdatenmarkierungen jedes Objekts und sein Typ aus der Datei gelesen. Der resultierende Typ wird in die Variable m_element_type geschrieben und eine Methode zur Erstellung eines neuen Elements aufgerufen. Bei dieser Methode wird ein neues Element mit dem empfangenen Typ erstellt und in eine Knotenzeigervariable geschrieben, die ihrerseits der Liste hinzugefügt wird. Die gesamte Logik der Methode wird in den Kommentaren ausführlich erläutert. Betrachten wir eine Methode zur Erstellung eines neuen Listeneintrags.

Die Methode zur Erstellung eines Listeneintrags:

//+------------------------------------------------------------------+
//| List element creation method                                     |
//+------------------------------------------------------------------+
CObject *CListObj::CreateElement(void)
  {
//--- Create a new object depending on the object type in m_element_type 
   switch(this.m_element_type)
     {
      case OBJECT_TYPE_TABLE_CELL   :  return new CTableCell();
      case OBJECT_TYPE_TABLE_ROW    :  return new CTableRow();
      case OBJECT_TYPE_TABLE_MODEL  :  return new CTableModel();
      default                       :  return NULL;
     }
  }

Dies bedeutet, dass vor dem Aufruf der Methode der Typ des zu erstellenden Objekts bereits in die Variable m_element_type geschrieben wird. Je nach Elementtyp wird ein neues Objekt des entsprechenden Typs erstellt und ein Zeiger darauf zurückgegeben. In Zukunft werden bei der Entwicklung neuer Steuerelemente deren Typen in die Aufzählung ENUM_OBJECT_TYPE geschrieben. Außerdem werden hier neue Fälle hinzugefügt, um neue Arten von Objekten zu schaffen. Die Klasse der verketteten Liste, die auf dem Standard CList basiert, ist fertig. Jetzt kann sie alle Objekte bekannter Typen speichern, Listen in einer Datei speichern und aus der Datei hochladen und korrekt wiederherstellen.


2. Tabelle Zellenklasse

Eine Tabellenzelle ist das einfachste Element einer Tabelle, das einen bestimmten Wert speichert. Zellen bilden Listen, die Tabellenzeilen darstellen. Jede Liste steht für eine Tabellenzeile. In unserer Tabelle können die Zellen jeweils nur einen Wert verschiedener Typen speichern – einen Real-, Integer- oder String-Wert.

Zusätzlich zu einem einfachen Wert kann einer Zelle ein Objekt eines bekannten Typs aus der Aufzählung ENUM_OBJECT_TYPE zugewiesen werden. In diesem Fall kann die Zelle einen Wert eines beliebigen der aufgeführten Typen sowie einen Zeiger auf ein Objekt speichern, dessen Typ in eine spezielle Variable geschrieben wird. So kann in Zukunft die View-Komponente angewiesen werden, ein solches Objekt in einer Zelle anzuzeigen, um mit ihm über die Controller-Komponente zu interagieren.

Da mehrere verschiedene Arten von Werten in einer Zelle gespeichert werden können, werden wir Union verwenden, um sie zu schreiben, zu speichern und zurückzugeben. Die Union ist ein spezieller Datentyp, der mehrere Felder im selben Speicherbereich speichert. Eine Vereinigung ähnelt einer Struktur, aber anders als bei einer Struktur gehören hier verschiedene Begriffe der Vereinigung zum selben Speicherbereich. In der Struktur wird jedem Feld ein eigener Speicherbereich zugewiesen.

Fahren wir mit dem Schreiben des Codes in der bereits erstellten Datei fort. Beginnen wir mit dem Schreiben einer neuen Klasse. Im geschützten Bereich schreiben wir eine Union und deklarieren Variablen:

//+------------------------------------------------------------------+
//| Table cell class                                                 |
//+------------------------------------------------------------------+
class CTableCell : public CObject
  {
protected:
//--- Combining for storing cell values (double, long, string)
   union DataType
     {
      protected:
      double         double_value;
      long           long_value;
      ushort         ushort_value[MAX_STRING_LENGTH];

      public:
      //--- Set values
      void           SetValueD(const double value) { this.double_value=value;                   }
      void           SetValueL(const long value)   { this.long_value=value;                     }
      void           SetValueS(const string value) { ::StringToShortArray(value,ushort_value);  }
      
      //--- Return values
      double         ValueD(void) const { return this.double_value; }
      long           ValueL(void) const { return this.long_value; }
      string         ValueS(void) const
                       {
                        string res=::ShortArrayToString(this.ushort_value);
                        res.TrimLeft();
                        res.TrimRight();
                        return res;
                       }
     };
//--- Variables
   DataType          m_datatype_value;                      // Value
   ENUM_DATATYPE     m_datatype;                            // Data type
   CObject          *m_object;                              // Cell object
   ENUM_OBJECT_TYPE  m_object_type;                         // Object type in the cell
   int               m_row;                                 // Row index
   int               m_col;                                 // Column index
   int               m_digits;                              // Data representation accuracy
   uint              m_time_flags;                          // Date/time display flags
   bool              m_color_flag;                          // Color name display flag
   bool              m_editable;                            // Editable cell flag
   
public:

Im öffentlichen Bereich schreiben wir die Zugriffsmethoden auf geschützte Variablen, virtuelle Methoden und Klassenkonstruktoren für verschiedene Arten von Daten, die in einer Zelle gespeichert sind:

public:
//--- Return cell coordinates and properties
   uint              Row(void)                           const { return this.m_row;                            }
   uint              Col(void)                           const { return this.m_col;                            }
   ENUM_DATATYPE     Datatype(void)                      const { return this.m_datatype;                       }
   int               Digits(void)                        const { return this.m_digits;                         }
   uint              DatetimeFlags(void)                 const { return this.m_time_flags;                     }
   bool              ColorNameFlag(void)                 const { return this.m_color_flag;                     }
   bool              IsEditable(void)                    const { return this.m_editable;                       }
//--- Return (1) double, (2) long and (3) string value
   double            ValueD(void)                        const { return this.m_datatype_value.ValueD();        }
   long              ValueL(void)                        const { return this.m_datatype_value.ValueL();        }
   string            ValueS(void)                        const { return this.m_datatype_value.ValueS();        }
//--- Return the value as a formatted string
   string            Value(void) const
                       {
                        switch(this.m_datatype)
                          {
                           case TYPE_DOUBLE  :  return(::DoubleToString(this.ValueD(),this.Digits()));
                           case TYPE_LONG    :  return(::IntegerToString(this.ValueL()));
                           case TYPE_DATETIME:  return(::TimeToString(this.ValueL(),this.m_time_flags));
                           case TYPE_COLOR   :  return(::ColorToString((color)this.ValueL(),this.m_color_flag));
                           default           :  return this.ValueS();
                          }
                       }
   string            DatatypeDescription(void) const
                       {
                        string type=::StringSubstr(::EnumToString(this.m_datatype),5);
                        type.Lower();
                        return type;
                       }
//--- Set variable values
   void              SetRow(const uint row)                    { this.m_row=(int)row;                          }
   void              SetCol(const uint col)                    { this.m_col=(int)col;                          }
   void              SetDatatype(const ENUM_DATATYPE datatype) { this.m_datatype=datatype;                     }
   void              SetDigits(const int digits)               { this.m_digits=digits;                         }
   void              SetDatetimeFlags(const uint flags)        { this.m_time_flags=flags;                      }
   void              SetColorNameFlag(const bool flag)         { this.m_color_flag=flag;                       }
   void              SetEditable(const bool flag)              { this.m_editable=flag;                         }
   void              SetPositionInTable(const uint row,const uint col)
                       {
                        this.SetRow(row);
                        this.SetCol(col);
                       }
//--- Assign an object to a cell
   void              AssignObject(CObject *object)
                       {
                        if(object==NULL)
                          {
                           ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
                           return;
                          }
                        this.m_object=object;
                        this.m_object_type=(ENUM_OBJECT_TYPE)object.Type();
                       }
//--- Remove the object assignment
   void              UnassignObject(void)
                       {
                        this.m_object=NULL;
                        this.m_object_type=-1;
                       }
                       
//--- Set double value
   void              SetValue(const double value)
                       {
                        this.m_datatype=TYPE_DOUBLE;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueD(value);
                       }
//--- Set long value
   void              SetValue(const long value)
                       {
                        this.m_datatype=TYPE_LONG;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueL(value);
                       }
//--- Set datetime value
   void              SetValue(const datetime value)
                       {
                        this.m_datatype=TYPE_DATETIME;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueL(value);
                       }
//--- Set color value
   void              SetValue(const color value)
                       {
                        this.m_datatype=TYPE_COLOR;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueL(value);
                       }
//--- Set string value
   void              SetValue(const string value)
                       {
                        this.m_datatype=TYPE_STRING;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueS(value);
                       }
//--- Clear data
   void              ClearData(void)
                       {
                        if(this.Datatype()==TYPE_STRING)
                           this.SetValue("");
                        else
                           this.SetValue(0.0);
                       }
//--- (1) Return and (2) display the object description in the journal
   string            Description(void);
   void              Print(void);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(OBJECT_TYPE_TABLE_CELL);}
   
   
//--- Constructors/destructor
                     CTableCell(void) : m_row(0), m_col(0), m_datatype(-1), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueD(0);
                       }
                     //--- Accept a double value
                     CTableCell(const uint row,const uint col,const double value,const int digits) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueD(value);
                       }
                     //--- Accept a long value
                     CTableCell(const uint row,const uint col,const long value) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_LONG), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueL(value);
                       }
                     //--- Accept a datetime value
                     CTableCell(const uint row,const uint col,const datetime value,const uint time_flags) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_DATETIME), m_digits(0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueL(value);
                       }
                     //--- Accept color value
                     CTableCell(const uint row,const uint col,const color value,const bool color_names_flag) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_COLOR), m_digits(0), m_time_flags(0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueL(value);
                       }
                     //--- Accept string value
                     CTableCell(const uint row,const uint col,const string value) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_STRING), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueS(value);
                       }
                    ~CTableCell(void) {}
  };

Bei den Methoden zum Setzen von Werten wird zuerst der Typ des in der Zelle gespeicherten Wertes gesetzt und dann das Flag der Funktion zum Bearbeiten von Werten in der Zelle überprüft. Und nur wenn das Flag gesetzt ist, wird der neue Wert in der Zelle gespeichert:

//--- Set double value
   void              SetValue(const double value)
                       {
                        this.m_datatype=TYPE_DOUBLE;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueD(value);
                       }

Warum wird das so gemacht? Bei der Erstellung einer neuen Zelle wird diese mit dem tatsächlichen Typ des gespeicherten Wertes erstellt. Wenn Sie den Typ des Wertes ändern wollen, die Zelle aber gleichzeitig nicht editierbar ist, können Sie die Methode aufrufen, um den Wert des gewünschten Typs mit einem beliebigen Wert zu belegen. Der Typ des gespeicherten Wertes wird geändert, aber der Wert in der Zelle selbst wird nicht beeinflusst.

Die Datenbereinigungsmethode setzt numerische Werte auf Null und fügt in String-Werte ein Leerzeichen ein:

//--- Clear data
   void              ClearData(void)
                       {
                        if(this.Datatype()==TYPE_STRING)
                           this.SetValue("");
                        else
                           this.SetValue(0.0);
                       }

Später werden wir es anders machen – damit keine Daten in gelöschten Zellen angezeigt werden – um die Zelle leer zu halten, werden wir einen „leeren“ Wert für die Zelle machen, und dann, wenn die Zelle gelöscht wird, werden alle Werte, die in ihr aufgezeichnet und angezeigt werden, gelöscht werden. Schließlich ist auch die Null ein vollwertiger Wert, und wenn nun die Zelle gelöscht wird, werden die digitalen Daten mit Nullen aufgefüllt. Dies ist falsch.

In den parametrischen Konstruktoren der Klasse werden die Koordinaten der Zellen in der Tabelle übergeben – die Anzahl der Zeilen und Spalten und der Wert des gewünschten Typs (double, long, datetime, color, string). Einige Arten von Werten erfordern zusätzliche Informationen:

  • double – Genauigkeit des Ausgabewerts (Anzahl der Dezimalstellen),
  • datetime – Flag zur Zeitangabe (Datum/Stunden-Minuten/Sekunden),
  • color – Flag zur Anzeige von Namen bekannter Standardfarben.

In Konstruktoren mit diesen Typen von in Zellen gespeicherten Werten werden zusätzliche Parameter übergeben, um das Format der in den Zellen angezeigten Werte festzulegen:

 //--- Accept a double value
 CTableCell(const uint row,const uint col,const double value,const int digits) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueD(value);
   }
 //--- Accept a long value
 CTableCell(const uint row,const uint col,const long value) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_LONG), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueL(value);
   }
 //--- Accept a datetime value
 CTableCell(const uint row,const uint col,const datetime value,const uint time_flags) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_DATETIME), m_digits(0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueL(value);
   }

 //--- Accept color value
 CTableCell(const uint row,const uint col,const color value,const bool color_names_flag) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_COLOR), m_digits(0), m_time_flags(0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueL(value);
   }
 //--- Accept string value
 CTableCell(const uint row,const uint col,const string value) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_STRING), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueS(value);
   }

Die Methode zum Vergleich zweier Objekte:

//+------------------------------------------------------------------+
//| Compare two objects                                              |
//+------------------------------------------------------------------+
int CTableCell::Compare(const CObject *node,const int mode=0) const
  {
   const CTableCell *obj=node;
   switch(mode)
     {
      case CELL_COMPARE_MODE_COL :  return(this.Col()>obj.Col() ? 1 : this.Col()<obj.Col() ? -1 : 0);
      case CELL_COMPARE_MODE_ROW :  return(this.Row()>obj.Row() ? 1 : this.Row()<obj.Row() ? -1 : 0);
      //---CELL_COMPARE_MODE_ROW_COL
      default                    :  return
                                      (
                                       this.Row()>obj.Row() ? 1 : this.Row()<obj.Row() ? -1 :
                                       this.Col()>obj.Col() ? 1 : this.Col()<obj.Col() ? -1 : 0
                                      );
     }
  }

Die Methode ermöglicht den Vergleich von Parametern zweier Objekte nach einem von drei Vergleichskriterien – nach Spaltennummer, nach Zeilennummer oder gleichzeitig nach Zeilen- und Spaltennummer.

Diese Methode ist notwendig, um Tabellenzeilen nach Werten von Tabellenspalten sortieren zu können. Dies wird von Controller in späteren Artikeln behandelt.

Die Methode zum Speichern von Zelleigenschaften in einer Datei:

//+------------------------------------------------------------------+
//| Save to file                                                     |
//+------------------------------------------------------------------+
bool CTableCell::Save(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long))
      return(false);
//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return(false);

   //--- Save the data type
   if(::FileWriteInteger(file_handle,this.m_datatype,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Save the object type in the cell
   if(::FileWriteInteger(file_handle,this.m_object_type,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Save the row index
   if(::FileWriteInteger(file_handle,this.m_row,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Save the column index
   if(::FileWriteInteger(file_handle,this.m_col,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Maintain the accuracy of data representation
   if(::FileWriteInteger(file_handle,this.m_digits,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Save date/time display flags
   if(::FileWriteInteger(file_handle,this.m_time_flags,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Save the color name display flag
   if(::FileWriteInteger(file_handle,this.m_color_flag,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Save the edited cell flag
   if(::FileWriteInteger(file_handle,this.m_editable,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Save the value
   if(::FileWriteStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value))
      return(false);
   
//--- All is successful
   return true;
  }

Nach dem Schreiben der Startdatenmarkierung und des Objekttyps in die Datei werden nacheinander alle Zelleneigenschaften gespeichert. Die Vereinigung muss als Struktur mit FileWriteStruct() gespeichert werden.

Die Methode zum Laden von Zelleigenschaften aus einer Datei:

//+------------------------------------------------------------------+
//| Load from file                                                   |
//+------------------------------------------------------------------+
bool CTableCell::Load(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Load the object type
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return(false);

   //--- Load the data type
   this.m_datatype=(ENUM_DATATYPE)::FileReadInteger(file_handle,INT_VALUE);
   //--- Load the object type in the cell
   this.m_object_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE);
   //--- Load the row index
   this.m_row=::FileReadInteger(file_handle,INT_VALUE);
   //--- Load the column index
   this.m_col=::FileReadInteger(file_handle,INT_VALUE);
   //--- Load the precision of the data representation
   this.m_digits=::FileReadInteger(file_handle,INT_VALUE);
   //--- Load date/time display flags
   this.m_time_flags=::FileReadInteger(file_handle,INT_VALUE);
   //--- Load the color name display flag
   this.m_color_flag=::FileReadInteger(file_handle,INT_VALUE);
   //--- Load the edited cell flag
   this.m_editable=::FileReadInteger(file_handle,INT_VALUE);
   //--- Load the value
   if(::FileReadStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value))
      return(false);
   
//--- All is successful
   return true;
  }

Nach dem Lesen und Prüfen der Datenanfangsmarkierungen und des Objekttyps werden alle Eigenschaften des Objekts der Reihe nach in der gleichen Reihenfolge geladen, in der sie gespeichert wurden. Unions werden mit FileReadStruct() gelesen.

Die Methode, die eine Beschreibung des Objekts liefert:

//+------------------------------------------------------------------+
//| Return the object description                                    |
//+------------------------------------------------------------------+
string CTableCell::Description(void)
  {
   return(::StringFormat("%s: Row %u, Col %u, %s <%s>Value: %s",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Row(),this.Col(),
                         (this.m_editable ? "Editable" : "Uneditable"),this.DatatypeDescription(),this.Value()));
  }

Hier wird eine Zeile aus einigen Zellparametern erstellt und z. B. für double in diesem Format zurückgegeben:

  Table Cell: Row 2, Col 2, Uneditable <double>Value: 0.00

Die Methode, die eine Objektbeschreibung in das Protokoll ausgibt:

//+------------------------------------------------------------------+
//| Display the object description in the journal                    |
//+------------------------------------------------------------------+
void CTableCell::Print(void)
  {
   ::Print(this.Description());
  }

Hier wird die Objektbeschreibung einfach in das Protokoll gedruckt.

//+------------------------------------------------------------------+
//| Table row class                                                  |
//+------------------------------------------------------------------+
class CTableRow : public CObject
  {
protected:
   CTableCell        m_cell_tmp;                            // Cell object to search in the list
   CListObj          m_list_cells;                          // List of cells
   uint              m_index;                               // Row index
   
//--- Add the specified cell to the end of the list
   bool              AddNewCell(CTableCell *cell);
   
public:
//--- (1) Set and (2) return the row index
   void              SetIndex(const uint index)                { this.m_index=index;  }
   uint              Index(void)                         const { return this.m_index; }
//--- Set the row and column positions to all cells
   void              CellsPositionUpdate(void);
   
//--- Create a new cell and add it to the end of the list
   CTableCell       *CreateNewCell(const double value);
   CTableCell       *CreateNewCell(const long value);
   CTableCell       *CreateNewCell(const datetime value);
   CTableCell       *CreateNewCell(const color value);
   CTableCell       *CreateNewCell(const string value);
   
//--- Return (1) the cell by index and (2) the number of cells
   CTableCell       *GetCell(const uint index)                 { return this.m_list_cells.GetNodeAtIndex(index);  }
   uint              CellsTotal(void)                    const { return this.m_list_cells.Total();                }
   
//--- Set the value to the specified cell
   void              CellSetValue(const uint index,const double value);
   void              CellSetValue(const uint index,const long value);
   void              CellSetValue(const uint index,const datetime value);
   void              CellSetValue(const uint index,const color value);
   void              CellSetValue(const uint index,const string value);
//--- (1) assign to a cell and (2) remove an assigned object from the cell
   void              CellAssignObject(const uint index,CObject *object);
   void              CellUnassignObject(const uint index);
   
//--- (1) Delete and (2) move the cell
   bool              CellDelete(const uint index);
   bool              CellMoveTo(const uint cell_index, const uint index_to);
   
//--- Reset the data of the row cells
   void              ClearData(void);

//--- (1) Return and (2) display the object description in the journal
   string            Description(void);
   void              Print(const bool detail, const bool as_table=false, const int cell_width=10);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(OBJECT_TYPE_TABLE_ROW); }
   
//--- Constructors/destructor
                     CTableRow(void) : m_index(0) {}
                     CTableRow(const uint index) : m_index(index) {}
                    ~CTableRow(void){}
  };


3. Klasse der Tabellenzeile

Eine Tabellenzeile ist im Wesentlichen eine verknüpfte Liste von Zellen. Die Zeilenklasse muss das Hinzufügen, Löschen und Umordnen von Zellen in der Liste an eine neue Position unterstützen. Die Klasse muss über ein Mindestmaß an Methoden zur Verwaltung der Zellen in der Liste verfügen.

Schreiben wir den Code weiter in dieselbe Datei. In den Klassenparametern ist nur eine Variable verfügbar – der Zeilenindex in der Tabelle. Alle anderen sind Methoden für die Arbeit mit Zeileneigenschaften und mit einer Liste ihrer Zellen.

Betrachten wir die Methoden der Klasse.

Die Methode zum Vergleich zweier Tabellenzeilen:

//+------------------------------------------------------------------+
//| Compare two objects                                              |
//+------------------------------------------------------------------+
int CTableRow::Compare(const CObject *node,const int mode=0) const
  {
   const CTableRow *obj=node;
   return(this.Index()>obj.Index() ? 1 : this.Index()<obj.Index() ? -1 : 0);
  }

Da Zeilen nur über ihren einzigen Parameter – den Zeilenindex – verglichen werden können, wird dieser Vergleich hier durchgeführt. Diese Methode wird benötigt, um die Tabellenzeilen zu sortieren.

Überladene Methoden zur Erstellung von Zellen mit verschiedenen Arten von gespeicherten Daten:

//+------------------------------------------------------------------+
//| Create a new double cell and add it to the end of the list       |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const double value)
  {
//--- Create a new cell object storing a value of double type
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,2);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Add the created cell to the end of the list
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Return the pointer to the object
   return cell;
  }
//+------------------------------------------------------------------+
//| Create a new long cell and add it to the end of the list         |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const long value)
  {
//--- Create a new cell object storing a long value
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Add the created cell to the end of the list
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Return the pointer to the object
   return cell;
  }
//+------------------------------------------------------------------+
//| Create a new datetime cell and add it to the end of the list     |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const datetime value)
  {
//--- Create a new cell object storing a value of datetime type
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Add the created cell to the end of the list
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Return the pointer to the object
   return cell;
  }
//+------------------------------------------------------------------+
//| Create a new color cell and add it to the end of the list        |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const color value)
  {
//--- Create a new cell object storing a value of color type
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,true);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Add the created cell to the end of the list
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Return the pointer to the object
   return cell;
  }
//+------------------------------------------------------------------+
//| Create a new string cell and add it to the end of the list       |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const string value)
  {
//--- Create a new cell object storing a value of string type
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Add the created cell to the end of the list
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Return the pointer to the object
   return cell;
  }

Fünf Methoden, um eine neue Zelle zu erstellen (double, long, datetime, color, string) und sie am Ende der Listen hinzuzufügen. Zusätzliche Parameter des Datenausgabeformats in die Zelle werden mit Standardwerten eingestellt. Sie können nach der Erstellung der Zelle geändert werden. Zunächst wird ein neues Zellenobjekt erstellt und dann dem Listenende hinzugefügt. Wenn das Objekt nicht erstellt wurde, wird es gelöscht, um Speicherlecks zu vermeiden.

Die Methode, die dem Listenende eine Zelle hinzufügt:

//+------------------------------------------------------------------+
//| Add a cell to the end of the list                                |
//+------------------------------------------------------------------+
bool CTableRow::AddNewCell(CTableCell *cell)
  {
//--- If an empty object is passed, report it and return 'false'
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Empty CTableCell object passed",__FUNCTION__);
      return false;
     }
//--- Set the cell index in the list and add the created cell to the end of the list
   cell.SetPositionInTable(this.m_index,this.CellsTotal());
   if(this.m_list_cells.Add(cell)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add cell (%u,%u) to list",__FUNCTION__,this.m_index,this.CellsTotal());
      return false;
     }
//--- Successful
   return true;
  }

Jede neu erstellte Zelle wird immer an das Listenende angehängt. Anschließend kann sie mit Hilfe von Methoden der später zu erstellenden Klasse des Tabellenmodells an die entsprechende Stelle verschoben werden.

Überladene Methoden zum Setzen von Werten in der angegebenen Zelle:

//+------------------------------------------------------------------+
//| Set the double value to the specified cell                       |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const double value)
  {
//--- Get the required cell from the list and set a new value into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Set a long value to the specified cell                           |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const long value)
  {
//--- Get the required cell from the list and set a new value into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Set the datetime value to the specified cell                     |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const datetime value)
  {
//--- Get the required cell from the list and set a new value into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Set the color value to the specified cell                        |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const color value)
  {
//--- Get the required cell from the list and set a new value into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Set a string value to the specified cell                         |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const string value)
  {
//--- Get the required cell from the list and set a new value into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }

Anhand des Indexes holen wir die gewünschte Zelle aus der Liste und setzen den Wert für sie. Wenn die Zelle nicht editierbar ist, setzt die Methode SetValue() des Zellenobjekts für die Zelle nur den Typ des zu setzenden Wertes. Der Wert selbst wird nicht gesetzt.

Eine Methode, die einer Zelle ein Objekt zuweist:

//+------------------------------------------------------------------+
//| Assign an object to the cell                                     |
//+------------------------------------------------------------------+
void CTableRow::CellAssignObject(const uint index,CObject *object)
  {
//--- Get the required cell from the list and set a pointer to the object into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.AssignObject(object);
  }

Wir erhalten ein Zellobjekt über seinen Index und verwenden seine Methode AssignObject(), um der Zelle einen Zeiger auf das Objekt zuzuweisen.

Die Methode, die ein zugewiesenes Objekt für eine Zelle aufhebt:

//+------------------------------------------------------------------+
//| Cancel the assigned object for the cell                          |
//+------------------------------------------------------------------+
void CTableRow::CellUnassignObject(const uint index)
  {
//--- Get the required cell from the list and cancel the pointer to the object and its type in it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.UnassignObject();
  }

Wir holen uns das Zellenobjekt über seinen Index und verwenden seine Methode UnassignObject(), um den Zeiger auf das der Zelle zugewiesene Objekt zu entfernen.

Die Methode, die eine Zelle löscht:

//+------------------------------------------------------------------+
//| Delete a cell                                                    |
//+------------------------------------------------------------------+
bool CTableRow::CellDelete(const uint index)
  {
//--- Delete a cell in the list by index
   if(!this.m_list_cells.Delete(index))
      return false;
//--- Update the indices for the remaining cells in the list
   this.CellsPositionUpdate();
   return true;
  }

Mit der Methode Delete() der Klasse CList löschen wir die Zelle aus der Liste. Nachdem eine Zelle aus der Liste gelöscht wurde, werden die Indizes der verbleibenden Zellen geändert. Mit der Methode CellsPositionUpdate() aktualisieren wir die Indizes aller verbleibenden Zellen in der Liste.

Die Methode, die eine Zelle an die angegebene Position verschiebt:

//+------------------------------------------------------------------+
//| Moves the cell to the specified position                         |
//+------------------------------------------------------------------+
bool CTableRow::CellMoveTo(const uint cell_index,const uint index_to)
  {
//--- Select the desired cell by index in the list, turning it into the current one
   CTableCell *cell=this.GetCell(cell_index);
//--- Move the current cell to the specified position in the list
   if(cell==NULL || !this.m_list_cells.MoveToIndex(index_to))
      return false;
//--- Update the indices of all cells in the list
   this.CellsPositionUpdate();
   return true;
  }

Damit die Klasse CList mit einem Objekt arbeiten kann, muss dieses Objekt in der Liste das aktuelle sein. Sie wird z. B. aktuell, wenn sie ausgewählt wird. Deshalb holen wir hier zunächst das Zellenobjekt aus der Liste nach Index. Die Zelle wird zur aktuellen Zelle, und mit der Methode MoveToIndex() der Klasse CList verschieben wir das Objekt an die gewünschte Position in der Liste. Nachdem ein Objekt erfolgreich an eine neue Position verschoben wurde, müssen die Indizes der verbleibenden Objekte angepasst werden, was mit der Methode CellsPositionUpdate() geschieht.

Die Methode, die Zeilen- und Spaltenpositionen für alle Zellen in der liste festlegt:

//+------------------------------------------------------------------+
//| Set the row and column positions to all cells                    |
//+------------------------------------------------------------------+
void CTableRow::CellsPositionUpdate(void)
  {
//--- In the loop through all cells in the list
   for(int i=0;i<this.m_list_cells.Total();i++)
     {
      //--- get the next cell and set the row and column indices in it
      CTableCell *cell=this.GetCell(i);
      if(cell!=NULL)
         cell.SetPositionInTable(this.Index(),this.m_list_cells.IndexOf(cell));
     }
  }

Die Klasse CList ermöglicht es Ihnen, den aktuellen Objektindex in der Liste zu finden. Dazu muss das Objekt ausgewählt sein. Hier durchlaufen wir eine Schleife durch alle Zellenobjekte in der Liste, wählen jedes einzelne aus und ermitteln seinen Index mit der Methode IndexOf() der Klasse CList. Der Zeilenindex und der gefundene Zellenindex werden mit der Methode SetPositionInTable() auf das Zellenobjekt gesetzt.

Die Methode zum Zurücksetzen der Daten von Zeilen:

//+------------------------------------------------------------------+
//| Reset the cell data of a row                                     |
//+------------------------------------------------------------------+
void CTableRow::ClearData(void)
  {
//--- In the loop through all cells in the list
   for(uint i=0;i<this.CellsTotal();i++)
     {
      //--- get the next cell and set an empty value to it
      CTableCell *cell=this.GetCell(i);
      if(cell!=NULL)
         cell.ClearData();
     }
  }

Setzen Sie in der Schleife jede nächste Zelle in der Liste mit der Zellobjektmethode ClearData() zurück. Bei Zeichenkettendaten wird eine leere Zeile in die Zelle geschrieben, bei numerischen Daten wird eine Null geschrieben.

Die Methode, die eine Beschreibung des Objekts liefert:

//+------------------------------------------------------------------+
//| Return the object description                                    |
//+------------------------------------------------------------------+
string CTableRow::Description(void)
  {
   return(::StringFormat("%s: Position %u, Cells total: %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Index(),this.CellsTotal()));
  }

Eine Zeile wird aus den Eigenschaften und Daten des Objekts zusammengestellt und beispielsweise im folgenden Format zurückgegeben:

Table Row: Position 1, Cells total: 4:

Die Methode, die eine Objektbeschreibung in das Protokoll ausgibt:

//+------------------------------------------------------------------+
//| Display the object description in the journal                    |
//+------------------------------------------------------------------+
void CTableRow::Print(const bool detail, const bool as_table=false, const int cell_width=10)
  {
      
//--- Number of cells
   int total=(int)this.CellsTotal();
   
//--- If the output is in tabular form
   string res="";
   if(as_table)
     {
      //--- create a table row from the values of all cells
      string head=" Row "+(string)this.Index();
      string res=::StringFormat("|%-*s |",cell_width,head);
      for(int i=0;i<total;i++)
        {
         CTableCell *cell=this.GetCell(i);
         if(cell==NULL)
            continue;
         res+=::StringFormat("%*s |",cell_width,cell.Value());
        }
      //--- Display a row in the journal
      ::Print(res);
      return;
     }
     
//--- Display the header as a row description
   ::Print(this.Description()+(detail ? ":" : ""));
   
//--- If detailed description
   if(detail)
     {
      
      //--- The output is not in tabular form
      //--- In the loop through the list of cells in the row
      for(int i=0; i<total; i++)
        {
         //--- get the current cell and add its description to the final row
         CTableCell *cell=this.GetCell(i);
         if(cell!=NULL)
            res+="  "+cell.Description()+(i<total-1 ? "\n" : "");
        }
      //--- Send the row created in the loop to the journal
      ::Print(res);
     }
  }

Bei nicht-tabellarischer Datenanzeige im Protokoll wird die Überschrift zunächst als Zeilenbeschreibung im Protokoll angezeigt. Wenn das Flag für die detaillierte Anzeige gesetzt ist, werden die Beschreibungen der einzelnen Zellen im Protokoll in einer Schleife durch die Liste der Zellen angezeigt.

Die detaillierte Anzeige einer Tabellenzeile im Protokoll sieht dann z.B. so aus (bei einer nicht-tabellarischen Ansicht):

Table Row: Position 0, Cells total: 4:
  Table Cell: Row 0, Col 0, Editable <long>Value: 10
  Table Cell: Row 0, Col 1, Editable <long>Value: 21
  Table Cell: Row 0, Col 2, Editable <long>Value: 32
  Table Cell: Row 0, Col 3, Editable <long>Value: 43

Bei einer tabellarischen Darstellung sieht das Ergebnis zum Beispiel wie folgt aus:

| Row 0     |         0 |         1 |         2 |         3 |

Die Methode zum Speichern einer Tabellenzeile in einer Datei:

//+------------------------------------------------------------------+
//| Save to file                                                     |
//+------------------------------------------------------------------+
bool CTableRow::Save(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long))
      return(false);
//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return(false);

//--- Save the index
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return(false);
//--- Save the list of cells
   if(!this.m_list_cells.Save(file_handle))
      return(false);
   
//--- Successful
   return true;
  }

Speichern Sie die Datenanfangsmarkierungen, dann den Objekttyp. Dies ist der Standard-Header für jedes Objekt in der Datei. Danach folgt ein Eintrag in die Eigenschaftsdatei des Objekts. Hier wird der Zeilenindex in der Datei gespeichert und dann die Liste der Zellen.

Die Methode zum Hochladen der Zeile aus einer Datei:

//+------------------------------------------------------------------+
//| Load from file                                                   |
//+------------------------------------------------------------------+
bool CTableRow::Load(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Load the object type
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return(false);

//--- Load the index
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the list of cells
   if(!this.m_list_cells.Load(file_handle))
      return(false);
   
//--- Successful
   return true;
  }

Hier ist alles in der gleichen Reihenfolge wie beim Speichern. Zunächst werden die Datenanfangsmarkierung und der Objekttyp geladen und geprüft. Dann werden der Zeilenindex und die gesamte Liste der Zellen geladen.


4. Die Klasse des Tabellenmodels

In seiner einfachsten Form ist das Tabellenmodell eine verknüpfte Liste von Zeilen, die ihrerseits verknüpfte Listen von Zellen enthalten. Unser Modell, das wir heute erstellen, empfängt ein zweidimensionales Array von einem der fünf Typen (double, long, datetime, color, string) als Eingabe und erstellt daraus eine virtuelle Tabelle. Außerdem werden wir diese Klasse erweitern, um andere Argumente für die Erstellung von Tabellen aus anderen Eingabedaten zu akzeptieren. Das gleiche Modell wird als Standardmodell betrachtet.

Schreiben wir den Code weiter in dieselbe Datei \MQL5\Scripts\TableModel\TableModelTest.mq5.

Die Klasse des Tabellenmodells ist im Wesentlichen eine verknüpfte Liste von Zeilen mit Methoden zur Verwaltung von Zeilen, Spalten und Zellen. Wir schreiben den Klassenkörper mit allen Variablen und Methoden und berücksichtigen dann die deklarierten Methoden der Klasse:

//+------------------------------------------------------------------+
//| Table model class                                                |
//+------------------------------------------------------------------+
class CTableModel : public CObject
  {
protected:
   CTableRow         m_row_tmp;                             // Row object to search in the list
   CListObj          m_list_rows;                           // List of table rows
//--- Create a table model from a two-dimensional array
template<typename T>
   void              CreateTableModel(T &array[][]);
//--- Return the correct data type
   ENUM_DATATYPE     GetCorrectDatatype(string type_name)
                       {
                        return
                          (
                           //--- Integer value
                           type_name=="bool" || type_name=="char"    || type_name=="uchar"   ||
                           type_name=="short"|| type_name=="ushort"  || type_name=="int"     ||
                           type_name=="uint" || type_name=="long"    || type_name=="ulong"   ?  TYPE_LONG      :
                           //--- Real value
                           type_name=="float"|| type_name=="double"                          ?  TYPE_DOUBLE    :
                           //--- Date/time value
                           type_name=="datetime"                                             ?  TYPE_DATETIME  :
                           //--- Color value
                           type_name=="color"                                                ?  TYPE_COLOR     :
                           /*--- String value */                                          TYPE_STRING    );
                       }
     
//--- Create and add a new empty string to the end of the list
   CTableRow        *CreateNewEmptyRow(void);
//--- Add a string to the end of the list
   bool              AddNewRow(CTableRow *row);
//--- Set the row and column positions to all table cells
   void              CellsPositionUpdate(void);
   
public:
//--- Return (1) cell, (2) row by index, number (3) of rows, cells (4) in the specified row and (5) in the table
   CTableCell       *GetCell(const uint row, const uint col);
   CTableRow        *GetRow(const uint index)                  { return this.m_list_rows.GetNodeAtIndex(index);   }
   uint              RowsTotal(void)                     const { return this.m_list_rows.Total();  }
   uint              CellsInRow(const uint index);
   uint              CellsTotal(void);

//--- Set (1) value, (2) precision, (3) time display flags and (4) color name display flag to the specified cell
template<typename T>
   void              CellSetValue(const uint row, const uint col, const T value);
   void              CellSetDigits(const uint row, const uint col, const int digits);
   void              CellSetTimeFlags(const uint row, const uint col, const uint flags);
   void              CellSetColorNamesFlag(const uint row, const uint col, const bool flag);
//--- (1) Assign and (2) cancel the object in the cell
   void              CellAssignObject(const uint row, const uint col,CObject *object);
   void              CellUnassignObject(const uint row, const uint col);
//--- (1) Delete and (2) move the cell
   bool              CellDelete(const uint row, const uint col);
   bool              CellMoveTo(const uint row, const uint cell_index, const uint index_to);
   
//--- (1) Return and (2) display the cell description and (3) the object assigned to the cell
   string            CellDescription(const uint row, const uint col);
   void              CellPrint(const uint row, const uint col);
   CObject          *CellGetObject(const uint row, const uint col);

public:
//--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Remove or (2) relocate the row, (3) clear the row data
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowResetData(const uint index);
//--- (1) Return and (2) display the row description in the journal
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Remove or (2) relocate the column, (3) clear the column data
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint row_index, const uint index_to);
   void              ColumnResetData(const uint index);
   
//--- (1) Return and (2) display the table description in the journal
   string            Description(void);
   void              Print(const bool detail);
   void              PrintTable(const int cell_width=10);
   
//--- (1) Clear the data, (2) destroy the model
   void              ClearData(void);
   void              Destroy(void);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(OBJECT_TYPE_TABLE_MODEL);  }
   
//--- Constructors/destructor
                     CTableModel(void){}
                     CTableModel(double &array[][])   { this.CreateTableModel(array); }
                     CTableModel(long &array[][])     { this.CreateTableModel(array); }
                     CTableModel(datetime &array[][]) { this.CreateTableModel(array); }
                     CTableModel(color &array[][])    { this.CreateTableModel(array); }
                     CTableModel(string &array[][])   { this.CreateTableModel(array); }
                    ~CTableModel(void){}
  };

Im Grunde gibt es nur ein Objekt für eine verknüpfte Liste von Tabellenzeilen und Methoden zur Verwaltung von Zeilen, Zellen und Spalten. Und Konstruktoren, die verschiedene Typen von zweidimensionalen Arrays akzeptieren.

Dem Klassenkonstruktor wird ein Array übergeben, und es wird eine Methode zur Erstellung eines Tabellenmodells aufgerufen:

//+------------------------------------------------------------------+
//| Create the table model from a two-dimensional array              |
//+------------------------------------------------------------------+
template<typename T>
void CTableModel::CreateTableModel(T &array[][])
  {
//--- Get the number of table rows and columns from the array properties
   int rows_total=::ArrayRange(array,0);
   int cols_total=::ArrayRange(array,1);
//--- In a loop by row indices
   for(int r=0; r<rows_total; r++)
     {
      //--- create a new empty row and add it to the end of the list of rows
      CTableRow *row=this.CreateNewEmptyRow();
      //--- If a row is created and added to the list,
      if(row!=NULL)
        {
         //--- In the loop by the number of cells in a row, 
         //--- create all the cells, adding each new one to the end of the list of cells in the row
         for(int c=0; c<cols_total; c++)
            row.CreateNewCell(array[r][c]);
        }
     }
  }

Die Logik der Methode wird in den Kommentaren erläutert. Die erste Dimension des Arrays sind die Tabellenzeilen, die zweite die Zellen der einzelnen Zeilen. Bei der Erstellung von Zellen wird derselbe Datentyp verwendet, der als Array an die Methode übergeben wird.

So können wir mehrere Tabellenmodelle erstellen, deren Zellen zunächst unterschiedliche Datentypen speichern (double, long, datetime, color und string). Später, nach der Erstellung des Tabellenmodells, können die in den Zellen gespeicherten Datentypen geändert werden.

Die Methode, die eine neue leere Zeile erstellt und an das Ende der Liste anfügt:

//+------------------------------------------------------------------+
//| Create a new empty string and add it to the end of the list      |
//+------------------------------------------------------------------+
CTableRow *CTableModel::CreateNewEmptyRow(void)
  {
//--- Create a new row object
   CTableRow *row=new CTableRow(this.m_list_rows.Total());
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new row at position %u",__FUNCTION__, this.m_list_rows.Total());
      return NULL;
     }
//--- If failed to add the row to the list, remove the newly created object and return NULL
   if(!this.AddNewRow(row))
     {
      delete row;
      return NULL;
     }
   
//--- Success - return the pointer to the created object
   return row;
  }

Die Methode erstellt ein neues Objekt der Klasse CTableRow und fügt es mit der Methode AddNewRow() an das Ende der Zeilenliste an. Tritt ein Additionsfehler auf, wird das neu erstellte Objekt gelöscht und NULL zurückgegeben. Bei Erfolg gibt die Methode einen Zeiger auf die Zeile zurück, die der Liste neu hinzugefügt wurde.

Die Methode, die dem Listenende ein Zeilenobjekt hinzufügt:

//+------------------------------------------------------------------+
//| Add a row to the end of the list                                 |
//+------------------------------------------------------------------+
bool CTableModel::AddNewRow(CTableRow *row)
  {
//--- If an empty object is passed, report this and return 'false'
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Empty CTableRow object passed",__FUNCTION__);
      return false;
     }
//--- Set the row index in the list and add it to the end of the list
   row.SetIndex(this.RowsTotal());
   if(this.m_list_rows.Add(row)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add row (%u) to list",__FUNCTION__,row.Index());
      return false;
     }

//--- Successful
   return true;
  }

Die beiden oben beschriebenen Methoden befinden sich im geschützten Bereich der Klasse, arbeiten paarweise und werden intern verwendet, wenn der Tabelle neue Zeilen hinzugefügt werden.

Methode zur Erstellung einer neuen Zeile und deren Hinzufügung zum Listenende:

//+------------------------------------------------------------------+
//| Create a new string and add it to the end of the list            |
//+------------------------------------------------------------------+
CTableRow *CTableModel::RowAddNew(void)
  {
//--- Create a new empty row and add it to the end of the list of rows
   CTableRow *row=this.CreateNewEmptyRow();
   if(row==NULL)
      return NULL;
      
//--- Create cells equal to the number of cells in the first row
   for(uint i=0;i<this.CellsInRow(0);i++)
      row.CreateNewCell(0.0);
   row.ClearData();
   
//--- Success - return the pointer to the created object
   return row;
  }

Dies ist eine öffentliche Methode. Sie wird verwendet, um der Tabelle eine neue Zeile mit Zellen hinzuzufügen. Die Anzahl der Zellen für die erstellte Zeile wird aus der allerersten Zeile der Tabelle übernommen.

Die Methode zum Erstellen und Hinzufügen einer neuen Zeile an einer bestimmten Listenposition:

//+------------------------------------------------------------------+
//| Create and add a new string to the specified position in the list|
//+------------------------------------------------------------------+
CTableRow *CTableModel::RowInsertNewTo(const uint index_to)
  {
//--- Create a new empty row and add it to the end of the list of rows
   CTableRow *row=this.CreateNewEmptyRow();
   if(row==NULL)
      return NULL;
     
//--- Create cells equal to the number of cells in the first row
   for(uint i=0;i<this.CellsInRow(0);i++)
      row.CreateNewCell(0.0);
   row.ClearData();
   
//--- Shift the row to index_to
   this.RowMoveTo(this.m_list_rows.IndexOf(row),index_to);
   
//--- Success - return the pointer to the created object
   return row;
  }

Manchmal müssen Sie eine neue Zeile nicht am Ende der Liste von Zeilen einfügen, sondern zwischen den vorhandenen Zeilen. Diese Methode erstellt zunächst eine neue Zeile am Ende der Liste, füllt sie mit Zellen, löscht sie und verschiebt die Zeile dann an die gewünschte Position.

Die Methode, die Werte in die angegebene Zelle setzt:

//+------------------------------------------------------------------+
//| Set the value to the specified cell                              |
//+------------------------------------------------------------------+
template<typename T>
void CTableModel::CellSetValue(const uint row,const uint col,const T value)
  {
//--- Get a cell by row and column indices
   CTableCell *cell=this.GetCell(row,col);
   if(cell==NULL)
      return;
//--- Get the correct type of the data being set (double, long, datetime, color, string)
   ENUM_DATATYPE type=this.GetCorrectDatatype(typename(T));
//--- Depending on the data type, we call the corresponding
//--- cell method for setting the value, explicitly specifying the required type
   switch(type)
     {
      case TYPE_DOUBLE  :  cell.SetValue((double)value);    break;
      case TYPE_LONG    :  cell.SetValue((long)value);      break;
      case TYPE_DATETIME:  cell.SetValue((datetime)value);  break;
      case TYPE_COLOR   :  cell.SetValue((color)value);     break;
      case TYPE_STRING  :  cell.SetValue((string)value);    break;
      default           :  break;
     }
  }

Holen Sie sich zunächst einen Zeiger auf die gewünschte Zelle anhand der Koordinaten ihrer Zeile und Spalte, und setzen Sie dann einen Wert darauf. Unabhängig von dem Wert, der an die Methode zur Installation in der Zelle übergeben wird, wählt die Methode nur den richtigen Typ für die Installation aus – double, long, datetime, color oder string.

Methode zur Einstellung der Genauigkeit der Anzeige von Daten in der angegebenen Zelle:

//+------------------------------------------------------------------+
//| Set the precision of data display in the specified cell          |
//+------------------------------------------------------------------+
void CTableModel::CellSetDigits(const uint row,const uint col,const int digits)
  {
//--- Get a cell by row and column indices and
//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetDigits(digits);
  }

Die Methode ist nur für Zellen relevant, die den Typ Realwert speichern. Er wird verwendet, um die Anzahl der Dezimalstellen für den von der Zelle angezeigten Wert anzugeben.

Die Methode, die Zeitanzeigeflags für die angegebene Zelle setzt :

//+------------------------------------------------------------------+
//| Set the time display flags to the specified cell                 |
//+------------------------------------------------------------------+
void CTableModel::CellSetTimeFlags(const uint row,const uint col,const uint flags)
  {
//--- Get a cell by row and column indices and
//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetDatetimeFlags(flags);
  }

Das ist relevant für die Zellen, die Datumswerte anzeigen. Es Legt das Format der Zeitanzeige in der Zelle fest (eines von TIME_DATE|TIME_MINUTES|TIME_SECONDS oder Kombinationen davon).

TIME_DATE liefert das Ergebnis als „jjjj.mm.tt“,
TIME_MINUTES liefert das Ergebnis als „hh:mi“,
TIME_SECONDS liefert das Ergebnis als „hh:mi:ss“.

Die Methode zum Setzen von Farbnamen-Anzeigeflags für die angegebene Zelle setzt:

//+------------------------------------------------------------------+
//| Set the flag for displaying color names in the specified cell    |
//+------------------------------------------------------------------+
void CTableModel::CellSetColorNamesFlag(const uint row,const uint col,const bool flag)
  {
//--- Get a cell by row and column indices and
//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetColorNameFlag(flag);
  }

Ist nur für die Zellen relevant, die Farbwerte anzeigen. Es zeigt an, dass die Farbnamen angezeigt werden müssen, wenn die in der Zelle gespeicherte Farbe in der Farbtabelle vorhanden ist.

Die Methode, die einer Zelle ein Objekt zuweist:

//+------------------------------------------------------------------+
//| Assign an object to a cell                                       |
//+------------------------------------------------------------------+
void CTableModel::CellAssignObject(const uint row,const uint col,CObject *object)
  {
//--- Get a cell by row and column indices and
//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.AssignObject(object);
  }

Die Methode, die die Zuordnung eines Objekts in einer Zelle aufhebt:

//+------------------------------------------------------------------+
//| Unassign an object from a cell                                   |
//+------------------------------------------------------------------+
void CTableModel::CellUnassignObject(const uint row,const uint col)
  {
//--- Get a cell by row and column indices and
//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.UnassignObject();
  }

Mit den beiden oben vorgestellten Methoden können Sie ein Objekt einer Zelle zuordnen oder seine Zuordnung aufheben. Das Objekt muss von der Klasse CObject abgeleitet werden. Im Zusammenhang mit Artikeln über Tabellen kann ein Objekt z. B. ein Objekt aus der Liste der bekannten Objekte aus der Aufzählung ENUM_OBJECT_TYPE sein. Im Moment enthält die Liste nur Zellobjekte, Zeilen und Tabellenmodelle. Es macht keinen Sinn, sie einer Zelle zuzuordnen. Die Enumeration wird jedoch erweitert, wenn wir Artikel über die View-Komponente schreiben, in der die Steuerelemente erstellt werden. Es wäre sinnvoll, einer Zelle z. B. das Steuerelement „Dropdown-Liste“ zuzuweisen.

Die Methode, die die angegebene Zelle löscht:

//+------------------------------------------------------------------+
//| Delete a cell                                                    |
//+------------------------------------------------------------------+
bool CTableModel::CellDelete(const uint row,const uint col)
  {
//--- Get the row by index and return the result of deleting the cell from the list
   CTableRow *row_obj=this.GetRow(row);
   return(row_obj!=NULL ? row_obj.CellDelete(col) : false);
  }

Die Methode holt sich das Zeilenobjekt über seinen Index und ruft seine Methode zum Löschen der angegebenen Zelle auf. Für eine einzelne Zelle in einer einzigen Zeile ist die Methode noch nicht sinnvoll, da sie die Anzahl der Zellen in nur einer Zeile der Tabelle reduziert. Dies führt dazu, dass die Zellen nicht mehr mit den benachbarten Zeilen synchronisiert sind. Bisher gibt es keine Verarbeitung einer solchen Löschung, bei der es notwendig ist, die Zelle neben der gelöschten auf die Größe von zwei Zellen zu „erweitern“, damit die Tabellenstruktur nicht gestört wird. Diese Methode wird jedoch als Teil der Methode zum Löschen von Tabellenspalten verwendet, bei der Zellen in allen Zeilen auf einmal gelöscht werden, ohne die Integrität der gesamten Tabelle zu verletzen.

Die Methode zum Verschieben einer Tabellenzelle:

//+------------------------------------------------------------------+
//| Move the cell                                                    |
//+------------------------------------------------------------------+
bool CTableModel::CellMoveTo(const uint row,const uint cell_index,const uint index_to)
  {
//--- Get the row by index and return the result of moving the cell to a new position
   CTableRow *row_obj=this.GetRow(row);
   return(row_obj!=NULL ? row_obj.CellMoveTo(cell_index,index_to) : false);
  }

Abrufen des Zeilenobjekts anhand seines Index und Aufrufen seiner Methode zum Löschen der angegebenen Zelle.

Die Methode, die die Anzahl der Zellen in der angegebenen Zeile zurückgibt:

//+------------------------------------------------------------------+
//| Return the number of cells in the specified row                  |
//+------------------------------------------------------------------+
uint CTableModel::CellsInRow(const uint index)
  {
   CTableRow *row=this.GetRow(index);
   return(row!=NULL ? row.CellsTotal() : 0);
  }

Holen Sie die Zeile nach Index und geben Sie die Anzahl der Zellen darin zurück, indem Sie die Methode CellsTotal() der Zeile aufrufen.

Methode zur Rückgabe der Anzahl der Zellen in der Tabelle zurückgibt:

//+------------------------------------------------------------------+
//| Return the number of cells in the table                          |
//+------------------------------------------------------------------+
uint CTableModel::CellsTotal(void)
  {
//--- count cells in a loop by rows (slow with a large number of rows)
   uint res=0, total=this.RowsTotal();
   for(int i=0; i<(int)total; i++)
     {
      CTableRow *row=this.GetRow(i);
      res+=(row!=NULL ? row.CellsTotal() : 0);
     }
   return res;
  }

Die Methode durchläuft alle Zeilen der Tabelle und addiert die Anzahl der Zellen jeder Zeile zum Gesamtergebnis, das zurückgegeben wird. Bei einer großen Anzahl von Zeilen in der Tabelle kann eine solche Zählung langsam sein. Nachdem alle Methoden, die sich auf die Anzahl der Zellen in der Tabelle auswirken, erstellt wurden, sind sie nur dann zu berücksichtigen, wenn sich ihre Anzahl ändert.

Die Methode, die die angegebene Tabellenzelle zurückgibt:

//+------------------------------------------------------------------+
//| Return the specified table cell                                  |
//+------------------------------------------------------------------+
CTableCell *CTableModel::GetCell(const uint row,const uint col)
  {
//--- get the row by index row and return the row cell by 'row' index by 'col' index
   CTableRow *row_obj=this.GetRow(row);
   return(row_obj!=NULL ? row_obj.GetCell(col) : NULL);
  }

Abrufen der Zeile nach Zeilenindex und Rückgabe des Zeigers auf das Zellenobjekt nach Spaltenindex mit der Methode GetCell() des Zeilenobjekts.

Die Methode, die dieBeschreibung der Zelle zurückgibt:

//+------------------------------------------------------------------+
//| Return the cell description                                      |
//+------------------------------------------------------------------+
string CTableModel::CellDescription(const uint row,const uint col)
  {
   CTableCell *cell=this.GetCell(row,col);
   return(cell!=NULL ? cell.Description() : "");
  }

Abrufen der Zeile nach Index, Abrufen der Zelle aus der Zeile und Rückgabe ihrer Beschreibung.

Die Methode, die eine Zellenbeschreibung im Protokoll anzeigt:

//+------------------------------------------------------------------+
//| Display a cell description in the journal                        |
//+------------------------------------------------------------------+
void CTableModel::CellPrint(const uint row,const uint col)
  {
//--- Get a cell by row and column index and return its description
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.Print();
  }

Holen Sie sich einen Zeiger auf die Zelle durch Zeilen- und Spaltenindizes und zeigen Sie mit der Print()-Methode des Zellenobjekts dessen Beschreibung im Protokoll an.

Die Methode, die die angegebene Zeile löscht:

//+------------------------------------------------------------------+
//| Delete a row                                                     |
//+------------------------------------------------------------------+
bool CTableModel::RowDelete(const uint index)
  {
//--- Remove a string from the list by index
   if(!this.m_list_rows.Delete(index))
      return false;
//--- After deleting a row, be sure to update all indices of all table cells
   this.CellsPositionUpdate();
   return true;
  }

Mit der Methode Delete() der Klasse CList wird das Zeilenobjekt nach Index aus der Liste gelöscht. Nach dem Löschen der Zeile entsprechen die Indizes der verbleibenden Zeilen und der darin enthaltenen Zellen nicht der Realität und müssen mit der Methode CellsPositionUpdate() angepasst werden.

Die Methode, die eine Zeile an die angegebene Position verschiebt:

//+------------------------------------------------------------------+
//| Move a string to a specified position                            |
//+------------------------------------------------------------------+
bool CTableModel::RowMoveTo(const uint row_index,const uint index_to)
  {
//--- Get the row by index, turning it into the current one
   CTableRow *row=this.GetRow(row_index);
//--- Move the current string to the specified position in the list
   if(row==NULL || !this.m_list_rows.MoveToIndex(index_to))
      return false;
//--- After moving a row, it is necessary to update all indices of all table cells
   this.CellsPositionUpdate();
   return true;
  }

In der Klasse CList arbeiten viele Methoden mit dem aktuellen Listenobjekt. Holen Sie sich einen Zeiger auf die gewünschte Zeile, machen Sie sie zur aktuellen Zeile und verschieben Sie sie mit der Methode MoveToIndex() der Klasse CList an die gewünschte Position. Nachdem die Zeile an eine neue Position verschoben wurde, müssen die Indizes für die verbleibenden Zeilen aktualisiert werden, was mit der Methode CellsPositionUpdate() geschieht.

Die Methode, die Zeilen- und Spaltenpositionen für alle Zellen festlegt:

//+------------------------------------------------------------------+
//| Set the row and column positions to all cells                    |
//+------------------------------------------------------------------+
void CTableModel::CellsPositionUpdate(void)
  {
//--- In the loop by the list of rows
   for(int i=0;i<this.m_list_rows.Total();i++)
     {
      //--- get the next row
      CTableRow *row=this.GetRow(i);
      if(row==NULL)
         continue;
      //--- set the index, found by the IndexOf() method of the list, to the row
      row.SetIndex(this.m_list_rows.IndexOf(row));
      //--- Update the row cell position indices
      row.CellsPositionUpdate();
     }
  }

Gehen Sie durch die Liste aller Zeilen in der Tabelle, wählen Sie jede nachfolgende Zeile aus und setzen Sie einen korrekten Index für sie, der mit der Methode IndexOf() der Klasse CList gefunden wurde. Rufen Sie dann die Zeilenmethode CellsPositionUpdate() auf, die den richtigen Index für jede Zelle in der Zeile festlegt.

Die Methode, die die daten aller Zellen in einer Zeile löscht:

//+------------------------------------------------------------------+
//| Clear the row (only the data in the cells)                       |
//+------------------------------------------------------------------+
void CTableModel::RowResetData(const uint index)
  {
//--- Get a row from the list and clear the data of the row cells using the ClearData() method
   CTableRow *row=this.GetRow(index);
   if(row!=NULL)
      row.ClearData();
  }

Jede Zelle in der Zeile wird auf einen „leeren“ Wert zurückgesetzt. Zur Vereinfachung ist der leere Wert für numerische Zellen zunächst Null, aber das wird später geändert, da Null auch ein Wert ist, der in der Zelle angezeigt werden muss. Und das Zurücksetzen eines Wertes bedeutet, dass ein leeres Zellenfeld angezeigt wird.

Die Methode zum Löschen der Daten von aller Zellen in der Tabelle:

//+------------------------------------------------------------------+
//| Clear the table (data of all cells)                              |
//+------------------------------------------------------------------+
void CTableModel::ClearData(void)
  {
//--- Clear the data of each row in the loop through all the table rows
   for(uint i=0;i<this.RowsTotal();i++)
      this.RowResetData(i);
  }

Gehen Sie durch alle Zeilen der Tabelle und rufen Sie für jede Zeile die oben beschriebene Methode RowResetData() auf.

Die Methode, die die Beschreibung der Zeile zurückgibt:

//+------------------------------------------------------------------+
//| Return the row description                                       |
//+------------------------------------------------------------------+
string CTableModel::RowDescription(const uint index)
  {
//--- Get a row by index and return its description
   CTableRow *row=this.GetRow(index);
   return(row!=NULL ? row.Description() : "");
  }

Holt einen Zeiger auf die Zeile nach Index und gibt ihre Beschreibung zurück.

Methode zur Anzeige der Zeilenbeschreibung im Protokoll:

//+------------------------------------------------------------------+
//| Display the row description in the journal                       |
//+------------------------------------------------------------------+
void CTableModel::RowPrint(const uint index,const bool detail)
  {
   CTableRow *row=this.GetRow(index);
   if(row!=NULL)
      row.Print(detail);
  }

Holen Sie sich einen Zeiger auf die Zeile und rufen Sie die Methode Print() des empfangenen Objekts auf.

Die Methode zum Löschen von Tabellenspalten:

//+------------------------------------------------------------------+
//| Remove the column                                                |
//+------------------------------------------------------------------+
bool CTableModel::ColumnDelete(const uint index)
  {
   bool res=true;
   for(uint i=0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         res &=row.CellDelete(index);
     }
   return res;
  }

Gehen Sie in einer Schleife durch alle Zeilen der Tabelle, holen Sie sich die jeweils nächste Zeile und löschen Sie darin eine gewünschte Zelle nach Spaltenindex. Dadurch werden alle Zellen in der Tabelle gelöscht, die die gleichen Spaltenindizes haben.

Die Methode, die eine Tabellenspalte verschiebt:

//+------------------------------------------------------------------+
//| Move the column                                                  |
//+------------------------------------------------------------------+
bool CTableModel::ColumnMoveTo(const uint col_index,const uint index_to)
  {
   bool res=true;
   for(uint i=0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         res &=row.CellMoveTo(col_index,index_to);
     }
   return res;
  }

Gehen Sie in einer Schleife durch alle Zeilen der Tabelle, holen Sie die jeweils nächste Zeile und verschieben Sie die gewünschte Zelle an eine neue Position. Dadurch werden alle Zellen in der Tabelle verschoben, die die gleichen Spaltenindizes haben.

Die Methode, die Daten von Spaltenzellen löscht:

//+------------------------------------------------------------------+
//| Clear the column data                                            |
//+------------------------------------------------------------------+
void CTableModel::ColumnResetData(const uint index)
  {
//--- In a loop through all table rows
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- get the cell with the column index from each row and clear it
      CTableCell *cell=this.GetCell(i, index);
      if(cell!=NULL)
         cell.ClearData();
     }
  }

Wir gehen in einer Schleife durch alle Zeilen der Tabelle, rufen wir jede Zeile auf und löschen die Daten in der gewünschten Zelle. Dadurch werden alle Zellen in der Tabelle gelöscht, die die gleichen Spaltenindizes haben.

Die Methode, die eine Beschreibung des Objekts liefert:

//+------------------------------------------------------------------+
//| Return the object description                                    |
//+------------------------------------------------------------------+
string CTableModel::Description(void)
  {
   return(::StringFormat("%s: Rows %u, Cells in row %u, Cells Total %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.RowsTotal(),this.CellsInRow(0),this.CellsTotal()));
  }

In diesem Format wird aus einigen Parametern des Tabellenmodells eine Zeile erstellt und zurückgegeben:

Table Model: Rows 4, Cells in row 4, Cells Total 16:

Die Methode, die eine Objektbeschreibung in das Protokoll ausgibt:

//+------------------------------------------------------------------+
//| Display the object description in the journal                    |
//+------------------------------------------------------------------+
void CTableModel::Print(const bool detail)
  {
//--- Display the header in the journal
   ::Print(this.Description()+(detail ? ":" : ""));
//--- If detailed description,
   if(detail)
     {
      //--- In a loop through all table rows
      for(uint i=0; i<this.RowsTotal(); i++)
        {
         //--- get the next row and display its detailed description to the journal
         CTableRow *row=this.GetRow(i);
         if(row!=NULL)
            row.Print(true,false);
        }
     }
  }

Zunächst wird die Kopfzeile als Beschreibung des Modells gedruckt, und dann, wenn das Flag für detaillierte Ausgabe gesetzt ist, werden in der Schleife detaillierte Beschreibungen aller Zeilen des Tabellenmodells gedruckt.

Methode zur Ausgabe der Objektbeschreibung in Tabellenform in das Protokoll ausgibt:

//+------------------------------------------------------------------+
//| Display the object description as a table in the journal         |
//+------------------------------------------------------------------+
void CTableModel::PrintTable(const int cell_width=10)
  {
//--- Get the pointer to the first row (index 0)
   CTableRow *row=this.GetRow(0);
   if(row==NULL)
      return;
   //--- Create a table header row based on the number of cells in the first row of the table
   uint total=row.CellsTotal();
   string head=" n/n";
   string res=::StringFormat("|%*s |",cell_width,head);
   for(uint i=0;i<total;i++)
     {
      if(this.GetCell(0, i)==NULL)
         continue;
      string cell_idx=" Column "+(string)i;
      res+=::StringFormat("%*s |",cell_width,cell_idx);
     }
   //--- Display the header row in the journal
   ::Print(res);
   
   //--- Iterate through all table rows and display them in tabular form
   for(uint i=0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         row.Print(true,true,cell_width);
     }
  }

Wir erstellen zunächst auf der Grundlage der Anzahl der Zellen in der allerersten Zeile der Tabelle den Tabellenkopf mit den Namen der Tabellenspalten und drucken ihn im Protokoll aus. Gehen Sie dann in einer Schleife durch alle Zeilen der Tabelle und drucken Sie jede Zeile in Tabellenform aus.

Die Methode, die das Tabellenmodell vernichtet:

//+------------------------------------------------------------------+
//| Destroy the model                                                |
//+------------------------------------------------------------------+
void CTableModel::Destroy(void)
  {
//--- Clear cell list
   m_list_rows.Clear();
  }

Die Liste der Tabellenzeilen wird einfach geleert und alle Objekte werden mit der Klassenmethode Clear() von CList zerstört.

Die Methode zum Speichern des Tabellenmodells in einer Datei:

//+------------------------------------------------------------------+
//| Save to file                                                     |
//+------------------------------------------------------------------+
bool CTableModel::Save(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long))
      return(false);
//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return(false);

   //--- Save the list of rows
   if(!this.m_list_rows.Save(file_handle))
      return(false);
   
//--- Successful
   return true;
  }

Nach dem Speichern der Datenanfangsmarkierung und des Typs der Liste wird die Liste der Zeilen mit der Klassenmethode Save() von CList in der Datei gespeichert.

Die Methode zum Laden eines Tabellenmodells aus einer Datei:

//+------------------------------------------------------------------+
//| Load from file                                                   |
//+------------------------------------------------------------------+
bool CTableModel::Load(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Load the object type
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return(false);

   //--- Load the list of rows
   if(!this.m_list_rows.Load(file_handle))
      return(false);
   
//--- Successful
   return true;
  }

Nach dem Laden und Überprüfen der Datenanfangsmarkierung und des Listentyps laden Sie die Liste der Zeilen aus der Datei mit der Load()-Methode der Klasse CListObj, die am Anfang des Artikels beschrieben wurde.

Alle Klassen zur Erstellung eines Tabellenmodells sind fertig. Lassen Sie uns nun ein Skript schreiben, um die Funktionsweise des Modells zu testen.


Testen Sie das Ergebnis.

Schreiben Sie den Code weiter in dieselbe Datei. Schreiben Sie ein Skript, das ein zweidimensionales 4x4-Array (4 Zeilen mit je 4 Zellen) mit dem Typ long erstellt. Öffnen Sie dann eine Datei, um die Daten des Tabellenmodells in diese zu schreiben, und laden Sie die Daten aus der Datei in die Tabelle. Erstellen Sie ein Tabellenmodell und überprüfen Sie die Funktionsweise einiger seiner Methoden.

Jedes Mal, wenn die Tabelle geändert wird, protokollieren wir das erhaltene Ergebnis mit der Funktion TableModelPrint(), die festlegt, wie das Tabellenmodell zu drucken ist. Wenn der Wert des Makros PRINT_AS_TABLE true ist, erfolgt die Protokollierung mit der Methode PrintTable() der Klasse CTableModel, wenn der Wert false ist – mit der Methode Print() derselben Klasse.

Erstellen Sie in einem Script ein Tabellenmodell, drucken Sie es in Tabellarischer Form aus und speichern Sie das Modell in einer Datei. Fügen Sie dann Zeilen hinzu, löschen Sie Spalten, und ändern Sie die Bearbeitungsberechtigung...

Laden Sie dann die ursprüngliche Version der Tabelle erneut aus der Datei herunter und drucken Sie das Ergebnis aus.

#define  PRINT_AS_TABLE    true  // Display the model as a table
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Declare and fill the 4x4 array
//--- Acceptable array types: double, long, datetime, color, string
   long array[4][4]={{ 1,  2,  3,  4},
                     { 5,  6,  7,  8},
                     { 9, 10, 11, 12},
                     {13, 14, 15, 16}};
     
//--- Create a table model from the 4x4 long array created above
   CTableModel *tm=new CTableModel(array);
   
//--- Leave if the model is not created
   if(tm==NULL)
      return;

//--- Print the model in tabular form
   Print("The table model has been successfully created:");
   tm.PrintTable();
   
//--- Delete the table model object
   delete tm;
  }

Als Ergebnis erhalten wir das folgende Ergebnis des Skripts im Protokoll:

The table model has been successfully created:
|       n/n |  Column 0 |  Column 1 |  Column 2 |  Column 3 |
| Row 0     |         1 |         2 |         3 |         4 |
| Row 1     |         5 |         6 |         7 |         8 |
| Row 2     |         9 |        10 |        11 |        12 |
| Row 3     |        13 |        14 |        15 |        16 |

Um das Arbeiten mit dem Tabellenmodell, das Hinzufügen, Löschen und Verschieben von Zeilen und Spalten sowie das Arbeiten mit einer Datei zu testen, vervollständigen Sie das Skript:

#define  PRINT_AS_TABLE    true  // Display the model as a table
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Declare and fill the 4x4 array
//--- Acceptable array types: double, long, datetime, color, string
   long array[4][4]={{ 1,  2,  3,  4},
                     { 5,  6,  7,  8},
                     { 9, 10, 11, 12},
                     {13, 14, 15, 16}};
     
//--- Create a table model from the 4x4 long array created above
   CTableModel *tm=new CTableModel(array);
   
//--- Leave if the model is not created
   if(tm==NULL)
      return;

//--- Print the model in tabular form
   Print("The table model has been successfully created:");
   tm.PrintTable();
   
   
//--- Check handling files and the functionality of the table model
//--- Open a file to write table model data into it
   int handle=FileOpen(MQLInfoString(MQL_PROGRAM_NAME)+".bin",FILE_READ|FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
      return;
      
   //--- Save the original created table to the file
   if(tm.Save(handle))
      Print("\nThe table model has been successfully saved to file.");
   
//--- Now insert a new row into the table at position 2
//--- Get the last cell of the created row and make it non-editable
//--- Print the modified table model in the journal
   if(tm.RowInsertNewTo(2))
     {
      Print("\nInsert a new row at position 2 and set cell 3 to non-editable");
      CTableCell *cell=tm.GetCell(2,3);
      if(cell!=NULL)
         cell.SetEditable(false);
      TableModelPrint(tm);
     }
   
//--- Now delete the table column with index 1 and
//--- print the resulting table model in the journal
   if(tm.ColumnDelete(1))
     {
      Print("\nRemove column from position 1");
      TableModelPrint(tm);
     }
   
//--- When saving table data, the file pointer was shifted to the last set data
//--- Place the pointer at the beginning of the file, load the previously saved original table and print it
   if(FileSeek(handle,0,SEEK_SET) && tm.Load(handle))
     {
      Print("\nLoad the original table view from the file:");
      TableModelPrint(tm);
     }
   
//--- Close the open file and delete the table model object
   FileClose(handle);
   delete tm;
  }
//+------------------------------------------------------------------+
//| Display the table model                                          |
//+------------------------------------------------------------------+
void TableModelPrint(CTableModel *tm)
  {
   if(PRINT_AS_TABLE)
      tm.PrintTable();  // Print the model as a table
   else
      tm.Print(true);   // Print detailed table data
  }

Dieses Ergebnis wird in das Protokoll aufgenommen:

The table model has been successfully created:
|       n/n |  Column 0 |  Column 1 |  Column 2 |  Column 3 |
| Row 0     |         1 |         2 |         3 |         4 |
| Row 1     |         5 |         6 |         7 |         8 |
| Row 2     |         9 |        10 |        11 |        12 |
| Row 3     |        13 |        14 |        15 |        16 |

The table model has been successfully saved to file.

Insert a new row at position 2 and set cell 3 to non-editable
|       n/n |  Column 0 |  Column 1 |  Column 2 |  Column 3 |
| Row 0     |         1 |         2 |         3 |         4 |
| Row 1     |         5 |         6 |         7 |         8 |
| Row 2     |      0.00 |      0.00 |      0.00 |      0.00 |
| Row 3     |         9 |        10 |        11 |        12 |
| Row 4     |        13 |        14 |        15 |        16 |

Remove column from position 1
|       n/n |  Column 0 |  Column 1 |  Column 2 |
| Row 0     |         1 |         3 |         4 |
| Row 1     |         5 |         7 |         8 |
| Row 2     |      0.00 |      0.00 |      0.00 |
| Row 3     |         9 |        11 |        12 |
| Row 4     |        13 |        15 |        16 |

Load the original table view from the file:
|       n/n |  Column 0 |  Column 1 |  Column 2 |  Column 3 |
| Row 0     |         1 |         2 |         3 |         4 |
| Row 1     |         5 |         6 |         7 |         8 |
| Row 2     |         9 |        10 |        11 |        12 |
| Row 3     |        13 |        14 |        15 |        16 |

Wenn Sie die Daten, die nicht in Tabellenform vorliegen, aus dem Tabellenmodell in das Protokoll ausgeben wollen, setzen Sie im Makro PRINT_AS_TABLE den Wert false:

#define  PRINT_AS_TABLE    false  // Display the model as a table

In diesem Fall wird im Protokoll Folgendes angezeigt:

The table model has been successfully created:
|       n/n |  Column 0 |  Column 1 |  Column 2 |  Column 3 |
| Row 0     |         1 |         2 |         3 |         4 |
| Row 1     |         5 |         6 |         7 |         8 |
| Row 2     |         9 |        10 |        11 |        12 |
| Row 3     |        13 |        14 |        15 |        16 |

The table model has been successfully saved to file.

Insert a new row at position 2 and set cell 3 to non-editable
Table Model: Rows 5, Cells in row 4, Cells Total 20:
Table Row: Position 0, Cells total: 4:
  Table Cell: Row 0, Col 0, Editable <long>Value: 1
  Table Cell: Row 0, Col 1, Editable <long>Value: 2
  Table Cell: Row 0, Col 2, Editable <long>Value: 3
  Table Cell: Row 0, Col 3, Editable <long>Value: 4
Table Row: Position 1, Cells total: 4:
  Table Cell: Row 1, Col 0, Editable <long>Value: 5
  Table Cell: Row 1, Col 1, Editable <long>Value: 6
  Table Cell: Row 1, Col 2, Editable <long>Value: 7
  Table Cell: Row 1, Col 3, Editable <long>Value: 8
Table Row: Position 2, Cells total: 4:
  Table Cell: Row 2, Col 0, Editable <double>Value: 0.00
  Table Cell: Row 2, Col 1, Editable <double>Value: 0.00
  Table Cell: Row 2, Col 2, Editable <double>Value: 0.00
  Table Cell: Row 2, Col 3, Uneditable <double>Value: 0.00
Table Row: Position 3, Cells total: 4:
  Table Cell: Row 3, Col 0, Editable <long>Value: 9
  Table Cell: Row 3, Col 1, Editable <long>Value: 10
  Table Cell: Row 3, Col 2, Editable <long>Value: 11
  Table Cell: Row 3, Col 3, Editable <long>Value: 12
Table Row: Position 4, Cells total: 4:
  Table Cell: Row 4, Col 0, Editable <long>Value: 13
  Table Cell: Row 4, Col 1, Editable <long>Value: 14
  Table Cell: Row 4, Col 2, Editable <long>Value: 15
  Table Cell: Row 4, Col 3, Editable <long>Value: 16

Remove column from position 1
Table Model: Rows 5, Cells in row 3, Cells Total 15:
Table Row: Position 0, Cells total: 3:
  Table Cell: Row 0, Col 0, Editable <long>Value: 1
  Table Cell: Row 0, Col 1, Editable <long>Value: 3
  Table Cell: Row 0, Col 2, Editable <long>Value: 4
Table Row: Position 1, Cells total: 3:
  Table Cell: Row 1, Col 0, Editable <long>Value: 5
  Table Cell: Row 1, Col 1, Editable <long>Value: 7
  Table Cell: Row 1, Col 2, Editable <long>Value: 8
Table Row: Position 2, Cells total: 3:
  Table Cell: Row 2, Col 0, Editable <double>Value: 0.00
  Table Cell: Row 2, Col 1, Editable <double>Value: 0.00
  Table Cell: Row 2, Col 2, Uneditable <double>Value: 0.00
Table Row: Position 3, Cells total: 3:
  Table Cell: Row 3, Col 0, Editable <long>Value: 9
  Table Cell: Row 3, Col 1, Editable <long>Value: 11
  Table Cell: Row 3, Col 2, Editable <long>Value: 12
Table Row: Position 4, Cells total: 3:
  Table Cell: Row 4, Col 0, Editable <long>Value: 13
  Table Cell: Row 4, Col 1, Editable <long>Value: 15
  Table Cell: Row 4, Col 2, Editable <long>Value: 16

Load the original table view from the file:
Table Model: Rows 4, Cells in row 4, Cells Total 16:
Table Row: Position 0, Cells total: 4:
  Table Cell: Row 0, Col 0, Editable <long>Value: 1
  Table Cell: Row 0, Col 1, Editable <long>Value: 2
  Table Cell: Row 0, Col 2, Editable <long>Value: 3
  Table Cell: Row 0, Col 3, Editable <long>Value: 4
Table Row: Position 1, Cells total: 4:
  Table Cell: Row 1, Col 0, Editable <long>Value: 5
  Table Cell: Row 1, Col 1, Editable <long>Value: 6
  Table Cell: Row 1, Col 2, Editable <long>Value: 7
  Table Cell: Row 1, Col 3, Editable <long>Value: 8
Table Row: Position 2, Cells total: 4:
  Table Cell: Row 2, Col 0, Editable <long>Value: 9
  Table Cell: Row 2, Col 1, Editable <long>Value: 10
  Table Cell: Row 2, Col 2, Editable <long>Value: 11
  Table Cell: Row 2, Col 3, Editable <long>Value: 12
Table Row: Position 3, Cells total: 4:
  Table Cell: Row 3, Col 0, Editable <long>Value: 13
  Table Cell: Row 3, Col 1, Editable <long>Value: 14
  Table Cell: Row 3, Col 2, Editable <long>Value: 15
  Table Cell: Row 3, Col 3, Editable <long>Value: 16

Diese Ausgabe liefert weitere Informationen zur Fehlersuche. Hier sehen wir zum Beispiel, dass das Setzen des Bearbeitungsverbotsflags für eine Zelle in den Programmprotokollen angezeigt wird.

Alle genannten Funktionen funktionieren wie erwartet: Zeilen werden hinzugefügt, verschoben, Spalten gelöscht, wir können Zelleigenschaften ändern und mit Dateien arbeiten.


Schlussfolgerung

Dies ist die erste Version eines einfachen Tabellenmodells, das die Erstellung einer Tabelle aus einem zweidimensionalen Datenfeld ermöglicht. Als Nächstes werden wir andere, spezialisierte Tabellenmodelle erstellen, die Komponente Ansichtstabelle erstellen, und dann werden wir mit Tabellendaten als eines der Steuerelemente der grafischen Nutzeroberfläche arbeiten können.

Die Datei des heute erstellten Skripts mit den darin enthaltenen Klassen ist dem Artikel beigefügt. Sie können es zum Selbststudium herunterladen.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/17653

Beigefügte Dateien |
TableModelTest.mq5 (136.43 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (9)
Alexey Viktorov
Alexey Viktorov | 4 Apr. 2025 in 15:31
Artyom Trishkin #:

Wenn eine Klasse von SomeObject aus einer Datei geladen wird, indem man die Load()-Methode von eben diesem SomeObject aufruft, wird geprüft, ob ich mich wirklich aus der Datei eingelesen habe (das ist die Frage, die Sie stellen). Wenn nicht, bedeutet das, dass etwas schief gelaufen ist und es keinen Sinn hat, weiter zu laden.

Was ich hier habe, ist eine LISTE (CListObj), die einen Objekttyp aus einer Datei liest. Die Liste weiß nicht, was sich in der Datei befindet (welches Objekt). Aber sie muss diesen Objekttyp kennen, um ihn in ihrer CreateElement()-Methode zu erzeugen. Deshalb prüft sie nicht den Typ des geladenen Objekts aus der Datei. Schließlich wird es einen Vergleich mit Type() geben, der in dieser Methode den Typ einer Liste und nicht eines Objekts zurückgibt.

Danke, ich habe es herausgefunden, ich verstehe.

Maxim Kuznetsov
Maxim Kuznetsov | 5 Apr. 2025 in 08:05

Ich habe es gelesen, und dann habe ich es noch einmal gelesen.

ist etwas anderes als ein "Modell" in MVC. Einige ListStorage zum Beispiel

Rashid Umarov
Rashid Umarov | 5 Apr. 2025 in 08:37
Lassen Sie uns zur Sache kommen. Behalten Sie Ihre Meinung für sich.
Aleksey Nikolayev
Aleksey Nikolayev | 5 Apr. 2025 in 09:38
Ich frage mich. Ist es möglich, einige Analogon von Python und R dataframes auf diese Weise zu erhalten? Dies sind Tabellen, wo verschiedene Spalten Daten verschiedener Typen enthalten können (aus einer begrenzten Menge von Typen, aber einschließlich String).
Artyom Trishkin
Artyom Trishkin | 5 Apr. 2025 in 11:29
Aleksey Nikolayev #:
Ich frage mich. Ist es möglich, einige Analogon von Python und R dataframes auf diese Weise zu erhalten? Dies sind solche Tabellen, in denen verschiedene Spalten Daten verschiedener Typen enthalten können (aus einer begrenzten Anzahl von Typen, aber einschließlich String).

Sie können. Wenn wir über verschiedene Spalten einer Tabelle sprechen, dann kann in der beschriebenen Implementierung jede Zelle der Tabelle einen anderen Datentyp haben.

Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Vom Neuling zum Experten: Entwicklung eines geografischen Marktbewusstseins mit MQL5-Visualisierung Vom Neuling zum Experten: Entwicklung eines geografischen Marktbewusstseins mit MQL5-Visualisierung
Handeln ohne Sitzungsbewusstsein ist wie Navigieren ohne Kompass – man bewegt sich, aber nicht zielgerichtet. Heute revolutionieren wir die Art und Weise, wie Händler das Markt-Timing wahrnehmen, indem wir gewöhnliche Charts in dynamische geografische Darstellungen verwandeln. Mithilfe der leistungsstarken Visualisierungsfunktionen von MQL5 erstellen wir eine Live-Weltkarte, die aktive Handelssitzungen in Echtzeit beleuchtet und abstrakte Marktzeiten in intuitive visuelle Intelligenz verwandelt. Diese Reise schärft Ihre Handelspsychologie und offenbart professionelle Programmiertechniken, die die Lücke zwischen komplexen Marktstrukturen und praktischen, umsetzbaren Erkenntnissen schließen.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Aufbau eines Remote-Forex-Risikomanagementsystems in Python Aufbau eines Remote-Forex-Risikomanagementsystems in Python
Wir entwickeln einen professionellen Remote-Risikomanager für Forex in Python, der Schritt für Schritt auf dem Server installiert wird. Im Laufe des Artikels werden wir verstehen, wie man die Forex-Risiken programmatisch verwalten kann und wie man eine Forex-Einlage nicht mehr verschwenden kann.