Русский
preview
Table and Header Classes based on a table model in MQL5: Applying the MVC concept

Table and Header Classes based on a table model in MQL5: Applying the MVC concept

MetaTrader 5Examples |
309 0
Artyom Trishkin
Artyom Trishkin

Contents


Introduction

In the first article covering the creation of the Table Control, we created a table model in MQL5 using the MVC architecture template. Classes of cells, rows, and table models were developed, which enabled to organize data in a convenient and structured form.

Now we move on to the next stage — the development of table classes and table headers. Column headers of a table are not just column labels, but a tool for managing the table and its columns. They allow you to add, delete, and rename columns. Of course, a table can work without a header class, but then its features will be limited. A simple static table will be created without column headers and, accordingly, without the feature of controlling columns.

To implement column control feature the table model must be refined. We will supplement it with methods that allow you to work with columns: change their structure, add new ones, or delete existing ones. These methods will be used by the table header class to provide convenient control of its structure.

This development phase will form the basis for further implementation of View and Controller components, which will be discussed in the following articles. This step is an important milestone toward creating a full-fledged interface for operating data.


Refinement of the Table Model

At the moment, the table model is created from a two-dimensional array, but in order to increase flexibility and convenience of working with the table, we will add additional initialization methods. This will allow to adapt the model to different usage scenarios. The following methods will appear in the updated version of the table model class:

  • Creating a model from a two-dimensional array

    void CreateTableModel(T &array[][]);

    This method allows you to quickly create a table model based on an existing two-dimensional data array.

  • Creating an empty model with a set number of rows and columns

    void CreateTableModel(const uint num_rows, const uint num_columns);

    This method is suitable when the table structure is known in advance, but the data will be added later.

  • Creating a model from a data matrix

    void CreateTableModel(const matrix &row_data);

    This method allows you to use a data matrix for initializing a table, which is convenient for working with pre-prepared datasets.

  • Creating a model from a linked list

    void CreateTableModel(CList &list_param);

    In this case, an array of arrays will be used for data storage, where one CList object (data on table rows) contains other CList objects which hold data on table cells. This approach allows to dynamically control the structure of the table and its contents.

These changes will make the table model more versatile and convenient for the use in various scenarios. For example, it will be possible to easily create tables from both pre-prepared data arrays and dynamically generated lists.

In the last article, we described all the classes to create a table model directly in the test script file. Today we will transfer these classes to our own include file.

In the folder where the script from the previous article is stored (by default: \MQL5\Scripts\TableModel\), create a new include file named Tables.mqh and copy to it from the TableModelTest.mq5 file located in the same folder, everything from the beginning of the file to the beginning of the test script code - all classes for creating a table model. Now we have a separate file with classes of the table model named Tables.mqh. Make changes and improvements to this file.

Move the created file to a new folder, MQL5\Scripts\Tables\ — we will create this project in this folder.

In the section of included files/libraries, add a forward declaration of new classes. We will do them today, and the declaration of classes is necessary so that the CListObj object list class created in the previous article can create objects of these classes in its CreateElement() method:

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#include <Arrays\List.mqh>

//--- Форвард-декларация классов
class CTableCell;                   // Класс ячейки таблицы
class CTableRow;                    // Класс строки таблицы
class CTableModel;                  // Класс модели таблицы
class CColumnCaption;               // Класс заголовка столбца таблицы
class CTableHeader;                 // Класс заголовка таблицы
class CTable;                       // Класс таблицы
class CTableByParam;                // Класс таблицы на основе массива параметров

//+------------------------------------------------------------------+
//| Макросы                                                          |
//+------------------------------------------------------------------+

In the macros section add a determination of the table cell width in characters, equal to 19. This is the minimum width value at which the date—time text completely fits into the cell space in the log, without shifting the right border of the cell, which causes the size of all cells of the table drawn in the log to become out of sync:

//+------------------------------------------------------------------+
//| Макросы                                                          |
//+------------------------------------------------------------------+
#define  MARKER_START_DATA    -1    // Маркер начала данных в файле
#define  MAX_STRING_LENGTH    128   // Максимальная длина строки в ячейке
#define  CELL_WIDTH_IN_CHARS  19    // Ширина ячейки таблицы в символах

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+

In the enumeration section add new constants to the enumeration of object types:

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum ENUM_OBJECT_TYPE               // Перечисление типов объектов
  {
   OBJECT_TYPE_TABLE_CELL=10000,    // Ячейка таблицы
   OBJECT_TYPE_TABLE_ROW,           // Строка таблицы
   OBJECT_TYPE_TABLE_MODEL,         // Модель таблицы
   OBJECT_TYPE_COLUMN_CAPTION,      // Заголовок столбца таблицы
   OBJECT_TYPE_TABLE_HEADER,        // Заголовок таблицы
   OBJECT_TYPE_TABLE,               // Таблица
   OBJECT_TYPE_TABLE_BY_PARAM,      // Таблица на данных массива параметров
  };

In the CListObj object list class, in the element creation method,  add new cases for creating new types of objects:

//+------------------------------------------------------------------+
//| Метод создания элемента списка                                   |
//+------------------------------------------------------------------+
CObject *CListObj::CreateElement(void)
  {
//--- В зависимости от типа объекта в 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();
      case OBJECT_TYPE_COLUMN_CAPTION  :  return new CColumnCaption();
      case OBJECT_TYPE_TABLE_HEADER    :  return new CTableHeader();
      case OBJECT_TYPE_TABLE           :  return new CTable();
      case OBJECT_TYPE_TABLE_BY_PARAM  :  return new CTableByParam();
      default                          :  return NULL;
     }
  }

After creating new classes, the CListObj object list will be able to create objects of these types. This will allow to save and load lists from files containing these types of objects.

In order to set an "empty" value in a cell, which will be displayed as an empty row, and not as a "0" value, as is done now, it is necessary to determine which value should be considered as "empty". It is clear that for string values, an empty row will be such a value. While for numeric values define DBL_MAX for real types and LONG_MAX for integers.

To set such a value, in the object class of a table cell, in the protected area, write the following method:

class CTableCell : public CObject
  {
protected:
//--- Объединение для хранения значений ячейки (double, long, string)
   union DataType
     {
      protected:
      double         double_value;
      long           long_value;
      ushort         ushort_value[MAX_STRING_LENGTH];

      public:
      //--- Установка значений
      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);  }
      
      //--- Возврат значений
      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;
                       }
     };
//--- Переменные
   DataType          m_datatype_value;                      // Значение
   ENUM_DATATYPE     m_datatype;                            // Тип данных
   CObject          *m_object;                              // Объект в ячейке
   ENUM_OBJECT_TYPE  m_object_type;                         // Тип объекта в ячейке
   int               m_row;                                 // Номер строки
   int               m_col;                                 // Номер столбца
   int               m_digits;                              // Точность представления данных
   uint              m_time_flags;                          // Флаги отображения даты/времени
   bool              m_color_flag;                          // Флаг отображения наименования цвета
   bool              m_editable;                            // Флаг редактируемой ячейки
   
//--- Устанавливает "пустое значение"
   void              SetEmptyValue(void)
                       {
                        switch(this.m_datatype)
                          {
                           case TYPE_LONG    :  
                           case TYPE_DATETIME:  
                           case TYPE_COLOR   :  this.SetValue(LONG_MAX);   break;
                           case TYPE_DOUBLE  :  this.SetValue(DBL_MAX);    break;
                           default           :  this.SetValue("");         break;
                          }
                       }
public:
//--- Возврат координат и свойств ячейки

The method that returns the value stored in a cell as a formatted string now checks the value in the cell for being non-"empty" and if the value is "empty", it returns an empty row:

public:
//--- Возврат координат и свойств ячейки
   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;                 }
//--- Возвращает (1) double, (2) long, (3) string значение
   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();  }
//--- Возвращает значение в виде форматированной строки
   string            Value(void) const
                       {
                        switch(this.m_datatype)
                          {
                           case TYPE_DOUBLE  :  return(this.ValueD()!=DBL_MAX  ? ::DoubleToString(this.ValueD(),this.Digits())            : "");
                           case TYPE_LONG    :  return(this.ValueL()!=LONG_MAX ? ::IntegerToString(this.ValueL())                         : "");
                           case TYPE_DATETIME:  return(this.ValueL()!=LONG_MAX ? ::TimeToString(this.ValueL(),this.m_time_flags)          : "");
                           case TYPE_COLOR   :  return(this.ValueL()!=LONG_MAX ? ::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;
                       }
//--- Очищает данные
   void              ClearData(void)                           { this.SetEmptyValue();                   }

The method for clearing data in a cell now does not set zero in it, but calls a method to set an empty value in the cell.

Methods of all classes from which the table model object is created, which return the description of the object, are now made virtual — in case of inheritance from these objects:

//--- (1) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   void              Print(void);

In the table row class, all CreateNewCell() methods that create a new cell and add it to the end of the list have now been renamed:

//+------------------------------------------------------------------+
//| Класс строки таблицы                                             |
//+------------------------------------------------------------------+
class CTableRow : public CObject
  {
protected:
   CTableCell        m_cell_tmp;                            // Объект ячейки для поиска в списке
   CListObj          m_list_cells;                          // Список ячеек
   uint              m_index;                               // Индекс строки
   
//--- Добавляет указанную ячейку в конец списка
   bool              AddNewCell(CTableCell *cell);
   
public:
//--- (1) Устанавливает, (2) возвращает индекс строки
   void              SetIndex(const uint index)                { this.m_index=index;  }
   uint              Index(void)                         const { return this.m_index; }
//--- Устанавливает позиции строки и колонки всем ячейкам
   void              CellsPositionUpdate(void);
   
//--- Создаёт новую ячейку и добавляет в конец списка
   CTableCell       *CellAddNew(const double value);
   CTableCell       *CellAddNew(const long value);
   CTableCell       *CellAddNew(const datetime value);
   CTableCell       *CellAddNew(const color value);
   CTableCell       *CellAddNew(const string value);

This is done so that all methods responsible for accessing cells start with the substring "Cell". In other classes, methods for accessing, for example, a table row will start with the substring "Row". This brings order to methods of classes.

To build a table model, we must develop a universal approach that will allow to create tables from almost any data. These can be, for example, structures, lists of trades, orders, positions, or any other data. The idea is to create a toolkit that will allow to create a list of rows, where each row will be a list of properties. Each property in the list will correspond to one cell of the table.

Here, attention should be paid to the structure of input parameters of MqlParam. It provides the following features:

  • Specifying the type of data that is stored in the structure (ENUM_DATATYPE).
  • Storing values in three fields:
    1. integer_value — for integer data,
    2. double_value — for real data,
    3. string_value — for string data.

This structure allows to work with various types of data, which will allow storing any properties, such as parameters of transactions, orders or other objects.

For convenient data storage create the CMqlParamObj class, inherited from the basic CObject class from the Standard Library. This class will include the MqlParam structure and provide methods for setting and retrieving data. By inheriting from CObject, such objects can be stored in CList lists.

Consider the entire class:

//+------------------------------------------------------------------+
//| Класс объекта параметра структуры                                |
//+------------------------------------------------------------------+
class CMqlParamObj : public CObject
  {
protected:
public:
   MqlParam          m_param;
//--- Установка параметров
   void              Set(const MqlParam &param)
                       {
                        this.m_param.type=param.type;
                        this.m_param.double_value=param.double_value;
                        this.m_param.integer_value=param.integer_value;
                        this.m_param.string_value=param.string_value;
                       }
//--- Возврат параметров
   MqlParam          Param(void)       const { return this.m_param;              }
   ENUM_DATATYPE     Datatype(void)    const { return this.m_param.type;         }
   double            ValueD(void)      const { return this.m_param.double_value; }
   long              ValueL(void)      const { return this.m_param.integer_value;}
   string            ValueS(void)      const { return this.m_param.string_value; }
//--- Описание объекта
   virtual string    Description(void)
                       {
                        string t=::StringSubstr(::EnumToString(this.m_param.type),5);
                        t.Lower();
                        string v="";
                        switch(this.m_param.type)
                          {
                           case TYPE_STRING  :  v=this.ValueS(); break;
                           case TYPE_FLOAT   :  case TYPE_DOUBLE : v=::DoubleToString(this.ValueD()); break;
                           case TYPE_DATETIME:  v=::TimeToString(this.ValueL(),TIME_DATE|TIME_MINUTES|TIME_SECONDS); break;
                           default           :  v=(string)this.ValueL(); break;
                          }
                        return(::StringFormat("<%s>%s",t,v));
                       }
   
//--- Конструкторы/деструктор
                     CMqlParamObj(void){}
                     CMqlParamObj(const MqlParam &param) { this.Set(param);  }
                    ~CMqlParamObj(void){}
  };

This is a common wrapper around the MqlParam structure, since it requires an object inherited from CObject in order to store such objects in the CList list.

The structure of data created will be as follows:

  • One object of the CMqlParamObj class will represent one property, for example, the price of a trade, its volume, or opening time,
  • A single CList list will represent a table row containing all the properties of a single trade,
  • The main CList list will contain a set of rows (CList lists), each of which corresponds to one trade, order, position, or any other entity.

Thus, we will get a structure similar to an array of arrays:

  • The main CList list is an "array of rows",
  • Each nested CList list is an "array of cells" (properties of some object).

Here is an example of such a data structure for a list of historical trades:

  1. The main CList list  stores rows of a table. Each row is a separate CList list.
  2. Nested lists CList — each nested list represents a row in a table and contains objects of the CMqlParamObj class that store properties. For example:
    • row one: properties of trade No. 1 (price, volume, opening time, etc.),
    • row two: properties of trade No. 2 (price, volume, opening time, etc.),
    • row three: properties of trade No. 3 (price, volume, opening time, etc.),
    • etc.
  3. Property objects (CMqlParamObj)  — each object stores one property, for example, the price of a trade or its volume.

After forming the data structure (CList lists), it can be passed to the CreateTableModel (CList &list_param) method of the table model. This method will interpret the data as follows:

  • The main CList list is a list of table rows,
  • Each nested CList list is cells of each row,
  • CMqlParamObj objects inside nested lists are cell values.

Thus, based on the passed list, a table will be created that fully corresponds to the source data.

For the convenience of creating such lists develop a special class. This class will provide methods for:

  1. Creating a new row (of the CList list) and adding it to the main list,
  2. Adding a new property (the CMqlParamObj object) to the row.
//+------------------------------------------------------------------+
//| Класс для создания списков данных                                |
//+------------------------------------------------------------------+
class DataListCreator
  {
public:
//--- Добавляет новую строку к списку CList list_data
   static CList     *AddNewRowToDataList(CList *list_data)
                       {
                        CList *row=new CList;
                        if(row==NULL || list_data.Add(row)<0)
                           return NULL;
                        return row;
                       }
//--- Создаёт новый объект параметров CMqlParamObj и добавляет его к списку CList
   static bool       AddNewCellParamToRow(CList *row,MqlParam &param)
                       {
                        CMqlParamObj *cell=new CMqlParamObj(param);
                        if(cell==NULL)
                           return false;
                        if(row.Add(cell)<0)
                          {
                           delete cell;
                           return false;
                          }
                        return true;
                       }
  };

This is a static class that provides a feature to conveniently create lists with the correct structure for passing them to methods of creating table models:

  1. Specify the necessary property (e.g., the trade price),
  2. The class automatically creates a CMqlParamObj object, writes the property value to it, and adds it to the row,
  3. The row is added to the main list.

After that, the finished list can be passed to the table model for development.

As a result, the developed approach allows to convert data from any structure or object into a format suitable for building a table. Using CList lists and CMqlParamObj objects provides flexibility and convenience of operation, and the auxiliary DataListCreator class simplifies the process of creating such lists. This will be the basis for building a universal table model being created from any data and for any task.

In the table models class, add three new methods for creating a table model, three methods for column manipulation. Instead of five overloaded parametric constructors add one template constructor and three new ones based on the types of input parameters of the new methods for creating a table model:

//+------------------------------------------------------------------+
//| Класс модели таблицы                                             |
//+------------------------------------------------------------------+
class CTableModel : public CObject
  {
protected:
   CTableRow         m_row_tmp;                             // Объект строки для поиска в списке
   CListObj          m_list_rows;                           // Список строк таблицы
//--- Создаёт модель таблицы из двумерного массива
template<typename T>
   void              CreateTableModel(T &array[][]);
   void              CreateTableModel(const uint num_rows,const uint num_columns);
   void              CreateTableModel(const matrix &row_data);
   void              CreateTableModel(CList &list_param);
//--- Возвращает корректный тип данных
   ENUM_DATATYPE     GetCorrectDatatype(string type_name)
                       {
                        return
                          (
                           //--- Целочисленное значение
                           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      :
                           //--- Вещественное значение
                           type_name=="float"|| type_name=="double"                          ?  TYPE_DOUBLE    :
                           //--- Значение даты/времени
                           type_name=="datetime"                                             ?  TYPE_DATETIME  :
                           //--- Значение цвета
                           type_name=="color"                                                ?  TYPE_COLOR     :
                           /*--- Строковое значение */                                          TYPE_STRING    );
                       }
     
//--- Создаёт и добавляет новую пустую строку в конец списка
   CTableRow        *CreateNewEmptyRow(void);
//--- Добавляет строку в конец списка
   bool              AddNewRow(CTableRow *row);
//--- Устанавливает позиции строки и колонки всем ячейкам таблицы
   void              CellsPositionUpdate(void);
   
public:
//--- Возвращает (1) ячейку, (2) строку по индексу, количество (3) строк, ячеек (4) в указанной строке, (5) в таблице
   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);

//--- Устанавливает (1) значение, (2) точность, (3) флаги отображения времени, (4) флаг отображения имён цветов в указанную ячейку
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) Назначает, (2) отменяет объект в ячейке
   void              CellAssignObject(const uint row, const uint col,CObject *object);
   void              CellUnassignObject(const uint row, const uint col);
//--- (1) Удаляет (2) перемещает ячейку
   bool              CellDelete(const uint row, const uint col);
   bool              CellMoveTo(const uint row, const uint cell_index, const uint index_to);
   
//--- (1) Возвращает, (2) выводит в журнал описание ячейки, (3) назначенный в ячейку объект
   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:
//--- Создаёт новую строку и (1) добавляет в конец списка, (2) вставляет в указанную позицию списка
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Удаляет (2) перемещает строку, (3) очищает данные строки
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Возвращает, (2) выводит в журнал описание строки
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Добавляет, (2) удаляет (3) перемещает столбец, (4) очищает данные, устанавливает (5) тип, (6) точность данных столбца
   bool              ColumnAddNew(const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint col_index, const uint index_to);
   void              ColumnClearData(const uint index);
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   void              ColumnSetDigits(const uint index,const int digits);
   
//--- (1) Возвращает, (2) выводит в журнал описание таблицы
   virtual string    Description(void);
   void              Print(const bool detail);
   void              PrintTable(const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- (1) Очищает данные, (2) уничтожает модель
   void              ClearData(void);
   void              Destroy(void);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);  }
   
//--- Конструкторы/деструктор
template<typename T> CTableModel(T &array[][])                                { this.CreateTableModel(array);                 }
                     CTableModel(const uint num_rows,const uint num_columns)  { this.CreateTableModel(num_rows,num_columns);  }
                     CTableModel(const matrix &row_data)                      { this.CreateTableModel(row_data);              }
                     CTableModel(CList &row_data)                             { this.CreateTableModel(row_data);              }
                     CTableModel(void){}
                    ~CTableModel(void){}
  };

Let's consider new methods.

A method that creates a table model from the specified number of rows and columns

//+------------------------------------------------------------------+
//| Создаёт модель таблицы из указанного количества строк и столбцов |
//+------------------------------------------------------------------+
void CTableModel::CreateTableModel(const uint num_rows,const uint num_columns)
  {
//--- В цикле по количеству строк
   for(uint r=0; r<num_rows; r++)
     {
      //--- создаём новую пустую строку и добавляем её в конец списка строк
      CTableRow *row=this.CreateNewEmptyRow();
      //--- Если строка создана и добавлена в список,
      if(row!=NULL)
        {
         //--- В цикле по количеству столбцов 
         //--- создаём все ячейки, добавляя каждую новую в конец списка ячеек строки
         for(uint c=0; c<num_columns; c++)
           {
            CTableCell *cell=row.CellAddNew(0.0);
            if(cell!=NULL)
               cell.ClearData();
           }
            
        }
     }
  }

The method creates an empty model with the specified number of rows and columns. It is suitable when the table structure is known in advance, but the data is supposed be added later.

A method that creates a table model from the specified matrix

//+------------------------------------------------------------------+
//| Создаёт модель таблицы из указанной матрицы                      |
//+------------------------------------------------------------------+
void CTableModel::CreateTableModel(const matrix &row_data)
  {
//--- Количество строк и столбцов
   ulong num_rows=row_data.Rows();
   ulong num_columns=row_data.Cols();
//--- В цикле по количеству строк
   for(uint r=0; r<num_rows; r++)
     {
      //--- создаём новую пустую строку и добавляем её в конец списка строк
      CTableRow *row=this.CreateNewEmptyRow();
      //--- Если строка создана и добавлена в список,
      if(row!=NULL)
        {
         //--- В цикле по количеству столбцов
         //--- создаём все ячейки, добавляя каждую новую в конец списка ячеек строки
         for(uint c=0; c<num_columns; c++)
            row.CellAddNew(row_data[r][c]);
        }
     }
  }

The method allows using a data matrix to initialize a table, which is convenient for operating pre-prepared datasets.

A method that creates a table model from the parameter list

//+------------------------------------------------------------------+
//| Создаёт модель таблицы из списка параметров                      |
//+------------------------------------------------------------------+
void CTableModel::CreateTableModel(CList &list_param)
  {
//--- Если передан пустой список - сообщаем об этом и уходим
   if(list_param.Total()==0)
     {
      ::PrintFormat("%s: Error. Empty list passed",__FUNCTION__);
      return;
     }
//--- Получаем указатель на первую строку таблицы для определения количества столбцов
//--- Если первую строку получить не удалось, или в ней нет ячеек - сообщаем об этом и уходим
   CList *first_row=list_param.GetFirstNode();
   if(first_row==NULL || first_row.Total()==0)
     {
      if(first_row==NULL)
         ::PrintFormat("%s: Error. Failed to get first row of list",__FUNCTION__);
      else
         ::PrintFormat("%s: Error. First row does not contain data",__FUNCTION__);
      return;
     }
//--- Количество строк и столбцов
   ulong num_rows=list_param.Total();
   ulong num_columns=first_row.Total();
//--- В цикле по количеству строк
   for(uint r=0; r<num_rows; r++)
     {
      //--- получаем очередную строку таблицы из списка list_param
      CList *col_list=list_param.GetNodeAtIndex(r);
      if(col_list==NULL)
         continue;
      //--- создаём новую пустую строку и добавляем её в конец списка строк
      CTableRow *row=this.CreateNewEmptyRow();
      //--- Если строка создана и добавлена в список,
      if(row!=NULL)
        {
         //--- В цикле по количеству столбцов
         //--- создаём все ячейки, добавляя каждую новую в конец списка ячеек строки
         for(uint c=0; c<num_columns; c++)
           {
            CMqlParamObj *param=col_list.GetNodeAtIndex(c);
            if(param==NULL)
               continue;

            //--- Объявляем указатель на ячейку и тип данных, которые будут в ней содержаться
            CTableCell *cell=NULL;
            ENUM_DATATYPE datatype=param.Datatype();
            //--- В зависимости от типа данных
            switch(datatype)
              {
               //--- вещественный тип данных
               case TYPE_FLOAT   :
               case TYPE_DOUBLE  :  cell=row.CellAddNew((double)param.ValueD());    // Создаём новую ячейку с double-данными и
                                    if(cell!=NULL)
                                       cell.SetDigits((int)param.ValueL());         // записываем точность отображаемых данных
                                    break;
               //--- тип данных datetime
               case TYPE_DATETIME:  cell=row.CellAddNew((datetime)param.ValueL());  // Создаём новую ячейку с datetime-данными и
                                    if(cell!=NULL)
                                       cell.SetDatetimeFlags((int)param.ValueD());  // записываем флаги отображения даты/времени
                                    break;
               //--- тип данных color
               case TYPE_COLOR   :  cell=row.CellAddNew((color)param.ValueL());     // Создаём новую ячейку с color-данными и
                                    if(cell!=NULL)
                                       cell.SetColorNameFlag((bool)param.ValueD()); // записваем флаг отображения наименования известных цветов
                                    break;
               //--- строковый тип данных
               case TYPE_STRING  :  cell=row.CellAddNew((string)param.ValueS());    // Создаём новую ячейку со string-данными
                                    break; 
               //--- целочисленный тип данных
               default           :  cell=row.CellAddNew((long)param.ValueL());      // Создаём новую ячейку с long-данными
                                    break; 
              }
           }
        }
     }
  }

This method makes it possible to create a table model based on a linked list, which can be useful for operating dynamic data structures.

It is worth noting that when creating cells with a data type, e. g., double, the display precision is taken from the long value of the param object of the CMqlParamObj class. This means that when creating a table structure using the DataListCreator class as discussed above, we can additionally pass the necessary clarifying information to the parameter object. For the financial result of a trade, this can be done as follows:

//--- Финансовый результат сделки
param.type=TYPE_DOUBLE;
param.double_value=HistoryDealGetDouble(ticket,DEAL_PROFIT);
param.integer_value=(param.double_value!=0 ? 2 : 1);
DataListCreator::AddNewCellParamToRow(row,param);

In the same manner, one can pass time display flags for table cells with the datetime type and a flag for displaying the color name for a cell with the color type.

The table displays the types of table cells and the types of parameters being passed for them via CMqlParamObj object:

Type in CMqlParamObj
Type of cell double Type of cell long
Type of cell datetime
Type of cell color
Type of cell string
  double_value cell value not used date/time display flags color name display flag not used
  integer_value cell value precision cell value cell value cell value not used
  string_value not used not used not used not used cell value

The table shows that when creating a table structure from some data, if this data is of a real type (written in the field of MqlParam double_value structure), then you can additionally write in the integer_value field the value of precision with which the data will be displayed in the table cell. The same applies to data with the datetime and color types, but flags are written to the double_value field, since the integer field is occupied by the property value itself.

This is optional. At the same time, the values of flags and precision in the cell will be set to zero. This value can then be changed both for a specific cell and for the entire column of the table.

A method that adds a new column to a table

//+------------------------------------------------------------------+
//| Добавляет столбец                                                |
//+------------------------------------------------------------------+
bool CTableModel::ColumnAddNew(const int index=-1)
  {
//--- Объявляем переменные
   CTableCell *cell=NULL;
   bool res=true;
//--- В цикле по количеству строк
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- получаем очередную строку
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
        {
         //--- добавляем в конец строки ячейку с типом double
         cell=row.CellAddNew(0.0);
         if(cell==NULL)
            res &=false;
         //--- очищаем ячейку
         else
            cell.ClearData();
        }
     }
//--- Если передан индекс колонки не отрицательный - сдвигаем колонку на указанную позицию
   if(res && index>-1)
      res &=this.ColumnMoveTo(this.CellsInRow(0)-1,index);
//--- Возвращаем результат
   return res;
  }

The index of the new column is passed to the method. First, new cells are added to the end of the row in all rows of the table, and then, if the index passed is not negative, all new cells are shifted by the specified index.

A method that sets the column data type

//+------------------------------------------------------------------+
//| Устанавливает тип данных столбца                                 |
//+------------------------------------------------------------------+
void CTableModel::ColumnSetDatatype(const uint index,const ENUM_DATATYPE type)
  {
//--- В цикле по всем строкам таблицы
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- получаем из каждой строки ячейку с индексом столбца и устанавливаем тип данных
      CTableCell *cell=this.GetCell(i, index);
      if(cell!=NULL)
         cell.SetDatatype(type);
     }
  }

In a loop through all rows of the table, get a cell of each row by index and set a data type for it. As a result, one identical value is set in the cells of the entire column.

A method that sets the column data accuracy

//+------------------------------------------------------------------+
//| Устанавливает точность данных столбца                            |
//+------------------------------------------------------------------+
void CTableModel::ColumnSetDigits(const uint index,const int digits)
  {
//--- В цикле по всем строкам таблицы
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- получаем из каждой строки ячейку с индексом столбца и устанавливаем точность данных
      CTableCell *cell=this.GetCell(i, index);
      if(cell!=NULL)
         cell.SetDigits(digits);
     }
  }

In a loop through all rows of the table, get a cell of each row by index and set a data type for it. As a result, the same value is set in the cells of the entire column.

Added methods to the table model class will allow operating with a set of cells as an entire table column. Columns of a table can only be controlled if the table has a header. The table will be static without the header.


Table Header Class

The table header is a list of column-header objects with a string value. They are located in a CListObj dynamic list. And the dynamic list forms the basis of the table header class.

Based on this, two classes will have to be created:

  1. The class of table column’s header object.
    It contains the text value of the header, column number, data type for the entire column, and methods for controlling column cells.
  2. The table header class.
    It contains a list of column header objects and access methods for controlling table columns.

Continue writing the code in the same file \MQL5\Scripts\Tables\Tables.mqh and write the table column header class:

//+------------------------------------------------------------------+
//| Класс заголовка столбца таблицы                                  |
//+------------------------------------------------------------------+
class CColumnCaption : public CObject
  {
protected:
//--- Переменные
   ushort            m_ushort_array[MAX_STRING_LENGTH];        // Массив символов заголовка
   uint              m_column;                                 // Номер столбца
   ENUM_DATATYPE     m_datatype;                               // Тип данных

public:
//--- (1) Устанавливает, (2) возвращает номер столбца
   void              SetColumn(const uint column)              { this.m_column=column;    }
   uint              Column(void)                        const { return this.m_column;    }

//--- (1) Устанавливает, (2) возвращает тип данных столбца
   ENUM_DATATYPE     Datatype(void)                      const { return this.m_datatype;  }
   void              SetDatatype(const ENUM_DATATYPE datatype) { this.m_datatype=datatype;}
   
//--- Очищает данные
   void              ClearData(void)                           { this.SetValue("");       }
   
//--- Устанавливает заголовок
   void              SetValue(const string value)
                       {
                        ::StringToShortArray(value,this.m_ushort_array);
                       }
//--- Возвращает текст заголовка
   string            Value(void) const
                       {
                        string res=::ShortArrayToString(this.m_ushort_array);
                        res.TrimLeft();
                        res.TrimRight();
                        return res;
                       }
   
//--- (1) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   void              Print(void);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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_COLUMN_CAPTION);  }
   
   
//--- Конструкторы/деструктор
                     CColumnCaption(void) : m_column(0) { this.SetValue(""); }
                     CColumnCaption(const uint column,const string value) : m_column(column) { this.SetValue(value); }
                    ~CColumnCaption(void) {}
  };

This is a highly simplified version of the table cell class. Consider some methods of the class.

A virtual method for comparing two objects

//+------------------------------------------------------------------+
//| Сравнение двух объектов                                          |
//+------------------------------------------------------------------+
int CColumnCaption::Compare(const CObject *node,const int mode=0) const
  {
   const CColumnCaption *obj=node;
   return(this.Column()>obj.Column() ? 1 : this.Column()<obj.Column() ? -1 : 0);
  }

The comparison is carried out by the index of the column for which the header was created.

A method of saving to a file

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
bool CColumnCaption::Save(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long))
      return(false);
//--- Сохраняем тип объекта
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return(false);

   //--- Сохраняем номер столбца
   if(::FileWriteInteger(file_handle,this.m_column,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем значение
   if(::FileWriteArray(file_handle,this.m_ushort_array)!=sizeof(this.m_ushort_array))
      return(false);
   
//--- Всё успешно
   return true;
  }

A method for downloading from a file

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
bool CColumnCaption::Load(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Загружаем и проверяем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Загружаем тип объекта
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return(false);

   //--- Загружаем номер столбца
   this.m_column=::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем значение
   if(::FileReadArray(file_handle,this.m_ushort_array)!=sizeof(this.m_ushort_array))
      return(false);
   
//--- Всё успешно
   return true;
  }

Similar methods were discussed in detail in the previous article. The logic here is exactly the same: first, the beginning-of-data markers and object type are recorded. And then, all its properties, element-wise. The reading takes place in the same order.

A method that returns description of an object

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
string CColumnCaption::Description(void)
  {
   return(::StringFormat("%s: Column %u, Value: \"%s\"",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Column(),this.Value()));
  }

A description string is created and returned in the format (Object Type: Column XX, Value "Value")

Method that outputs object description to the log

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта                                |
//+------------------------------------------------------------------+
void CColumnCaption::Print(void)
  {
   ::Print(this.Description());
  }

It just prints out the header description in the log.

Now such objects should be placed in the list, which will be the table header. Write the table header class:

//+------------------------------------------------------------------+
//| Класс заголовка таблицы                                          |
//+------------------------------------------------------------------+
class CTableHeader : public CObject
  {
protected:
   CColumnCaption    m_caption_tmp;                         // Объект заголовка столбца для поиска в списке
   CListObj          m_list_captions;                       // Список заголовков столбцлв
   
//--- Добавляет указанный заголовок в конец списка
   bool              AddNewColumnCaption(CColumnCaption *caption);
//--- Создаёт заголовок таблицы из строкового массива
   void              CreateHeader(string &array[]);
//--- Устанавливает позицию столбца всем заголовкам столбцов
   void              ColumnPositionUpdate(void);
   
public:
//--- Создаёт новый заголовок и добавляет в конец списка
   CColumnCaption   *CreateNewColumnCaption(const string caption);
   
//--- Возвращает (1) заголовок по индексу, (2) количество заголовков столбцов
   CColumnCaption   *GetColumnCaption(const uint index)        { return this.m_list_captions.GetNodeAtIndex(index);  }
   uint              ColumnsTotal(void)                  const { return this.m_list_captions.Total();                }
   
//--- Устанавливает значение указанному заголовку столбца
   void              ColumnCaptionSetValue(const uint index,const string value);
   
//--- (1) Устанавливает, (2) возвращает тип данных для указанного заголовка столбца
   void              ColumnCaptionSetDatatype(const uint index,const ENUM_DATATYPE type);
   ENUM_DATATYPE     ColumnCaptionDatatype(const uint index);
   
//--- (1) Удаляет (2) перемещает заголовок столбца
   bool              ColumnCaptionDelete(const uint index);
   bool              ColumnCaptionMoveTo(const uint caption_index, const uint index_to);
   
//--- Очищает данные заголовков столбцов
   void              ClearData(void);

//--- Очищает список заголовков столбцов
   void              Destroy(void)                             { this.m_list_captions.Clear();                       }

//--- (1) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   void              Print(const bool detail, const bool as_table=false, const int column_width=CELL_WIDTH_IN_CHARS);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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_HEADER); }
   
//--- Конструкторы/деструктор
                     CTableHeader(void) {}
                     CTableHeader(string &array[]) { this.CreateHeader(array);   }
                    ~CTableHeader(void){}
  };

Let us consider class methods.

A method that creates a new header and adds it to the end of the list of column headers

//+------------------------------------------------------------------+
//| Создаёт новый заголовок и добавляет в конец списка               |
//+------------------------------------------------------------------+
CColumnCaption *CTableHeader::CreateNewColumnCaption(const string caption)
  {
//--- Создаём новый объект заголовка
   CColumnCaption *caption_obj=new CColumnCaption(this.ColumnsTotal(),caption);
   if(caption_obj==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new column caption at position %u",__FUNCTION__, this.ColumnsTotal());
      return NULL;
     }
//--- Добавляем созданный заголовок в конец списка
   if(!this.AddNewColumnCaption(caption_obj))
     {
      delete caption_obj;
      return NULL;
     }
//--- Возвращаем указатель на объект
   return caption_obj;
  }

The header text is passed to the method. A new column header object is created with the specified text and an index equal to the number of headers in the list. This will be the index of the last header. Next, the created object is placed at the end of the list of column headers and a pointer to the created header is returned.

A method that adds the specified header to the list end

//+------------------------------------------------------------------+
//| Добавляет заголовок в конец списка                               |
//+------------------------------------------------------------------+
bool CTableHeader::AddNewColumnCaption(CColumnCaption *caption)
  {
//--- Если передан пустой объект - сообщаем и возвращаем false
   if(caption==NULL)
     {
      ::PrintFormat("%s: Error. Empty CColumnCaption object passed",__FUNCTION__);
      return false;
     }
//--- Устанавливаем индекс заголовка в списке и добавляем созданный заголовок в конец списка
   caption.SetColumn(this.ColumnsTotal());
   if(this.m_list_captions.Add(caption)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add caption (%u) to list",__FUNCTION__,this.ColumnsTotal());
      return false;
     }
//--- Успешно
   return true;
  }

A pointer to the column header object is passed to the method. It must be placed at the end of the header list. The method returns the result of adding an object to the list.

A method that creates a table header from a string array

//+------------------------------------------------------------------+
//| Создаёт заголовок таблицы из строкового массива                  |
//+------------------------------------------------------------------+
void CTableHeader::CreateHeader(string &array[])
  {
//--- Получаем из свойств массива количество столбцов таблицы
   uint total=array.Size();
//--- В цикле по размеру массива
//--- создаём все заголовки, добавляя каждый новый в конец списка
   for(uint i=0; i<total; i++)
      this.CreateNewColumnCaption(array[i]);
  }

A text array of headers is passed to the method. The size of the array determines the number of column header objects to be created, which are created when passing through the values of header texts in the array in a loop.

A method that sets a value to the specified header of a column

//+------------------------------------------------------------------+
//| Устанавливает значение в указанный заголовок столбца             |
//+------------------------------------------------------------------+
void CTableHeader::ColumnCaptionSetValue(const uint index,const string value)
  {
//--- Получаем из списка нужный заголовок и записываем в него новое значение
   CColumnCaption *caption=this.GetColumnCaption(index);
   if(caption!=NULL)
      caption.SetValue(value);
  }

This method allows you to set a new text value as specified by the header index.

A method that sets a data type for the specified column header

//+------------------------------------------------------------------+
//| Устанавливает тип данных для указанного заголовка столбца        |
//+------------------------------------------------------------------+
void CTableHeader::ColumnCaptionSetDatatype(const uint index,const ENUM_DATATYPE type)
  {
//--- Получаем из списка нужный заголовок и записываем в него новое значение
   CColumnCaption *caption=this.GetColumnCaption(index);
   if(caption!=NULL)
      caption.SetDatatype(type);
  }

This method allows setting a new value of the data stored in the column indicated by the header index. For each column of the table, you can set the type of data stored in the column cells. Setting a data type to the header object allows setting the same value for the entire column later. And you can read the data value of the entire column by reading this value from the header of this column.

A method that returns a data type of the specified column header

//+------------------------------------------------------------------+
//| Возвращает тип данных указанного заголовка столбца               |
//+------------------------------------------------------------------+
ENUM_DATATYPE CTableHeader::ColumnCaptionDatatype(const uint index)
  {
//--- Получаем из списка нужный заголовок и возвращаем из него тип данных столбца
   CColumnCaption *caption=this.GetColumnCaption(index);
   return(caption!=NULL ? caption.Datatype() : (ENUM_DATATYPE)WRONG_VALUE);
  }

This method allows retrieving a value of the data stored in the column by the header index. Getting the value from the header allows to find out the type of values stored in all cells of this column of the table.

A method that deletes the header of the specified column

//+------------------------------------------------------------------+
//| Удаляет заголовок указанного столбца                             |
//+------------------------------------------------------------------+
bool CTableHeader::ColumnCaptionDelete(const uint index)
  {
//--- Удаляем заголовок в списке по индексу
   if(!this.m_list_captions.Delete(index))
      return false;
//--- Обновляем индексы для оставшихся заголовков в списке
   this.ColumnPositionUpdate();
   return true;
  }

The object at the specified index is deleted from the list of headers. After successful deletion of the column header object, it is necessary to update indexes of the remaining objects in the list.

A method that moves a column header to the specified position

//+------------------------------------------------------------------+
//| Перемещает заголовок столбца на указанную позицию                |
//+------------------------------------------------------------------+
bool CTableHeader::ColumnCaptionMoveTo(const uint caption_index,const uint index_to)
  {
//--- Получаем нужный заголовок по индексу в списке, делая его текущим
   CColumnCaption *caption=this.GetColumnCaption(caption_index);
//--- Перемещаем текущий заголовок на указанную позицию в списке
   if(caption==NULL || !this.m_list_captions.MoveToIndex(index_to))
      return false;
//--- Обновляем индексы всех заголовков в списке
   this.ColumnPositionUpdate();
   return true;
  }

It allows to move the header from the specified index to a new position in the list.

A method that sets column positions for all headers

//+------------------------------------------------------------------+
//| Устанавливает позиции столбца всем заголовкам                    |
//+------------------------------------------------------------------+
void CTableHeader::ColumnPositionUpdate(void)
  {
//--- В цикле по всем заголовкам в списке
   for(int i=0;i<this.m_list_captions.Total();i++)
     {
      //--- получаем очередной заголовок и устанавливаем в него индекс столбца
      CColumnCaption *caption=this.GetColumnCaption(i);
      if(caption!=NULL)
         caption.SetColumn(this.m_list_captions.IndexOf(caption));
     }
  }

After deleting or moving an object in the list to another location, it is necessary to reassign indexes to all other objects in the list so that their indexes correspond to the actual position in the list. The method runs through all the objects in the list in a loop, gets the real index of each object and sets it as a property of the object.

A method that clears column header data in the list

//+------------------------------------------------------------------+
//| Очищает данные заголовков столбцов в списке                      |
//+------------------------------------------------------------------+
void CTableHeader::ClearData(void)
  {
//--- В цикле по всем заголовкам в списке
   for(uint i=0;i<this.ColumnsTotal();i++)
     {
      //--- получаем очередной заголовок и устанавливаем в него пустое значение
      CColumnCaption *caption=this.GetColumnCaption(i);
      if(caption!=NULL)
         caption.ClearData();
     }
  }

In a loop through all the column header objects in the list, get each regular object and set the header text to an empty value. This completely clears headers of each column of the table.

A method that returns description of an object

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
string CTableHeader::Description(void)
  {
   return(::StringFormat("%s: Captions total: %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.ColumnsTotal()));
  }

A string is created and returned in the format (Object Type: Captions total: XX)

Method that outputs object description to the log

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта                                |
//+------------------------------------------------------------------+
void CTableHeader::Print(const bool detail, const bool as_table=false, const int column_width=CELL_WIDTH_IN_CHARS)
  {
//--- Количество заголовков
   int total=(int)this.ColumnsTotal();
   
//--- Если вывод в табличном виде
   string res="";
   if(as_table)
     {
      //--- создаём строку таблицы из значений всех заголовков
      res="|";
      for(int i=0;i<total;i++)
        {
         CColumnCaption *caption=this.GetColumnCaption(i);
         if(caption==NULL)
            continue;
         res+=::StringFormat("%*s |",column_width,caption.Value());
        }
      //--- Выводим строку в журнал и уходим
      ::Print(res);
      return;
     }
     
//--- Выводим заголовок в виде описания строки
   ::Print(this.Description()+(detail ? ":" : ""));
   
//--- Если детализированное описание
   if(detail)
     {
      //--- В цикле по списку заголовков строки
      for(int i=0; i<total; i++)
        {
         //--- получаем текущий заголовок и добавляем в итоговую строку его описание
         CColumnCaption *caption=this.GetColumnCaption(i);
         if(caption!=NULL)
            res+="  "+caption.Description()+(i<total-1 ? "\n" : "");
        }
      //--- Выводим в журнал созданную в цикле строку
      ::Print(res);
     }
  }

The method can log header description in tabular form and as a list of column headers.

Methods for saving to a file and downloading a header from a file

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
bool CTableHeader::Save(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long))
      return(false);
//--- Сохраняем тип объекта
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return(false);

//--- Сохраняем список заголовков
   if(!this.m_list_captions.Save(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }
//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
bool CTableHeader::Load(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Загружаем и проверяем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Загружаем тип объекта
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return(false);

//--- Загружаем список заголовков
   if(!this.m_list_captions.Load(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }

The logic of the methods is commented out in the code and does not differ in any way from similar methods of other already created classes for creating tables.

We have everything ready to start assembling the table classes. The table class should be able to build a table based on its model, and should have a header by which table columns will be named. If you do not specify table’s header in the table, it will be built only according to the model, it will be static, and its features will be limited only by viewing the table. For simple tables, this is quite enough. However, in order to interact with the user using the Controller component, table’s header must be determined in the table. This will provide a wide range of options for controlling tables and their data. But we shall do it all later. Let us look at table classes now.


Table Classes

Continue writing the code in the same file and implement the table class:

//+------------------------------------------------------------------+
//| Класс таблицы                                                    |
//+------------------------------------------------------------------+
class CTable : public CObject 
  {
private:
//--- Заполняет массив заголовков столбцов в стиле Excel
   bool              FillArrayExcelNames(const uint num_columns);
//--- Возвращает наименование столбца как в Excel
   string            GetExcelColumnName(uint column_number);
//--- Возвращает доступность заголовка
   bool              HeaderCheck(void) const { return(this.m_table_header!=NULL && this.m_table_header.ColumnsTotal()>0);  }
   
protected:
   CTableModel      *m_table_model;                               // Указатель на модель таблицы
   CTableHeader     *m_table_header;                              // Указатель на заголовок таблицы
   CList             m_list_rows;                                 // Список массивов параметров из полей структуры
   string            m_array_names[];                             // Массив заголовков столбцов
   int               m_id;                                        // Идентификатор таблицы
//--- Копирует массив наименований заголовков
   bool              ArrayNamesCopy(const string &column_names[],const uint columns_total);
   
public:
//--- (1) Устанавливает, (2) возвращает модель таблицы
   void              SetTableModel(CTableModel *table_model)      { this.m_table_model=table_model;      }
   CTableModel      *GetTableModel(void)                          { return this.m_table_model;           }
//--- (1) Устанавливает, (2) возвращает заголовок
   void              SetTableHeader(CTableHeader *table_header)   { this.m_table_header=m_table_header;  }
   CTableHeader     *GetTableHeader(void)                         { return this.m_table_header;          }

//--- (1) Устанавливает, (2) возвращает идентификатор таблицы
   void              SetID(const int id)                          { this.m_id=id;                        }
   int               ID(void)                               const { return this.m_id;                    }
   
//--- Очищает данные заголовков столбцов
   void              HeaderClearData(void)
                       {
                        if(this.m_table_header!=NULL)
                           this.m_table_header.ClearData();
                       }
//--- Удаляет заголовок таблицы
   void              HeaderDestroy(void)
                       {
                        if(this.m_table_header==NULL)
                           return;
                        this.m_table_header.Destroy();
                        this.m_table_header=NULL;
                       }
                       
//--- (1) Очищает все данные, (2) уничтожает модель таблицы и заголовок
   void              ClearData(void)
                       {
                        if(this.m_table_model!=NULL)
                           this.m_table_model.ClearData();
                       }
   void              Destroy(void)
                       {
                        if(this.m_table_model==NULL)
                           return;
                        this.m_table_model.Destroy();
                        this.m_table_model=NULL;
                       }
   
//--- Возвращает (1) заголовок, (2) ячейку, (3) строку по индексу, количество (4) строк, (5) столбцов, ячеек (6) в указанной строке, (7) в таблице
   CColumnCaption   *GetColumnCaption(const uint index)        { return(this.m_table_header!=NULL  ?  this.m_table_header.GetColumnCaption(index)  :  NULL);   }
   CTableCell       *GetCell(const uint row, const uint col)   { return(this.m_table_model!=NULL   ?  this.m_table_model.GetCell(row,col)          :  NULL);   }
   CTableRow        *GetRow(const uint index)                  { return(this.m_table_model!=NULL   ?  this.m_table_model.GetRow(index)             :  NULL);   }
   uint              RowsTotal(void)                     const { return(this.m_table_model!=NULL   ?  this.m_table_model.RowsTotal()               :  0);      }
   uint              ColumnsTotal(void)                  const { return(this.m_table_model!=NULL   ?  this.m_table_model.CellsInRow(0)             :  0);      }
   uint              CellsInRow(const uint index)              { return(this.m_table_model!=NULL   ?  this.m_table_model.CellsInRow(index)         :  0);      }
   uint              CellsTotal(void)                          { return(this.m_table_model!=NULL   ?  this.m_table_model.CellsTotal()              :  0);      }

//--- Устанавливает (1) значение, (2) точность, (3) флаги отображения времени, (4) флаг отображения имён цветов в указанную ячейку
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) Назначает, (2) отменяет объект в ячейке
   void              CellAssignObject(const uint row, const uint col,CObject *object);
   void              CellUnassignObject(const uint row, const uint col);
//--- Возвращает строковое значение указанной ячейки
   virtual string    CellValueAt(const uint row, const uint col);

protected:
//--- (1) Удаляет (2) перемещает ячейку
   bool              CellDelete(const uint row, const uint col);
   bool              CellMoveTo(const uint row, const uint cell_index, const uint index_to);
   
public:
//--- (1) Возвращает, (2) выводит в журнал описание ячейки, (3) назначенный в ячейку объект
   string            CellDescription(const uint row, const uint col);
   void              CellPrint(const uint row, const uint col);
//---Возвращает (1) назначенный в ячейку объект, (2) тип назначенного в ячейку объекта
   CObject          *CellGetObject(const uint row, const uint col);
   ENUM_OBJECT_TYPE  CellGetObjType(const uint row, const uint col);
   
//--- Создаёт новую строку и (1) добавляет в конец списка, (2) вставляет в указанную позицию списка
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Удаляет (2) перемещает строку, (3) очищает данные строки
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Возвращает, (2) выводит в журнал описание строки
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Добавляет новый, (2) удаляет, (3) перемещает столбец, (4) очищает данные столбца
   bool              ColumnAddNew(const string caption,const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint index, const uint index_to);
   void              ColumnClearData(const uint index);
   
//--- Устанавливает (1) значение указанному заголовку, (2) точность данных указанному столбцу
   void              ColumnCaptionSetValue(const uint index,const string value);
   void              ColumnSetDigits(const uint index,const int digits);
   
//--- (1) Устанавливает, (2) возвращает тип данных для указанного столбца
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   ENUM_DATATYPE     ColumnDatatype(const uint index);
   
//--- (1) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   void              Print(const int column_width=CELL_WIDTH_IN_CHARS);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);           }
   
//--- Конструкторы/деструктор
                     CTable(void) : m_table_model(NULL), m_table_header(NULL) { this.m_list_rows.Clear();}
template<typename T> CTable(T &row_data[][],const string &column_names[]);
                     CTable(const uint num_rows, const uint num_columns);
                     CTable(const matrix &row_data,const string &column_names[]);
                    ~CTable (void);
  };

Pointers to the header and model table are declared in the class. To build a table, you must first create a table model from the data being passed to class constructors. The table can automatically generate a header with column names in the MS Excel style, where each column is assigned a name consisting of Latin letters.

The algorithm for calculating names is as follows:

  1. Single—letter names - the first 26 columns are indicated by letters from "A" to "Z".

  2. Two—letter names  - after "Z", columns are designated by a combination of two letters. The first letter changes more slowly, and the second one iterates the entire alphabet. For example:

    • "AA", "AB", "AC", ..., "AZ",
    • then "BA", "BB", ..., "BZ",
    • etc.
  3. Three—letter names  - after "ZZ", columns are designated by a combination of three letters. The principle is the same:

    • "AAA", "AAB", ..., "AAZ",
    • then "ABA", "ABB", ..., "ABZ",
    • etc.
  4. The general principle is that column names can be considered as numbers in the number system with base 26, where "A" corresponds to 1, "B" corresponds to 2, ..., "Z" — 26. For example:

    • "A" = 1,
    • "Z" = 26,
    • "AA" = 27 (1 * 26^1 + 1),
    • "AB" = 28 (1 * 26^1 + 2),
    • "BA" = 53 (2 * 26^1 + 1).

Thus, the algorithm automatically generates column names, increasing them in accordance with the considered principle. The maximum number of columns in Excel depends on the program version (for example, in Excel 2007 and later versions there are 16,384, ending with "XFD"). The algorithm created here is not limited to this figure. It can give names to the number of columns equal to INT_MAX:

//+------------------------------------------------------------------+
//| Возвращает наименование столбца как в Excel                      |
//+------------------------------------------------------------------+
string CTable::GetExcelColumnName(uint column_number)
  {
   string column_name="";
   uint index=column_number;

//--- Проверяем, что номер столбца больше 0
   if(index==0)
      return (__FUNCTION__+": Error. Invalid column number passed");
   
//--- Преобразование номера в название столбца
   while(!::IsStopped() && index>0)
     {
      index--;                                           // Уменьшаем номер на 1, чтобы сделать его 0-индексным
      uint  remainder =index % 26;                       // Остаток от деления на 26
      uchar char_code ='A'+(uchar)remainder;             // Рассчитываем код символа (буквы)
      column_name=::CharToString(char_code)+column_name; // Добавляем букву в начало строки
      index/=26;                                         // Переходим к следующему разряду
     }
   return column_name;
  }
//+------------------------------------------------------------------+
//| Заполняет массив заголовков столбцов в стиле Excel               |
//+------------------------------------------------------------------+
bool CTable::FillArrayExcelNames(const uint num_columns)
  {
   ::ResetLastError();
   if(::ArrayResize(this.m_array_names,num_columns,num_columns)!=num_columns)
     {
      ::PrintFormat("%s: ArrayResize() failed. Error %d",__FUNCTION__,::GetLastError());
      return false;
     }
   for(int i=0;i<(int)num_columns;i++)
      this.m_array_names[i]=this.GetExcelColumnName(i+1);

   return true;
  }

The methods allow filling in an array of Excel-style column names.

Consider parametric constructors of a class.

A template constructor specifying a two-dimensional array of data and a string array of headers

//+-------------------------------------------------------------------+
//| Конструктор с указанием массива таблицы и массива заголовков.     | 
//| Определяет количество и наименования колонок согласно column_names|
//| Количество строк определены размером массива данных row_data,     |
//| который используется и для заполнения таблицы                     |
//+-------------------------------------------------------------------+
template<typename T>
CTable::CTable(T &row_data[][],const string &column_names[]) : m_id(-1)
  {
   this.m_table_model=new CTableModel(row_data);
   if(column_names.Size()>0)
      this.ArrayNamesCopy(column_names,row_data.Range(1));
   else
     {
      ::PrintFormat("%s: An empty array names was passed. The header array will be filled in Excel style (A, B, C)",__FUNCTION__);
      this.FillArrayExcelNames((uint)::ArrayRange(row_data,1));
     }
   this.m_table_header=new CTableHeader(this.m_array_names);
  }

An array of data with any type is passed to the template constructor from the enumeration ENUM_DATATYPE. Next, it will be converted to the data type used by tables (double, long, datetime, color, string) to create a table model and an array of column headers. If the header array is empty, MS Excel-style headers will be created.

Constructor with indication of the number of rows and columns of the table

//+------------------------------------------------------------------+
//| Конструктор таблицы с определением количества колонок и строк.   |
//| Колонки будут иметь Excel-наименования "A", "B", "C" и т.д.      |
//+------------------------------------------------------------------+
CTable::CTable(const uint num_rows,const uint num_columns) : m_table_header(NULL), m_id(-1)
  {
   this.m_table_model=new CTableModel(num_rows,num_columns);
   if(this.FillArrayExcelNames(num_columns))
      this.m_table_header=new CTableHeader(this.m_array_names);
  }

The constructor creates an empty table model with an MS Excel-style header.

The constructor based on a data matrix and a column headers array

//+-------------------------------------------------------------------+
//| Конструктор таблицы с инициализацией колонок согласно column_names|
//| Количество строк определены параметром row_data, с типом matrix   |
//+-------------------------------------------------------------------+
CTable::CTable(const matrix &row_data,const string &column_names[]) : m_id(-1)
  {
   this.m_table_model=new CTableModel(row_data);
   if(column_names.Size()>0)
      this.ArrayNamesCopy(column_names,(uint)row_data.Cols());
   else
     {
      ::PrintFormat("%s: An empty array names was passed. The header array will be filled in Excel style (A, B, C)",__FUNCTION__);
      this.FillArrayExcelNames((uint)row_data.Cols());
     }
   this.m_table_header=new CTableHeader(this.m_array_names);
  }

A data matrix with the double  type is passed to the constructor to create a table model and a column headers array. If the header array is empty, MS Excel-style headers will be created.

In the class destructor, the model and the table header are destroyed.

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
CTable::~CTable(void)
  {
   if(this.m_table_model!=NULL)
     {
      this.m_table_model.Destroy();
      delete this.m_table_model;
     }
   if(this.m_table_header!=NULL)
     {
      this.m_table_header.Destroy();
      delete this.m_table_header;
     }
  }

A method for comparing two objects

//+------------------------------------------------------------------+
//| Сравнение двух объектов                                          |
//+------------------------------------------------------------------+
int CTable::Compare(const CObject *node,const int mode=0) const
  {
   const CTable *obj=node;
   return(this.ID()>obj.ID() ? 1 : this.ID()<obj.ID() ? -1 : 0);
  }

Each table can be assigned an identifier if the program is supposed to create multiple tables. Tables in the program can be identified by the set identifier, which by default has the value -1. If the created tables are placed in lists (CList, CArrayObj, etc.), then the comparison method allows for comparing tables by their identifiers to search and sort them:

//+------------------------------------------------------------------+
//| Сравнение двух объектов                                          |
//+------------------------------------------------------------------+
int CTable::Compare(const CObject *node,const int mode=0) const
  {
   const CTable *obj=node;
   return(this.ID()>obj.ID() ? 1 : this.ID()<obj.ID() ? -1 : 0);
  }

A method that copies a header names array

//+------------------------------------------------------------------+
//| Копирует массив наименований заголовков                          |
//+------------------------------------------------------------------+
bool CTable::ArrayNamesCopy(const string &column_names[],const uint columns_total)
  {
   if(columns_total==0)
     {
      ::PrintFormat("%s: Error. The table has no columns",__FUNCTION__);
      return false;
     }
   if(columns_total>column_names.Size())
     {
      ::PrintFormat("%s: The number of header names is less than the number of columns. The header array will be filled in Excel style (A, B, C)",__FUNCTION__);
      return this.FillArrayExcelNames(columns_total);
     }
   uint total=::fmin(columns_total,column_names.Size());
   return(::ArrayCopy(this.m_array_names,column_names,0,0,total)==total);
  }

An array of headers and the number of columns in the created table model is passed to the method. If there are no columns in the table, then there is no need to create headers. Report this and return false. If there are more columns in the table model than headers in the passed array, then all the headers will be created in Excel style so that the table does not have columns without captions in the headers.

A method that sets a value to the specified cell

//+------------------------------------------------------------------+
//| Устанавливает значение в указанную ячейку                        |
//+------------------------------------------------------------------+
template<typename T>
void CTable::CellSetValue(const uint row, const uint col, const T value)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.CellSetValue(row,col,value);
  }

Here we are referring to the method with the same name for the table model object.

essentially, in this class, many methods are duplicated from the table model class. If the model is created, then its similar method of getting or setting the property is called.

A method for operating table cells

//+------------------------------------------------------------------+
//| Устанавливает точность в указанную ячейку                        |
//+------------------------------------------------------------------+
void CTable::CellSetDigits(const uint row, const uint col, const int digits)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.CellSetDigits(row,col,digits);
  }
//+------------------------------------------------------------------+
//| Устанавливает флаги отображения времени в указанную ячейку       |
//+------------------------------------------------------------------+
void CTable::CellSetTimeFlags(const uint row, const uint col, const uint flags)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.CellSetTimeFlags(row,col,flags);
  }
//+------------------------------------------------------------------+
//| Устанавливает флаг отображения имён цветов в указанную ячейку    |
//+------------------------------------------------------------------+
void CTable::CellSetColorNamesFlag(const uint row, const uint col, const bool flag)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.CellSetColorNamesFlag(row,col,flag);
  }
//+------------------------------------------------------------------+
//| Назначает объект в ячейку                                        |
//+------------------------------------------------------------------+
void CTable::CellAssignObject(const uint row, const uint col,CObject *object)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.CellAssignObject(row,col,object);
  }
//+------------------------------------------------------------------+
//| Отменяет объект в ячейке                                         |
//+------------------------------------------------------------------+
void CTable::CellUnassignObject(const uint row, const uint col)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.CellUnassignObject(row,col);
  }
//+------------------------------------------------------------------+
//| Возвращает строковое значение указанной ячейки                   |
//+------------------------------------------------------------------+
string CTable::CellValueAt(const uint row,const uint col)
  {
   CTableCell *cell=this.GetCell(row,col);
   return(cell!=NULL ? cell.Value() : "");
  }
//+------------------------------------------------------------------+
//| Удаляет ячейку                                                   |
//+------------------------------------------------------------------+
bool CTable::CellDelete(const uint row, const uint col)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.CellDelete(row,col) : false);
  }
//+------------------------------------------------------------------+
//| Перемещает ячейку                                                |
//+------------------------------------------------------------------+
bool CTable::CellMoveTo(const uint row, const uint cell_index, const uint index_to)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.CellMoveTo(row,cell_index,index_to) : false);
  }
//+------------------------------------------------------------------+
//| Возвращает назначенный в ячейку объект                           |
//+------------------------------------------------------------------+
CObject *CTable::CellGetObject(const uint row, const uint col)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.CellGetObject(row,col) : NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает тип назначенного в ячейку объекта                     |
//+------------------------------------------------------------------+
ENUM_OBJECT_TYPE CTable::CellGetObjType(const uint row,const uint col)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.CellGetObjType(row,col) : (ENUM_OBJECT_TYPE)WRONG_VALUE);
  }
//+------------------------------------------------------------------+
//| Возвращает описание ячейки                                       |
//+------------------------------------------------------------------+
string CTable::CellDescription(const uint row, const uint col)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.CellDescription(row,col) : "");
  }
//+------------------------------------------------------------------+
//| Выводит в журнал описание ячейки                                 |
//+------------------------------------------------------------------+
void CTable::CellPrint(const uint row, const uint col)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.CellPrint(row,col);
  }

Methods for working with table rows

//+------------------------------------------------------------------+
//| Создаёт новую строку и добавляет в конец списка                  |
//+------------------------------------------------------------------+
CTableRow *CTable::RowAddNew(void)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.RowAddNew() : NULL);
  }
//+------------------------------------------------------------------+
//| Создаёт новую строку и вставляет в указанную позицию списка      |
//+------------------------------------------------------------------+
CTableRow *CTable::RowInsertNewTo(const uint index_to)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.RowInsertNewTo(index_to) : NULL);
  }
//+------------------------------------------------------------------+
//| Удаляет строку                                                   |
//+------------------------------------------------------------------+
bool CTable::RowDelete(const uint index)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.RowDelete(index) : false);
  }
//+------------------------------------------------------------------+
//| Перемещает строку                                                |
//+------------------------------------------------------------------+
bool CTable::RowMoveTo(const uint row_index, const uint index_to)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.RowMoveTo(row_index,index_to) : false);
  }
//+------------------------------------------------------------------+
//| Очищает данные строки                                            |
//+------------------------------------------------------------------+
void CTable::RowClearData(const uint index)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.RowClearData(index);
  }
//+------------------------------------------------------------------+
//| Возвращает описание строки                                       |
//+------------------------------------------------------------------+
string CTable::RowDescription(const uint index)
  {
   return(this.m_table_model!=NULL ? this.m_table_model.RowDescription(index) : "");
  }
//+------------------------------------------------------------------+
//| Выводит в журнал описание строки                                 |
//+------------------------------------------------------------------+
void CTable::RowPrint(const uint index,const bool detail)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.RowPrint(index,detail);
  }

A method that creates a new column and adds it to the specified table position

//+------------------------------------------------------------------+
//| Создаёт новый столбец и добавляет его в указанную позицию таблицы|
//+------------------------------------------------------------------+
bool CTable::ColumnAddNew(const string caption,const int index=-1)
  {
//--- Если нет модели таблицы, либо ошибка добавления нового столбца к модели - возвращаем false
   if(this.m_table_model==NULL || !this.m_table_model.ColumnAddNew(index))
      return false;
//--- Если нет заголовка - возвращаем true (столбец добавлен без заголовка)
   if(this.m_table_header==NULL)
      return true;
   
//--- Проверяем создание нового заголовка столбца и, если не создан - возвращаем false
   CColumnCaption *caption_obj=this.m_table_header.CreateNewColumnCaption(caption);
   if(caption_obj==NULL)
      return false;
//--- Если передан не отрицательный индекс - возвращаем результат перемещения заголовка на указанный индекс
//--- В ином случае уже всё готово - просто возвращаем true
   return(index>-1 ? this.m_table_header.ColumnCaptionMoveTo(caption_obj.Column(),index) : true);
  }

If there is no table model the method immediately returns an error. If the column has been successfully added to the table model, try to add the appropriate header. If the table does not have a header, return the success of creating a new column. If there is a header, add a new column header and move it to the specified position in the list.

Other methods for working with columns

//+------------------------------------------------------------------+
//| Удаляет столбец                                                  |
//+------------------------------------------------------------------+
bool CTable::ColumnDelete(const uint index)
  {
   if(!this.HeaderCheck() || !this.m_table_header.ColumnCaptionDelete(index))
      return false;
   return this.m_table_model.ColumnDelete(index);
  }
//+------------------------------------------------------------------+
//| Перемещает столбец                                               |
//+------------------------------------------------------------------+
bool CTable::ColumnMoveTo(const uint index, const uint index_to)
  {
   if(!this.HeaderCheck() || !this.m_table_header.ColumnCaptionMoveTo(index,index_to))
      return false;
   return this.m_table_model.ColumnMoveTo(index,index_to);
  }
//+------------------------------------------------------------------+
//| Очищает данные столбца                                           |
//+------------------------------------------------------------------+
void CTable::ColumnClearData(const uint index)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnClearData(index);
  }
//+------------------------------------------------------------------+
//| Устанавливает значение указанному заголовку                      |
//+------------------------------------------------------------------+
void CTable::ColumnCaptionSetValue(const uint index,const string value)
  {
   CColumnCaption *caption=this.m_table_header.GetColumnCaption(index);
   if(caption!=NULL)
      caption.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Устанавливает тип данных для указанного столбца                  |
//+------------------------------------------------------------------+
void CTable::ColumnSetDatatype(const uint index,const ENUM_DATATYPE type)
  {
//--- Если модель таблицы есть - устанавливаем тип данных для столбца
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetDatatype(index,type);
//--- Если заголовок есть - устанавливаем тип данных для заголовка
   if(this.m_table_header!=NULL)
      this.m_table_header.ColumnCaptionSetDatatype(index,type);
  }
//+------------------------------------------------------------------+
//| Устанавливает точность данных указанному столбцу                 |
//+------------------------------------------------------------------+
void CTable::ColumnSetDigits(const uint index,const int digits)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetDigits(index,digits);
  }
//+------------------------------------------------------------------+
//| Возвращает тип данных для указанного столбца                     |
//+------------------------------------------------------------------+
ENUM_DATATYPE CTable::ColumnDatatype(const uint index)
  {
   return(this.m_table_header!=NULL ? this.m_table_header.ColumnCaptionDatatype(index) : (ENUM_DATATYPE)WRONG_VALUE);
  }

A method that returns object description

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
string CTable::Description(void)
  {
   return(::StringFormat("%s: Rows total: %u, Columns total: %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.RowsTotal(),this.ColumnsTotal()));
  }

Creates and returns the string in the format (Object Type: Rows total: XX, Columns total: XX)

Method that outputs object description to the log

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта                                |
//+------------------------------------------------------------------+
void CTable::Print(const int column_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.HeaderCheck())
     {
      //--- Выводим заголовок в виде описания строки
      ::Print(this.Description()+":");
        
      //--- Количество заголовков
      int total=(int)this.ColumnsTotal();
      
      string res="";
      //--- создаём строку из значений всех заголовков столбцов таблицы
      res="|";
      for(int i=0;i<total;i++)
        {
         CColumnCaption *caption=this.GetColumnCaption(i);
         if(caption==NULL)
            continue;
         res+=::StringFormat("%*s |",column_width,caption.Value());
        }
      //--- Дополняем строку слева заголовком
      string hd="|";
      hd+=::StringFormat("%*s ",column_width,"n/n");
      res=hd+res;
      //--- Выводим строку заголовка в журнал
      ::Print(res);
     }
     
//--- Пройдём в цикле по всем строкам таблицы и распечатаем их в табличном виде
   for(uint i=0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
        {
         //--- создаём строку таблицы из значений всех ячеек
         string head=" "+(string)row.Index();
         string res=::StringFormat("|%-*s |",column_width,head);
         for(int i=0;i<(int)row.CellsTotal();i++)
           {
            CTableCell *cell=row.GetCell(i);
            if(cell==NULL)
               continue;
            res+=::StringFormat("%*s |",column_width,cell.Value());
           }
         //--- Выводим строку в журнал
         ::Print(res);
        }
     }
  }

The method outputs a description to the log, and below is a table with the header and data.

A method of saving the table to a file

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
bool CTable::Save(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long))
      return(false);
//--- Сохраняем тип объекта
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return(false);
      
//--- Сохраняем идентификатор
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      return(false);
//--- Проверяем модель таблицы
   if(this.m_table_model==NULL)
      return false;
//--- Сохраняем модель таблицы
   if(!this.m_table_model.Save(file_handle))
      return(false);

//--- Проверяем заголовок таблицы
   if(this.m_table_header==NULL)
      return false;
//--- Сохраняем заголовок таблицы
   if(!this.m_table_header.Save(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }

Successful saving will only occur if both the table model and its header have been created. The header can be empty, that is, it can have no columns, but the object must be created.

Method uploading the table from a file

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
bool CTable::Load(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Загружаем и проверяем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Загружаем тип объекта
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return(false);

//--- Загружаем идентификатор
   this.m_id=::FileReadInteger(file_handle,INT_VALUE);
//--- Проверяем модель таблицы
   if(this.m_table_model==NULL && (this.m_table_model=new CTableModel())==NULL)
      return(false);
//--- Загружаем модель таблицы
   if(!this.m_table_model.Load(file_handle))
      return(false);

//--- Проверяем заголовок таблицы
   if(this.m_table_header==NULL && (this.m_table_header=new CTableHeader())==NULL)
      return false;
//--- Загружаем заголовок таблицы
   if(!this.m_table_header.Load(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }

Given that the table stores both model data and header data, in this method (if the model or header is not created in the table) they are pre-created, and after that, their data is loaded from the file.

The simple table class is ready.

Now, consider the option of inheriting from a simple table class — create a table class that builds on the data recorded in the CList:

//+------------------------------------------------------------------+
//| Класс для создания таблиц на основе массива параметров           |
//+------------------------------------------------------------------+
class CTableByParam : public CTable
  {
public:
   virtual int       Type(void)  const { return(OBJECT_TYPE_TABLE_BY_PARAM);  }
//--- Конструктор/деструктор
                     CTableByParam(void) { this.m_list_rows.Clear(); }
                     CTableByParam(CList &row_data,const string &column_names[]);
                    ~CTableByParam(void) {}
  };

Here, the table type is returned as OBJECT_TYPE_TABLE_BY_PARAM, and the table model and the header are built in the class constructor:

//+------------------------------------------------------------------+
//| Конструктор с указанием массива таблицы на основе списка row_data|
//| содержащего объекты с данными полей структуры.                   | 
//| Определяет количество и наименования колонок согласно количеству |
//| наименований столбцов в массиве column_names                     |
//+------------------------------------------------------------------+
CTableByParam::CTableByParam(CList &row_data,const string &column_names[])
  {
//--- Копируем переданный список данных в переменную и
//--- создаём на основе этого списка модель таблицы
   this.m_list_rows=row_data;
   this.m_table_model=new CTableModel(this.m_list_rows);
   
//--- Копируем переданный список заголовков в m_array_names и
//--- создаём на основе этого списка заголовок таблицы
   this.ArrayNamesCopy(column_names,column_names.Size());
   this.m_table_header=new CTableHeader(this.m_array_names);
  }

Based on this example, one can create some other classes of tables, but for assume that everything created today is quite enough to create a wide variety of tables and an extensive set of possible data.

Let us test everything that we have.


Testing the Result

In \MQL5\Scripts\Tables\ folder, create a new script named TestEmptyTable.mq5, connect the created table class file to it and create an empty 4x4 table:

//+------------------------------------------------------------------+
//|                                               TestEmptyTable.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 "Tables.mqh"
  
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Создаём пустую таблицу 4x4
   CTable *table=new CTable(4,4);
   if(table==NULL)
      return;
//--- Распечатываем её в журнале и удаляем созданный объект
   table.Print(10);
   delete table;
  }

The script will produce the following output in the log:

Table: Rows total: 4, Columns total: 4:
|       n/n |         A |         B |         C |         D |
| 0         |           |           |           |           |
| 1         |           |           |           |           |
| 2         |           |           |           |           |
| 3         |           |           |           |           |

Here, the column headers are created automatically in the MS Excel style.

Write another script \MQL5\Scripts\Tables\TestTArrayTable.mq5:

//+------------------------------------------------------------------+
//|                                              TestTArrayTable.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 "Tables.mqh"
  
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Объявляем и инициализируем double-массив 4x4
   double array[4][4]={{ 1,  2,  3,  4},
                       { 5,  6,  7,  8},
                       { 9, 10, 11, 12},
                       {13, 14, 15, 16}};
                       
//--- Объявляем и инициализируем массив заголовков столбцов
   string headers[]={"Column 1","Column 2","Column 3","Column 4"};
   
//--- Создаём таблицу на основе массива данных и массива заголовков
   CTable *table=new CTable(array,headers);
   if(table==NULL)
      return;
//--- Распечатываем таблицу в журнале и удаляем созданный объект
   table.Print(10);
   delete table;
  }

As a result of script operation the following table will be displayed in the log:

Table: Rows total: 4, Columns total: 4:
|       n/n |  Column 1 |  Column 2 |  Column 3 |  Column 4 |
| 0         |      1.00 |      2.00 |      3.00 |      4.00 |
| 1         |      5.00 |      6.00 |      7.00 |      8.00 |
| 2         |      9.00 |     10.00 |     11.00 |     12.00 |
| 3         |     13.00 |     14.00 |     15.00 |     16.00 |

Here, columns are already titled from the headers array being passed to the class constructor. A two-dimensional array representing the data for creating a table can be of any type from the ENUM_DATATYPE enumeration.

All types are automatically converted to the five types used in the table model class: double, long, datetime, color and string.

Write the script \MQL5\Scripts\Tables\TestMatrixTable.mq5 to test the table on matrix data:

//+------------------------------------------------------------------+
//|                                              TestMatrixTable.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 "Tables.mqh"
  
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Объявляем и инициализируем матрицу 4x4
   matrix row_data = {{ 1,  2,  3,  4},
                      { 5,  6,  7,  8},
                      { 9, 10, 11, 12},
                      {13, 14, 15, 16}};
                       
//--- Объявляем и инициализируем массив заголовков столбцов
   string headers[]={"Column 1","Column 2","Column 3","Column 4"};
   
//--- Создаём таблицу на основе матрицы и массива заголовков
   CTable *table=new CTable(row_data,headers);
   if(table==NULL)
      return;
//--- Распечатываем таблицу в журнале и удаляем созданный объект
   table.Print(10);
   delete table;
  }

The result will be a table similar to the one built on the basis of a two-dimensional 4x4 array:

Table: Rows total: 4, Columns total: 4:
|       n/n |  Column 1 |  Column 2 |  Column 3 |  Column 4 |
| 0         |      1.00 |      2.00 |      3.00 |      4.00 |
| 1         |      5.00 |      6.00 |      7.00 |      8.00 |
| 2         |      9.00 |     10.00 |     11.00 |     12.00 |
| 3         |     13.00 |     14.00 |     15.00 |     16.00 |

Now, write the script \MQL5\Scripts\Tables\TestDealsTable.mq5, where we count all historical trades, create a trade table based on them and print it out in the log:

//+------------------------------------------------------------------+
//|                                               TestDealsTable.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 "Tables.mqh"
  
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Объявляем список сделок, объект параметров сделки и структуру параметров
   CList rows_data;
   CMqlParamObj *cell=NULL;
   MqlParam param={};
   
//--- Выбираем всю историю
   if(!HistorySelect(0,TimeCurrent()))
      return;
      
//--- Создаём список сделок в массиве массивов (CList in CList)
//--- (одна строка - одна сделка, столбцы - объекты свойств сделки)
   int total=HistoryDealsTotal();
   for(int i=0;i<total;i++)
     {
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket==0)
         continue;
      
      //--- Добавляем к списку сделок новую строку свойств очередной сделки
      CList *row=DataListCreator::AddNewRowToDataList(&rows_data);
      if(row==NULL)
         continue;
      
      //--- Создаём "ячейки" с параметрами сделки и
      //--- добавляем их к созданной строке свойств сделки
      string symbol=HistoryDealGetString(ticket,DEAL_SYMBOL);
      int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
      
      //--- Время совершения сделки (столбец 0)
      param.type=TYPE_DATETIME;
      param.integer_value=HistoryDealGetInteger(ticket,DEAL_TIME);
      param.double_value=(TIME_DATE|TIME_MINUTES|TIME_SECONDS);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Имя символа (столбец 1)
      param.type=TYPE_STRING;
      param.string_value=symbol;
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Тикет сделки (столбец 2)
      param.type=TYPE_LONG;
      param.integer_value=(long)ticket;
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Ордер, на основание которого выполнена сделка (столбец 3)
      param.type=TYPE_LONG;
      param.integer_value=HistoryDealGetInteger(ticket,DEAL_ORDER);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Идентификатор позиции (столбец 4)
      param.type=TYPE_LONG;
      param.integer_value=HistoryDealGetInteger(ticket,DEAL_POSITION_ID);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Тип сделки (столбец 5)
      param.type=TYPE_STRING;
      ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
      param.integer_value=deal_type;
      
      string type="";
      switch(deal_type)
        {
         case DEAL_TYPE_BUY                     :  type="Buy";                      break;
         case DEAL_TYPE_SELL                    :  type="Sell";                     break;
         case DEAL_TYPE_BALANCE                 :  type="Balance";                  break;
         case DEAL_TYPE_CREDIT                  :  type="Credit";                   break;
         case DEAL_TYPE_CHARGE                  :  type="Charge";                   break;
         case DEAL_TYPE_CORRECTION              :  type="Correction";               break;
         case DEAL_TYPE_BONUS                   :  type="Bonus";                    break;
         case DEAL_TYPE_COMMISSION              :  type="Commission";               break;
         case DEAL_TYPE_COMMISSION_DAILY        :  type="Commission daily";         break;
         case DEAL_TYPE_COMMISSION_MONTHLY      :  type="Commission monthly";       break;
         case DEAL_TYPE_COMMISSION_AGENT_DAILY  :  type="Commission agent daily";   break;
         case DEAL_TYPE_COMMISSION_AGENT_MONTHLY:  type="Commission agent monthly"; break;
         case DEAL_TYPE_INTEREST                :  type="Interest";                 break;
         case DEAL_TYPE_BUY_CANCELED            :  type="Buy canceled";             break;
         case DEAL_TYPE_SELL_CANCELED           :  type="Sell canceled";            break;
         case DEAL_DIVIDEND                     :  type="Dividend";                 break;
         case DEAL_DIVIDEND_FRANKED             :  type="Dividend franked";         break;
         case DEAL_TAX                          :  type="Tax";                      break;
         default                                :  break;
        }
      param.string_value=type;
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Направление сделки (столбец 6)
      param.type=TYPE_STRING;
      ENUM_DEAL_ENTRY deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);
      param.integer_value=deal_entry;
      
      string entry="";
      switch(deal_entry)
        {
         case DEAL_ENTRY_IN      :  entry="In";    break;
         case DEAL_ENTRY_OUT     :  entry="Out";   break;
         case DEAL_ENTRY_INOUT   :  entry="InOut"; break;
         case DEAL_ENTRY_OUT_BY  :  entry="OutBy"; break;
         default                 :  break;
        }
      param.string_value=entry;
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Объем сделки (столбец 7)
      param.type=TYPE_DOUBLE;
      param.double_value=HistoryDealGetDouble(ticket,DEAL_VOLUME);
      param.integer_value=2;
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Цена сделки (столбец 8)
      param.type=TYPE_DOUBLE;
      param.double_value=HistoryDealGetDouble(ticket,DEAL_PRICE);
      param.integer_value=(param.double_value>0 ? digits : 1);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Уровень Stop Loss (столбец 9)
      param.type=TYPE_DOUBLE;
      param.double_value=HistoryDealGetDouble(ticket,DEAL_SL);
      param.integer_value=(param.double_value>0 ? digits : 1);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Уровень Take Profit (столбец 10)
      param.type=TYPE_DOUBLE;
      param.double_value=HistoryDealGetDouble(ticket,DEAL_TP);
      param.integer_value=(param.double_value>0 ? digits : 1);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Финансовый результат сделки (столбец 11)
      param.type=TYPE_DOUBLE;
      param.double_value=HistoryDealGetDouble(ticket,DEAL_PROFIT);
      param.integer_value=(param.double_value!=0 ? 2 : 1);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Magic number для сделки (столбец 12)
      param.type=TYPE_LONG;
      param.integer_value=HistoryDealGetInteger(ticket,DEAL_MAGIC);
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Причина или источник проведения сделки (столбец 13)
      param.type=TYPE_STRING;
      ENUM_DEAL_REASON deal_reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON);
      param.integer_value=deal_reason;
      
      string reason="";
      switch(deal_reason)
        {
         case DEAL_REASON_CLIENT          :  reason="Client";           break;
         case DEAL_REASON_MOBILE          :  reason="Mobile";           break;
         case DEAL_REASON_WEB             :  reason="Web";              break;
         case DEAL_REASON_EXPERT          :  reason="Expert";           break;
         case DEAL_REASON_SL              :  reason="SL";               break;
         case DEAL_REASON_TP              :  reason="TP";               break;
         case DEAL_REASON_SO              :  reason="StopOut";          break;
         case DEAL_REASON_ROLLOVER        :  reason="Rollover";         break;
         case DEAL_REASON_VMARGIN         :  reason="VMargin";          break;
         case DEAL_REASON_SPLIT           :  reason="Split";            break;
         case DEAL_REASON_CORPORATE_ACTION:  reason="Corporate action"; break;
         default                          :  break;
        }
      param.string_value=reason;
      DataListCreator::AddNewCellParamToRow(row,param);
      
      //--- Комментарий к сделке (столбец 14)
      param.type=TYPE_STRING;
      param.string_value=HistoryDealGetString(ticket,DEAL_COMMENT);
      DataListCreator::AddNewCellParamToRow(row,param);
     }
   
//--- Объявляем и инициализируем заголовок таблицы
   string headers[]={"Time","Symbol","Ticket","Order","Position","Type","Entry","Volume","Price","SL","TP","Profit","Magic","Reason","Comment"};
   
//--- Создаём таблицу на основе созданного списка параметров и массива заголовков
   CTableByParam *table=new CTableByParam(rows_data,headers);
   if(table==NULL)
      return;
//--- Распечатываем таблицу в журнале и удаляем созданный объект
   table.Print();
   delete table;
  }

As a result, a table of all trades with a cell width of 19 characters will be printed out (by default in the Print method of the table class):

Table By Param: Rows total: 797, Columns total: 15:
|                n/n |               Time |             Symbol |             Ticket |              Order |           Position |               Type |              Entry |             Volume |              Price |                 SL |                 TP |             Profit |              Magic |             Reason |            Comment |
| 0                  |2025.01.01 10:20:10 |                    |         3152565660 |                  0 |                  0 |            Balance |                 In |               0.00 |                0.0 |                0.0 |                0.0 |          100000.00 |                  0 |             Client |                    |
| 1                  |2025.01.02 00:01:31 |             GBPAUD |         3152603334 |         3191672408 |         3191672408 |               Sell |                 In |               0.25 |            2.02111 |                0.0 |                0.0 |                0.0 |                112 |             Expert |                    |
| 2                  |2025.01.02 02:50:31 |             GBPAUD |         3152749152 |         3191820118 |         3191672408 |                Buy |                Out |               0.25 |            2.02001 |                0.0 |            2.02001 |              17.04 |                112 |                 TP |       [tp 2.02001] |
| 3                  |2025.01.02 04:43:43 |             GBPUSD |         3152949278 |         3191671491 |         3191671491 |               Sell |                 In |               0.10 |            1.25270 |                0.0 |            1.24970 |                0.0 |                 12 |             Expert |                    |
...
...

| 793                |2025.04.18 03:22:11 |             EURCAD |         3602552747 |         3652159095 |         3652048415 |               Sell |                Out |               0.25 |            1.57503 |                0.0 |            1.57503 |              12.64 |                112 |                 TP |       [tp 1.57503] |
| 794                |2025.04.18 04:06:52 |             GBPAUD |         3602588574 |         3652200103 |         3645122489 |               Sell |                Out |               0.25 |            2.07977 |                0.0 |            2.07977 |               3.35 |                112 |                 TP |       [tp 2.07977] |
| 795                |2025.04.18 04:06:52 |             GBPAUD |         3602588575 |         3652200104 |         3652048983 |               Sell |                Out |               0.25 |            2.07977 |                0.0 |            2.07977 |              12.93 |                112 |                 TP |       [tp 2.07977] |
| 796                |2025.04.18 05:57:48 |             AUDJPY |         3602664574 |         3652277665 |         3652048316 |                Buy |                Out |               0.25 |             90.672 |                0.0 |             90.672 |              19.15 |                112 |                 TP |        [tp 90.672] |

The example here shows the first and last four trades, but it gives an idea of the table being printed out in the log.

All created files are attached to the article for self-study. The archive file can be unzipped to the terminal folder, and all files will be located in the desired folder: MQL5\Scripts\Tables.


Conclusion

So, we have completed work on the basic components of the table model (Model) within the framework of MVC architecture. We created classes for working with tables and headers, and also tested them on different types of data: two-dimensional arrays, matrices, and trade history.

We now proceed to the next phase — developing the View and Controller components. In MQL5, these two components are closely linked due to the built-in event system that allows objects to react to user actions.

This gives us an opportunity to develop table visualization (View component) and its control (Controller component) at the same time. This will slightly simplify the rather complex and multi-level implementation of View component.

All examples and files from the article are available for download. In future articles, we will create View component combined with Controller to implement a full-fledged tool for operating tables in MQL5.

Upon completion of the project, new opportunities will open the door to creating other UI controls for use in our developments.


Programs used in the article:

#
Name
Type
Description
 1  Tables.mqh Class Library Class Library for table creation
 2  TestEmptyTable.mq5 Script A script for testing the creation of an empty table with a set number of rows and columns
 3  TestTArrayTable.mq5 Script A script for testing the creation of a table based on a two-dimensional data array
 4  TestMatrixTable.mq5 Script A script for testing the creation of a table based on a data matrix
 5  TestDealsTable.mq5 Script A script for testing the creation of a table based on user data (historical trades)
 6  MQL5.zip Archive An archive of the files presented above for unpacking into the MQL5 directory of the client terminal

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/17803

Attached files |
Tables.mqh (261.24 KB)
TestEmptyTable.mq5 (3.22 KB)
TestDealsTable.mq5 (24.63 KB)
MQL5.zip (30.86 KB)
Overcoming The Limitation of Machine Learning (Part 8): Nonparametric Strategy Selection Overcoming The Limitation of Machine Learning (Part 8): Nonparametric Strategy Selection
This article shows how to configure a black-box model to automatically uncover strong trading strategies using a data-driven approach. By using Mutual Information to prioritize the most learnable signals, we can build smarter and more adaptive models that outperform conventional methods. Readers will also learn to avoid common pitfalls like overreliance on surface-level metrics, and instead develop strategies rooted in meaningful statistical insight.
Developing a Trading Strategy: The Flower Volatility Index Trend-Following Approach Developing a Trading Strategy: The Flower Volatility Index Trend-Following Approach
The relentless quest to decode market rhythms has led traders and quantitative analysts to develop countless mathematical models. This article has introduced the Flower Volatility Index (FVI), a novel approach that transforms the mathematical elegance of Rose Curves into a functional trading tool. Through this work, we have shown how mathematical models can be adapted into practical trading mechanisms capable of supporting both analysis and decision-making in real market conditions.
Mastering Kagi Charts in MQL5 (Part I): Creating the Indicator Mastering Kagi Charts in MQL5 (Part I): Creating the Indicator
Learn how to build a complete Kagi Chart engine in MQL5—constructing price reversals, generating dynamic line segments, and updating Kagi structures in real time. This first part teaches you how to render Kagi charts directly on MetaTrader 5, giving traders a clear view of trend shifts and market strength while preparing for automated Kagi-based trading logic in Part 2.
Introduction to MQL5 (Part 28): Mastering API and WebRequest Function in MQL5 (II) Introduction to MQL5 (Part 28): Mastering API and WebRequest Function in MQL5 (II)
This article teaches you how to retrieve and extract price data from external platforms using APIs and the WebRequest function in MQL5. You’ll learn how URLs are structured, how API responses are formatted, how to convert server data into readable strings, and how to identify and extract specific values from JSON responses.