preview
Реализация модели таблицы в MQL5: Применение концепции MVC

Реализация модели таблицы в MQL5: Применение концепции MVC

MetaTrader 5Примеры | 4 апреля 2025, 11:05
500 9
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

В программировании архитектура приложения играет ключевую роль в обеспечении надёжности, масштабируемости и удобстве поддержки. Одним из подходов, который помогает достичь таких целей, является использование архитектурного шаблона MVC (Model-View-Controller).

Концепция MVC позволяет разделить приложение на три взаимосвязанных компонента: модель (управление данными и логикой), представление (отображение данных) и контроллер (обработка действий пользователя). Такое разделение упрощает разработку, тестирование и поддержку кода, делая его более структурированным и гибким.

В данной статье мы рассмотрим, как применить принципы MVC для реализации модели таблицы на языке MQL5. Таблицы являются важным инструментом для хранения, обработки и отображения данных, и их правильная организация может значительно упростить работу с информацией. Мы создадим классы для работы с таблицами: ячейки таблицы, строки и модель таблицы. Для хранения ячеек в строках и строк в модели таблицы будем использовать связанные списки Стандартной Библиотеки MQL5, которые позволяют эффективно хранить и использовать данные.


Немного о концепции MVC: что это и зачем нужно

Представьте себе приложение как театр. Есть сценарий — он описывает, что должно происходить (это модель). Есть сцена — то, что видит зритель (это представление). И наконец, есть режиссёр, который управляет всем процессом и соединяет остальные элементы (это контроллер). Так работает архитектурный шаблон MVC — Model-View-Controller.

Эта концепция помогает разделить обязанности внутри приложения. Модель отвечает за данные и логику, представление — за отображение и внешний вид, а контроллер — за обработку действий пользователя. Такое разделение делает код более понятным, гибким и удобным для командной работы.

Допустим, вы создаёте таблицу. Модель знает, какие строки и ячейки в ней есть, и умеет их изменять. Представление рисует таблицу на экране. А контроллер реагирует, когда пользователь нажимает "Добавить строку" — и передаёт задачу модели, а затем, говорит представлению обновиться.

MVC особенно полезен, когда приложение становится сложнее: добавляются новые функции, меняется интерфейс, работают несколько разработчиков. Благодаря чёткой архитектуре, проще вносить изменения, тестировать компоненты по отдельности и переиспользовать код.

Конечно, у этой схемы есть и свои минусы. Для совсем простых проектов MVC может оказаться избыточным — придётся делить даже то, что можно было бы уместить в пару функций. Но в масштабируемых, серьёзных приложениях эта структура быстро оправдывает себя.

Исходя из вышеперечисленного, делаем резюме:

MVC — это мощный архитектурный шаблон, который помогает организовать код, сделать его более понятным, тестируемым и масштабируемым. Он особенно полезен для сложных приложений, где важно разделение логики данных, пользовательского интерфейса и управления. Для небольших проектов его использование избыточно.

Видим, что парадигма Model-View-Controller очень хорошо подходит под нашу задачу. Таблица будет создана из самостоятельных объектов:

  • Ячейка таблицы.
    Объект, хранящий значение одного из типов — вещественное, целочисленное или строковое, наделён инструментами управления значением, его установкой и получением;
  • Строка таблицы.
    Объект, хранящий список объектов ячеек таблицы, наделён инструментами управления ячейками, их расположением, добавлением и удалением;
  • Модель таблицы.
    Объект, хранящий список объектов строк таблицы, наделён инструментами управления строками и столбцами таблицы, их расположением, добавлением и удалением, а также имеет доступ к элементам управления строк и ячеек.

На рисунке ниже схематично изображена структура модели таблицы размером 4x4:

Рис.1 Модель таблицы 4x4

Давайте перейдём от теории к реализации.


Пишем классы для построения модели таблицы

Для создания всех объектов будем использовать Стандартную Библиотеку MQL5.

Каждый объект будет являться наследником базового класса библиотеки. Это позволит хранить эти объекты в списках объектов.

Все классы будем писать в одном файле тестового скрипта — чтобы всё было в одном файле, на виду и в быстром доступе. В последующем распределим написанные классы по своим отдельным подключаемым файлам.


1. Связанные списки как основа хранения табличных данных

Для хранения табличных данных очень хорошо подходит связанный список CList. В нём, в отличие от похожего списка CArrayObj, реализованы методы доступа к соседним объектам списка, расположенным слева и справа от текущего. Это позволит легко реализовать перемещение ячеек в строке, либо перемещение строк в таблице, их добавление и удаление. При этом, сам список позаботится о правильной индексации перемещённых, добавленных или удалённых объектов в списке.

Но здесь есть один нюанс. Если обратиться к методам загрузки и сохранения списка в файл, то можно увидеть, что при загрузке из файла, класс списка должен создать новый объект в виртуальном методе CreateElement().

Этот метод в этом классе просто возвращает NULL:

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

А это значит, что для работы со связанными списками, и при условии, что нам необходимы будут файловые операции, мы должны унаследоваться от класса CList, и в своём классе реализовать этот метод.

Если же посмотреть методы сохранения объектов Стандартной Библиотеки в файл, то можно заметить такой алгоритм сохранения свойств объекта:

  1. В файл записывается маркер начала данных (-1),
  2. В файл записывается тип объекта,
  3. В файл поочерёдно записываются все свойства объекта.

Первый и второй пункты присущи всем реализованным методам сохранения/загрузки, которые есть у объектов Стандартной Библиотеки. Соответственно, следуя той же логике, нам нужно знать тип объекта, сохранённого в списке, чтобы при чтении из файла создать объект с таким типом в виртуальном методе CreateElement() класса списка, унаследованного от CList.

Плюс, все объекты, которые можно загрузить в список — их классы, должны быть продекларированы, либо созданы до реализации класса списка. В этом случае, список будет "знать" о каких объектах "идёт речь", и которые нужно создать.

В каталоге терминала \MQL5\Scripts\ создадим новую папку TableModel\, а в ней — новый файл тестового скрипта TableModelTest.mq5.

Подключим файл связанного списка и продекларируем будущие классы модели таблицы:

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

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

//--- Форвард-декларация классов
class CTableCell;                   // Класс ячейки таблицы
class CTableRow;                    // Класс строки таблицы
class CTableModel;                  // Класс модели таблицы

Форвард-декларация будущих классов здесь необходима для того, чтобы класс связанного списка, который будет унаследован от класса CList знал об этих типах классов, равно, как и знал о типах объектов, которые ему придётся создавать. Для этого напишем перечисление типов объектов, вспомогательные макросы и перечисление способов сортировки списков:

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

//--- Форвард-декларация классов
class CTableCell;                   // Класс ячейки таблицы
class CTableRow;                    // Класс строки таблицы
class CTableModel;                  // Класс модели таблицы

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

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum ENUM_OBJECT_TYPE               // Перечисление типов объектов
  {
   OBJECT_TYPE_TABLE_CELL=10000,    // Ячейка таблицы
   OBJECT_TYPE_TABLE_ROW,           // Строка таблицы
   OBJECT_TYPE_TABLE_MODEL,         // Модель таблицы
  };
  
enum ENUM_CELL_COMPARE_MODE         // Режимы сравнения ячеек таблицы
  {
   CELL_COMPARE_MODE_COL,           // Сравнение по номеру колонки
   CELL_COMPARE_MODE_ROW,           // Сравнение по номеру строки
   CELL_COMPARE_MODE_ROW_COL,       // Сравнение по строке и колонке
  };
  
//+------------------------------------------------------------------+
//| Функции                                                          |
//+------------------------------------------------------------------+
//--- Возвращает тип объекта как строку
string TypeDescription(const ENUM_OBJECT_TYPE type)
  {
   string array[];
   int total=StringSplit(EnumToString(type),StringGetCharacter("_",0),array);
   string result="";
   for(int i=2;i<total;i++)
     {
      array[i]+=" ";
      array[i].Lower();
      array[i].SetChar(0,ushort(array[i].GetChar(0)-0x20));
      result+=array[i];
     }
   result.TrimLeft();
   result.TrimRight();
   return result;
  }
//+------------------------------------------------------------------+
//| Классы                                                           |
//+------------------------------------------------------------------+

Функция, возвращающая описание типа объекта, построена на допущении, что все наименования констант типов объектов начинаются с подстроки "OBJECT_TYPE_". Тогда можно взять подстроку, следующую за этой, перевести все символы полученной строки в нижний регистр, первый символ перевести в верхний регистр, и у итоговой строки слева и справа очистить все пробелы и управляющие символы.

Напишем собственный класс связанного списка. Продолжим писать код далее в этом же файле:

//+------------------------------------------------------------------+
//| Классы                                                           |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Класс связанного списка объектов                                 |
//+------------------------------------------------------------------+
class CListObj : public CList
  {
protected:
   ENUM_OBJECT_TYPE  m_element_type;   // Тип создаваемого объекта в CreateElement()
public:
//--- Виртуальный метод (1) загрузки списка из файла, (2) создания элемента списка
   virtual bool      Load(const int file_handle);
   virtual CObject  *CreateElement(void);
  };

Класс CListObj — это наш новый класс связанного списка, унаследованный от класса Стандартной Библиотеки CList.

Единственной переменной в классе будет переменная, в которую будет записываться тип создаваемого объекта. Так как метод CreateElement() виртуальный, и должен иметь точно такую же сигнатуру, как у метода родительского класса, то мы не можем в него передать тип создаваемого объекта. Но можем записать этот тип в объявленную переменную, и с неё считывать тип создаваемого объекта.

Нам необходимо переопределить два виртуальных метода родительского класса: метод загрузки из файла и метод создания нового объекта. Рассмотрим их.

Метод, загружающий список из файла:

//+------------------------------------------------------------------+
//| Загрузка списка из файла                                         |
//+------------------------------------------------------------------+
bool CListObj::Load(const int file_handle)
  {
//--- Переменные
   CObject *node;
   bool     result=true;
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Загрузка и проверка маркера начала списка - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      return(false);
//--- Загрузка и проверка типа списка
   if(::FileReadInteger(file_handle,INT_VALUE)!=Type())
      return(false);
//--- Чтение размера списка (количество объектов)
   uint num=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Последовательно заново создаём элементы списка с помощью вызова метода Load() объектов node
   this.Clear();
   for(uint i=0; i<num; i++)
     {
      //--- Читаем и проверяем маркер начала данных объекта - 0xFFFFFFFFFFFFFFFF
      if(::FileReadLong(file_handle)!=MARKER_START_DATA)
         return false;
      //--- Читаем тип объекта
      this.m_element_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE);
      node=this.CreateElement();
      if(node==NULL)
         return false;
      this.Add(node);
      //--- Сейчас файловый указатель смещён относительно начала маркера объекта на 12 байт (8 - маркер, 4 - тип)
      //--- Поставим указатель на начало данных объекта и загрузим свойства объекта из файла методом Load() элемента node.
      if(!::FileSeek(file_handle,-12,SEEK_CUR))
         return false;
      result &=node.Load(file_handle);
     }
//--- Результат
   return result;
  }

Здесь сначала контролируется начало списка, его тип и размер — количество элементов в списке, а затем, в цикле по количеству элементов читаются из файла маркеры начала данных каждого объекта и его тип. Полученный тип записывается в переменную m_element_type и вызывается метод создания нового элемента. В этом методе создаётся новый элемент с полученным типом и записывается в переменную-указатель node, которая, в свою очередь, добавляется к списку. Вся логика метода подробно расписана в комментариях. Рассмотрим метод создания нового элемента списка.

Метод, создающий элемент списка:

//+------------------------------------------------------------------+
//| Метод создания элемента списка                                   |
//+------------------------------------------------------------------+
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();
      default                       :  return NULL;
     }
  }

Здесь подразумевается, что перед вызовом метода, в переменной m_element_type уже записан тип создаваемого объекта. В зависимости от типа элемента, создаётся новый объект соответствующего типа, и возвращается указатель на него. В дальнейшем, при разработке новых элементов управления, их типы будут вписываться в перечисление ENUM_OBJECT_TYPE, а здесь будут добавляться новые кейсы для создания новых типов объектов. Класс связанного списка на основе стандартного CList готов. Теперь он может хранить в себе все объекты известных типов, сохранять списки в файл и загружать их из файла и правильно восстанавливать.


2. Класс ячейки таблицы

Ячейка таблицы — это простейший элемент таблицы, хранящий в себе некое значение. Ячейки складываются в списки, представляющие собой строки таблицы. Один список — одна строка таблицы. В нашей таблице ячейки смогут хранить в себе одновременно только одно значение из нескольких типов — вещественное, целочисленное, или строковое значение.

Помимо простого значения, ячейке можно назначить один объект известного типа из перечисления ENUM_OBJECT_TYPE. В этом случае, ячейка может хранить значение какого-либо из перечисленных типов, плюс указатель на объект, тип которого записывается в специальную переменную. Таким образом, в последующем, компоненту View (представлению) можно указать отображать в ячейке такой объект для взаимодействия с ним при помощи компонента Controller.

Так как в одной ячейке можно хранить несколько разных типов значений, то для их записи, хранения и возврата будем использовать объединение (union) — особый тип данных, который состоит из нескольких переменных, разделяющих одну и ту же область памяти. Объединение похоже на структуру, но здесь, в отличие от структуры, разные члены объединения относятся к одному и тому же участку памяти. В структуре же, для каждого поля выделяется собственный участок памяти.

Продолжим писать код в уже созданном файле. Начнём писать новый класс. В защищённой секции напишем объединение и объявим переменные:

//+------------------------------------------------------------------+
//| Класс ячейки таблицы                                             |
//+------------------------------------------------------------------+
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;                            // Флаг редактируемой ячейки
   
public:

В публичной секции напишем методы доступа к защищённым переменным, виртуальные методы и конструкторы класса для различных типов хранимых в ячейке данных:

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(::DoubleToString(this.ValueD(),this.Digits()));
                           case TYPE_LONG    :  return(::IntegerToString(this.ValueL()));
                           case TYPE_DATETIME:  return(::TimeToString(this.ValueL(),this.m_time_flags));
                           case TYPE_COLOR   :  return(::ColorToString((color)this.ValueL(),this.m_color_flag));
                           default           :  return this.ValueS();
                          }
                       }
   string            DatatypeDescription(void) const
                       {
                        string type=::StringSubstr(::EnumToString(this.m_datatype),5);
                        type.Lower();
                        return type;
                       }
//--- Установка значений переменных
   void              SetRow(const uint row)                    { this.m_row=(int)row;                          }
   void              SetCol(const uint col)                    { this.m_col=(int)col;                          }
   void              SetDatatype(const ENUM_DATATYPE datatype) { this.m_datatype=datatype;                     }
   void              SetDigits(const int digits)               { this.m_digits=digits;                         }
   void              SetDatetimeFlags(const uint flags)        { this.m_time_flags=flags;                      }
   void              SetColorNameFlag(const bool flag)         { this.m_color_flag=flag;                       }
   void              SetEditable(const bool flag)              { this.m_editable=flag;                         }
   void              SetPositionInTable(const uint row,const uint col)
                       {
                        this.SetRow(row);
                        this.SetCol(col);
                       }
//--- Назначает объект в ячейку
   void              AssignObject(CObject *object)
                       {
                        if(object==NULL)
                          {
                           ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
                           return;
                          }
                        this.m_object=object;
                        this.m_object_type=(ENUM_OBJECT_TYPE)object.Type();
                       }
//--- Снимает назначение объекта
   void              UnassignObject(void)
                       {
                        this.m_object=NULL;
                        this.m_object_type=-1;
                       }
                       
//--- Устанавливает double-значение
   void              SetValue(const double value)
                       {
                        this.m_datatype=TYPE_DOUBLE;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueD(value);
                       }
//--- Устанавливает long-значение
   void              SetValue(const long value)
                       {
                        this.m_datatype=TYPE_LONG;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueL(value);
                       }
//--- Устанавливает datetime-значение
   void              SetValue(const datetime value)
                       {
                        this.m_datatype=TYPE_DATETIME;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueL(value);
                       }
//--- Устанавливает color-значение
   void              SetValue(const color value)
                       {
                        this.m_datatype=TYPE_COLOR;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueL(value);
                       }
//--- Устанавливает string-значение
   void              SetValue(const string value)
                       {
                        this.m_datatype=TYPE_STRING;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueS(value);
                       }
//--- Очищает данные
   void              ClearData(void)
                       {
                        if(this.Datatype()==TYPE_STRING)
                           this.SetValue("");
                        else
                           this.SetValue(0.0);
                       }
//--- (1) Возвращает, (2) выводит в журнал описание объекта
   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_TABLE_CELL);}
   
   
//--- Конструкторы/деструктор
                     CTableCell(void) : m_row(0), m_col(0), m_datatype(-1), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueD(0);
                       }
                     //--- Принимает double-значение
                     CTableCell(const uint row,const uint col,const double value,const int digits) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueD(value);
                       }
                     //--- Принимает long-значение
                     CTableCell(const uint row,const uint col,const long value) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_LONG), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueL(value);
                       }
                     //--- Принимает datetime-значение
                     CTableCell(const uint row,const uint col,const datetime value,const uint time_flags) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_DATETIME), m_digits(0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueL(value);
                       }
                     //--- Принимает color-значение
                     CTableCell(const uint row,const uint col,const color value,const bool color_names_flag) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_COLOR), m_digits(0), m_time_flags(0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueL(value);
                       }
                     //--- Принимает string-значение
                     CTableCell(const uint row,const uint col,const string value) :
                        m_row((int)row), m_col((int)col), m_datatype(TYPE_STRING), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
                       {
                        this.m_datatype_value.SetValueS(value);
                       }
                    ~CTableCell(void) {}
  };

В методах для установки значений сделано так, что сначала устанавливается тип значения, хранимого в ячейке, а затем, проверяется флаг возможности редактировать значения в ячейке, и только при установленном флаге новое значение сохраняется в ячейке:

//--- Устанавливает double-значение
   void              SetValue(const double value)
                       {
                        this.m_datatype=TYPE_DOUBLE;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueD(value);
                       }

Для чего сделано именно так? При создании новой ячейки, она создаётся с вещественным типом хранимого значения. Если нужно изменить тип значения, но при этом ячейка не редактируемая, то можно вызвать метод установки значения нужного типа с любым значением. Тип хранимого значения будет изменён, но само значение в ячейке затронуто не будет.

Метод очистки данных выставляет цифровые значения в ноль, а в строковых вписывает пробел:

//--- Очищает данные
   void              ClearData(void)
                       {
                        if(this.Datatype()==TYPE_STRING)
                           this.SetValue("");
                        else
                           this.SetValue(0.0);
                       }

Позже, сделаем иначе — чтобы в очищенных ячейках не выводились никакие данные — ячейка была пустой, сделаем "пустое" значение для ячейки, и тогда, при очищении ячейки, все записанные и отображаемые в ней значения будут стёрты. Ведь ноль — это тоже полноценное значение, а сейчас при очищении ячейки цифровые данные заполняются нулями. Это неправильно.

В параметрических конструкторах класса передаются координаты ячейки в таблице — номер строки и столбца и значение требуемого типа (double, long, datetime, color, string). Для некоторых типов значений требуется дополнительная информация:

  • double— точность выводимого значения (количество знаков после запятой),
  • datetime— флаги вывода времени (дата/часы-минуты/секунды),
  • color— флаг отображения наименований известных стандартных цветов.

В конструкторах с этими типами хранимых в ячейках значений передаются дополнительные параметры для установки формата выводимых в ячейках значений:

 //--- Принимает double-значение
 CTableCell(const uint row,const uint col,const double value,const int digits) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueD(value);
   }
 //--- Принимает long-значение
 CTableCell(const uint row,const uint col,const long value) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_LONG), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueL(value);
   }
 //--- Принимает datetime-значение
 CTableCell(const uint row,const uint col,const datetime value,const uint time_flags) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_DATETIME), m_digits(0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueL(value);
   }

 //--- Принимает color-значение
 CTableCell(const uint row,const uint col,const color value,const bool color_names_flag) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_COLOR), m_digits(0), m_time_flags(0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueL(value);
   }
 //--- Принимает string-значение
 CTableCell(const uint row,const uint col,const string value) :
    m_row((int)row), m_col((int)col), m_datatype(TYPE_STRING), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1)
   {
    this.m_datatype_value.SetValueS(value);
   }

Метод сравнения двух объектов:

//+------------------------------------------------------------------+
//| Сравнение двух объектов                                          |
//+------------------------------------------------------------------+
int CTableCell::Compare(const CObject *node,const int mode=0) const
  {
   const CTableCell *obj=node;
   switch(mode)
     {
      case CELL_COMPARE_MODE_COL :  return(this.Col()>obj.Col() ? 1 : this.Col()<obj.Col() ? -1 : 0);
      case CELL_COMPARE_MODE_ROW :  return(this.Row()>obj.Row() ? 1 : this.Row()<obj.Row() ? -1 : 0);
      //---CELL_COMPARE_MODE_ROW_COL
      default                    :  return
                                      (
                                       this.Row()>obj.Row() ? 1 : this.Row()<obj.Row() ? -1 :
                                       this.Col()>obj.Col() ? 1 : this.Col()<obj.Col() ? -1 : 0
                                      );
     }
  }

Метод позволяет сравнить параметры двух объектов по одному из трёх критериев сравнения — по номеру колонки, по номеру строки, одновременно по номерам строки и колонки.

Метод необходим для возможности сортировки строк таблицы по значениям колонок таблицы. Этим будет заниматься компонент Controller в последующих статьях.

Метод для сохранения свойств ячейки в файл:

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
bool CTableCell::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_datatype,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем тип объекта в ячейке
   if(::FileWriteInteger(file_handle,this.m_object_type,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем номер строки
   if(::FileWriteInteger(file_handle,this.m_row,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем номер столбца
   if(::FileWriteInteger(file_handle,this.m_col,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем точность представления данных
   if(::FileWriteInteger(file_handle,this.m_digits,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем флаги отображения даты/времени
   if(::FileWriteInteger(file_handle,this.m_time_flags,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем флаг отображения наименования цвета
   if(::FileWriteInteger(file_handle,this.m_color_flag,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем флаг редактируемой ячейки
   if(::FileWriteInteger(file_handle,this.m_editable,INT_VALUE)!=INT_VALUE)
      return(false);
   //--- Сохраняем значение
   if(::FileWriteStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value))
      return(false);
   
//--- Всё успешно
   return true;
  }

После записи стартового маркера данных и типа объекта в файл, поочерёдно сохраняются все свойства ячейки. Объединение необходимо сохранять как структуру функцией FileWriteStruct().

Метод для загрузки свойств ячейки из файла:

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
bool CTableCell::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_datatype=(ENUM_DATATYPE)::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем тип объекта в ячейке
   this.m_object_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем номер строки
   this.m_row=::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем номер столбца
   this.m_col=::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем точность представления данных
   this.m_digits=::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем флаги отображения даты/времени
   this.m_time_flags=::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем флаг отображения наименования цвета
   this.m_color_flag=::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем флаг редактируемой ячейки
   this.m_editable=::FileReadInteger(file_handle,INT_VALUE);
   //--- Загружаем значение
   if(::FileReadStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value))
      return(false);
   
//--- Всё успешно
   return true;
  }

После чтения и проверки маркера начала данных и типа объекта, поочерёдно загружаются все свойства объекта в том же порядке, в котором они были сохранены. Объединение считывается при помощи FileReadStruct().

Метод, возвращающий описание объекта:

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
string CTableCell::Description(void)
  {
   return(::StringFormat("%s: Row %u, Col %u, %s <%s>Value: %s",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Row(),this.Col(),
                         (this.m_editable ? "Editable" : "Uneditable"),this.DatatypeDescription(),this.Value()));
  }

Здесь создаётся строка из некоторых параметров ячейки и возвращается, например для double, в таком формате:

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

Метод, выводящий в журнал описание объекта:

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

Здесь просто распечатывается описание объекта в журнал.

//+------------------------------------------------------------------+
//| Класс строки таблицы                                             |
//+------------------------------------------------------------------+
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       *CreateNewCell(const double value);
   CTableCell       *CreateNewCell(const long value);
   CTableCell       *CreateNewCell(const datetime value);
   CTableCell       *CreateNewCell(const color value);
   CTableCell       *CreateNewCell(const string value);
   
//--- Возвращает (1) ячейку по индексу, (2) количество ячеек
   CTableCell       *GetCell(const uint index)                 { return this.m_list_cells.GetNodeAtIndex(index);  }
   uint              CellsTotal(void)                    const { return this.m_list_cells.Total();                }
   
//--- Устанавливает значение в указанную ячейку
   void              CellSetValue(const uint index,const double value);
   void              CellSetValue(const uint index,const long value);
   void              CellSetValue(const uint index,const datetime value);
   void              CellSetValue(const uint index,const color value);
   void              CellSetValue(const uint index,const string value);
//--- (1) назначает в ячейку, (2) снимает с ячейки назначенный объект
   void              CellAssignObject(const uint index,CObject *object);
   void              CellUnassignObject(const uint index);
   
//--- (1) Удаляет (2) перемещает ячейку
   bool              CellDelete(const uint index);
   bool              CellMoveTo(const uint cell_index, const uint index_to);
   
//--- Обнуляет данные ячеек строки
   void              ClearData(void);

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

//--- Виртуальные методы (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_ROW); }
   
//--- Конструкторы/деструктор
                     CTableRow(void) : m_index(0) {}
                     CTableRow(const uint index) : m_index(index) {}
                    ~CTableRow(void){}
  };


3. Класс строки таблицы

Строка таблицы — это, по сути, обычный список ячеек. Класс строки должен уметь добавлять новые ячейки, удалять и перемещать на новое расположение имеющиеся в списке ячейки. Класс должен иметь минимально-достаточный набор методов для управления ячейками в списке.

Продолжим писать код в том же самом файле. В параметрах класса всего одна переменная — индекс строки в таблице. Всё остальное — методы для работы со свойствами строки и со списком её ячеек.

Рассмотрим методы класса.

Метод для сравнения двух строк таблицы:

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

Так как строки можно сравнивать только по их единственному параметру — индексу строки, то здесь это сравнение и реализовано. Метод потребуется для сортировки строк таблицы.

Перегруженные методы для создания ячеек с разными типами хранимых данных:

//+------------------------------------------------------------------+
//| Создаёт новую double-ячейку и добавляет в конец списка           |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const double value)
  {
//--- Создаём новый объект ячейки, хранящей значение с типом double
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,2);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Добавляем созданную ячейку в конец списка
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Возвращаем указатель на объект
   return cell;
  }
//+------------------------------------------------------------------+
//| Создаёт новую long-ячейку и добавляет в конец списка             |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const long value)
  {
//--- Создаём новый объект ячейки, хранящей значение с типом long
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Добавляем созданную ячейку в конец списка
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Возвращаем указатель на объект
   return cell;
  }
//+------------------------------------------------------------------+
//| Создаёт новую datetime-ячейку и добавляет в конец списка         |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const datetime value)
  {
//--- Создаём новый объект ячейки, хранящей значение с типом datetime
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Добавляем созданную ячейку в конец списка
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Возвращаем указатель на объект
   return cell;
  }
//+------------------------------------------------------------------+
//| Создаёт новую color-ячейку и добавляет в конец списка            |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const color value)
  {
//--- Создаём новый объект ячейки, хранящей значение с типом color
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,true);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Добавляем созданную ячейку в конец списка
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Возвращаем указатель на объект
   return cell;
  }
//+------------------------------------------------------------------+
//| Создаёт новую string-ячейку и добавляет в конец списка           |
//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const string value)
  {
//--- Создаём новый объект ячейки, хранящей значение с типом string
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      return NULL;
     }
//--- Добавляем созданную ячейку в конец списка
   if(!this.AddNewCell(cell))
     {
      delete cell;
      return NULL;
     }
//--- Возвращаем указатель на объект
   return cell;
  }

Пять методов для создания новой ячейки (double, long, datetime, color, string) и добавления её в конец списка. Дополнительные параметры формата вывода данных в ячейку устанавливаются со значениями по умолчанию. Их можно будет изменить уже после создания ячейки. Сначала создаётся новый объект ячейки, и затем добавляется в конец списка. Если объект создан не был — он удаляется для избежания утечек памяти.

Метод, добавляющий ячейку в конец списка:

//+------------------------------------------------------------------+
//| Добавляет ячейку в конец списка                                  |
//+------------------------------------------------------------------+
bool CTableRow::AddNewCell(CTableCell *cell)
  {
//--- Если передан пустой объект - сообщаем и возвращаем false
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Empty CTableCell object passed",__FUNCTION__);
      return false;
     }
//--- Устанавливаем индекс ячейки в списке и добавляем созданную ячейку в конец списка
   cell.SetPositionInTable(this.m_index,this.CellsTotal());
   if(this.m_list_cells.Add(cell)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add cell (%u,%u) to list",__FUNCTION__,this.m_index,this.CellsTotal());
      return false;
     }
//--- Успешно
   return true;
  }

Любая вновь созданная ячейка всегда добавляется в конец списка. Далее, её можно будет переместить на нужное положение при помощи методов сласса модели таблицы, который будет создаваться далее.

Перегруженные методы установки значения в указанную ячейку:

//+------------------------------------------------------------------+
//| Устанавливает double-значение в указанную ячейку                 |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const double value)
  {
//--- Получаем из списка нужную ячейку и записываем в неё новое значение
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Устанавливает long-значение в указанную ячейку                   |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const long value)
  {
//--- Получаем из списка нужную ячейку и записываем в неё новое значение
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Устанавливает datetime-значение в указанную ячейку               |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const datetime value)
  {
//--- Получаем из списка нужную ячейку и записываем в неё новое значение
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Устанавливает color-значение в указанную ячейку                  |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const color value)
  {
//--- Получаем из списка нужную ячейку и записываем в неё новое значение
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
//+------------------------------------------------------------------+
//| Устанавливает string-значение в указанную ячейку                 |
//+------------------------------------------------------------------+
void CTableRow::CellSetValue(const uint index,const string value)
  {
//--- Получаем из списка нужную ячейку и записываем в неё новое значение
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }

Получаем по индексу требуемую ячейку из списка и устанавливаем для неё значение. Если ячейка нередактируемая, то метод SetValue() объекта ячейки установит для ячейки только тип устанавливаемого значения. Само значение установлено не будет.

Метод, назначающий в ячейку объект:

//+------------------------------------------------------------------+
//| Назначает в ячейку объект                                        |
//+------------------------------------------------------------------+
void CTableRow::CellAssignObject(const uint index,CObject *object)
  {
//--- Получаем из списка нужную ячейку и записываем в неё указатель на объект
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.AssignObject(object);
  }

Получаем по индексу объект ячейку и при помощи его метода AssignObject() назначаем ячейке указатель на объект.

Метод, отменяющий для ячейки назначенный объект:

//+------------------------------------------------------------------+
//| Отменяет для ячейки назначенный объект                           |
//+------------------------------------------------------------------+
void CTableRow::CellUnassignObject(const uint index)
  {
//--- Получаем из списка нужную ячейку и отменяем в ней указатель на объект и его тип
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.UnassignObject();
  }

Получаем по индексу объект ячейку и при помощи его метода UnassignObject() снимаем назначенный ячейке указатель на объект.

Метод, удаляющий ячейку:

//+------------------------------------------------------------------+
//| Удаляет ячейку                                                   |
//+------------------------------------------------------------------+
bool CTableRow::CellDelete(const uint index)
  {
//--- Удаляем ячейку в списке по индексу
   if(!this.m_list_cells.Delete(index))
      return false;
//--- Обновляем индексы для оставшихся ячеек в списке
   this.CellsPositionUpdate();
   return true;
  }

При помощи метода Delete() класса CList удаляем из списка ячейку. После того, как ячейка была удалена из списка, индексы остальных ячеек изменены. При помощи метода CellsPositionUpdate() обновляем индексы всех оставшихся в списке ячеек.

Метод, перемещающий ячейку на указанную позицию:

//+------------------------------------------------------------------+
//| Перемещает ячейку на указанную позицию                           |
//+------------------------------------------------------------------+
bool CTableRow::CellMoveTo(const uint cell_index,const uint index_to)
  {
//--- Выбираем нужную ячейку по индексу в списке, делая её текущей
   CTableCell *cell=this.GetCell(cell_index);
//--- Перемещаем текущую ячейку на указанную позицию в списке
   if(cell==NULL || !this.m_list_cells.MoveToIndex(index_to))
      return false;
//--- Обновляем индексы всех ячеек в списке
   this.CellsPositionUpdate();
   return true;
  }

Чтобы класс CList мог оперировать объектом, этот объект в списке должен быть текущим. Текущим он становится, например, при его выборе. Поэтому здесь сначала получаем объект ячейки из списка по индексу. Ячейка становится текущей, и далее, при помощи метода MoveToIndex() класса CList, перемещаем объект на требуемую позицию в списке. После успешного перемещения объекта на новую позицию, индексы остальных объектов необходимо скорректировать, что и делается при помощи метода CellsPositionUpdate().

Метод, устанавливающий позиции строки и колонки всем ячейкам списка:

//+------------------------------------------------------------------+
//| Устанавливает позиции строки и колонки всем ячейкам              |
//+------------------------------------------------------------------+
void CTableRow::CellsPositionUpdate(void)
  {
//--- В цикле по всем ячейкам в списке
   for(int i=0;i<this.m_list_cells.Total();i++)
     {
      //--- получаем очередную ячейку и устанавливаем в неё индексы строки и столбца
      CTableCell *cell=this.GetCell(i);
      if(cell!=NULL)
         cell.SetPositionInTable(this.Index(),this.m_list_cells.IndexOf(cell));
     }
  }

Класс CList позволяет найти индекс текущего объекта в списке. Для этого, объект должен быть выбран. Здесь мы в цикле проходим по всем объектам ячеек в списке, выбираем каждый и узнаём его индекс при помощи метода IndexOf() класса CList. Индекс строки и найденный индекс ячейки устанавливаются в объект ячейку при помощи его метода SetPositionInTable().

Метод, обнуляющий данные ячеек строки:

//+------------------------------------------------------------------+
//| Обнуляет данные ячеек строки                                     |
//+------------------------------------------------------------------+
void CTableRow::ClearData(void)
  {
//--- В цикле по всем ячейкам в списке
   for(uint i=0;i<this.CellsTotal();i++)
     {
      //--- получаем очередную ячейку и устанавливаем в неё пустое значение
      CTableCell *cell=this.GetCell(i);
      if(cell!=NULL)
         cell.ClearData();
     }
  }

Обнуляем в цикле каждую очередную ячейку в списке при помощи метода объекта ячейки ClearData(). Для строковых данных в ячейку записывается пустая строка, а для числовых — ноль.

Метод, возвращающий описание объекта:

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

Из свойств и данных объекта собирается строка и возвращается в, например, таком формате:

Table Row: Position 1, Cells total: 4:

Метод, выводящий в журнал описание объекта:

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

Для нетабличного вывода данных в журнал, сначала в журнал выводится заголовок в виде описания строки. Затем, если установлен флаг детализированного вывода, в цикле по списку ячеек в журнал выводятся описания каждой ячейки.

В итоге детализированный вывод строки таблицы в журнал выглядит, например так (для не табличного вида):

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

Для табличного вывода результат будет, например, таким:

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

Метод, сохраняющий строку таблицы в файл:

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
bool CTableRow::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_index,INT_VALUE)!=INT_VALUE)
      return(false);
//--- Сохраняем список ячеек
   if(!this.m_list_cells.Save(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }

Сохраняем маркер начала данных, затем тип объекта. Это стандартный заголовок каждого объекта в файле. После чего, следует запись в файл свойств объекта. Здесь сохраняется в файл индекс строки, и далее — список ячеек.

Метод, загружающий строку из файла:

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
bool CTableRow::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_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем список ячеек
   if(!this.m_list_cells.Load(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }

Здесь всё в том же порядке, что и при сохранении. Сначала загружается и проверяется маркер начала данных и тип объекта, далее загружается индекс строки и полностью весь список ячеек.


4. Класс модели таблицы

Модель таблицы в своём простом варианте представляет из себя связанный список строк, которые, в свою очередь, содержат связанные списки ячеек. Наша модель, которую создадим сегодня, будет получать на входе двумерный массив одного из пяти типов (double, long, datetime, color, string), и создавать из него виртуальную таблицу. В дальнейшем мы расширим этот класс для принятия иных аргументов для создания таблиц из других входных данных. Эта же модель будет считаться моделью по умолчанию.

Продолжим писать код в том же файле \MQL5\Scripts\TableModel\TableModelTest.mq5.

Класс модели таблицы — по сути, обычный список строк с методами управления строками, столбцами и ячейками. Напишем тело класса со всеми переменными и методами и далее рассмотрим объявленные методы класса:

//+------------------------------------------------------------------+
//| Класс модели таблицы                                             |
//+------------------------------------------------------------------+
class CTableModel : public CObject
  {
protected:
   CTableRow         m_row_tmp;                             // Объект строки для поиска в списке
   CListObj          m_list_rows;                           // Список строк таблицы
//--- Создаёт модель таблицы из двумерного массива
template<typename T>
   void              CreateTableModel(T &array[][]);
//--- Возвращает корректный тип данных
   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              RowResetData(const uint index);
//--- (1) Возвращает, (2) выводит в журнал описание строки
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Удаляет (2) перемещает столбец, (3) очищает данные столбца
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint row_index, const uint index_to);
   void              ColumnResetData(const uint index);
   
//--- (1) Возвращает, (2) выводит в журнал описание таблицы
   string            Description(void);
   void              Print(const bool detail);
   void              PrintTable(const int cell_width=10);
   
//--- (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);  }
   
//--- Конструкторы/деструктор
                     CTableModel(void){}
                     CTableModel(double &array[][])   { this.CreateTableModel(array); }
                     CTableModel(long &array[][])     { this.CreateTableModel(array); }
                     CTableModel(datetime &array[][]) { this.CreateTableModel(array); }
                     CTableModel(color &array[][])    { this.CreateTableModel(array); }
                     CTableModel(string &array[][])   { this.CreateTableModel(array); }
                    ~CTableModel(void){}
  };

В принципе, здесь лишь один объект связанного списка строк таблицы и методы для управления строками, ячейками и столбцами. И конструкторы, принимающие разные типы двумерных массивов.

В конструктор класса передаётся массив, и вызывается метод для создания модели таблицы:

//+------------------------------------------------------------------+
//| Создаёт модель таблицы из двумерного массива                     |
//+------------------------------------------------------------------+
template<typename T>
void CTableModel::CreateTableModel(T &array[][])
  {
//--- Получаем из свойств массива количество строк и столбцов таблицы
   int rows_total=::ArrayRange(array,0);
   int cols_total=::ArrayRange(array,1);
//--- В цикле по индексам строк
   for(int r=0; r<rows_total; r++)
     {
      //--- создаём новую пустую строку и добавляем её в конец списка строк
      CTableRow *row=this.CreateNewEmptyRow();
      //--- Если строка создана и добавлена в список,
      if(row!=NULL)
        {
         //--- В цикле по количеству ячеек в строке 
         //--- создаём все ячейки, добавляя каждую новую в конец списка ячеек строки
         for(int c=0; c<cols_total; c++)
            row.CreateNewCell(array[r][c]);
        }
     }
  }

Логика метода полностью расписана в комментариях. Первое измерение массива — это строки таблицы, второе измерение — это ячейки каждой строки. При создании ячеек используется тот тип данных, какого типа передан массив в метод.

Таким образом, мы можем создать несколько моделей таблиц, ячейки которых изначально хранят разные типы данных (double, long, datetime, color и string). В последствии, после создания модели таблицы, типы данных, хранимых в ячейках, можно изменить.

Метод, создающий новую пустую строку и добавляющий её в конец списка:

//+------------------------------------------------------------------+
//| Создаёт новую пустую строку и добавляет в конец списка           |
//+------------------------------------------------------------------+
CTableRow *CTableModel::CreateNewEmptyRow(void)
  {
//--- Создаём новый объект строки
   CTableRow *row=new CTableRow(this.m_list_rows.Total());
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new row at position %u",__FUNCTION__, this.m_list_rows.Total());
      return NULL;
     }
//--- Если строку не удалось добавить в список - удаляем созданный новый объект и возвращаем NULL
   if(!this.AddNewRow(row))
     {
      delete row;
      return NULL;
     }
   
//--- Успешно - возвращаем указатель на созданный объект
   return row;
  }

Метод создаёт новый объект класса CTableRow и добавляет его в конец списка строк при помощи метода AddNewRow(). При ошибке добавления, созданный новый объект удаляется и возвращается NULL. При успешном выполнении, метод возвращает указатель на вновь добавленную в список строку.

Метод, добавляющий объект строку в конец списка:

//+------------------------------------------------------------------+
//| Добавляет строку в конец списка                                  |
//+------------------------------------------------------------------+
bool CTableModel::AddNewRow(CTableRow *row)
  {
//--- Если передан пустой объект - сообщаем об этом и возвращаем false
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Empty CTableRow object passed",__FUNCTION__);
      return false;
     }
//--- Устанавливаем строке её индекс в списке и добавляем её в конец списка
   row.SetIndex(this.RowsTotal());
   if(this.m_list_rows.Add(row)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add row (%u) to list",__FUNCTION__,row.Index());
      return false;
     }

//--- Успешно
   return true;
  }

Оба рассмотренных выше метода находятся в защищённой секции класса, работают в паре и служат для внутреннего использования при добавлении новых строк в таблицу.

Метод, для создания новой строки и добавления её в конец списка:

//+------------------------------------------------------------------+
//| Создаёт новую строку и добавляет в конец списка                  |
//+------------------------------------------------------------------+
CTableRow *CTableModel::RowAddNew(void)
  {
//--- Создаём новую пустую строку и добавляем её в конец списка строк
   CTableRow *row=this.CreateNewEmptyRow();
   if(row==NULL)
      return NULL;
      
//--- Создаём ячейки по количеству ячеек первой строки
   for(uint i=0;i<this.CellsInRow(0);i++)
      row.CreateNewCell(0.0);
   row.ClearData();
   
//--- Успешно - возвращаем указатель на созданный объект
   return row;
  }

Это публичный метод. Используется для добавления в таблицу новой строки с ячейками. Количество ячеек для созданной строки берётся из самой первой строки таблицы.

Метод для создания и добавления новой строки в указанную позицию списка:

//+------------------------------------------------------------------+
//| Создаёт и добавляет новую строку в указанную позицию списка      |
//+------------------------------------------------------------------+
CTableRow *CTableModel::RowInsertNewTo(const uint index_to)
  {
//--- Создаём новую пустую строку и добавляем её в конец списка строк
   CTableRow *row=this.CreateNewEmptyRow();
   if(row==NULL)
      return NULL;
     
//--- Создаём ячейки по количеству ячеек первой строки
   for(uint i=0;i<this.CellsInRow(0);i++)
      row.CreateNewCell(0.0);
   row.ClearData();
   
//--- Смещаем строку на позицию index_to
   this.RowMoveTo(this.m_list_rows.IndexOf(row),index_to);
   
//--- Успешно - возвращаем указатель на созданный объект
   return row;
  }

Иногда требуется вставить новую строку не в конец списка строк, а между имеющимися. Этот метод сначала создаёт новую строку в конце списка, заполняет её ячейками, очищает их, а затем смещает строку в нужную позицию.

Метод, устанавливающий значение в указанную ячейку:

//+------------------------------------------------------------------+
//| Устанавливает значение в указанную ячейку                        |
//+------------------------------------------------------------------+
template<typename T>
void CTableModel::CellSetValue(const uint row,const uint col,const T value)
  {
//--- Получаем ячейку по индексам строки и столбца
   CTableCell *cell=this.GetCell(row,col);
   if(cell==NULL)
      return;
//--- Получаем корректный тип устанавливаемых данных (double, long, datetime, color, string)
   ENUM_DATATYPE type=this.GetCorrectDatatype(typename(T));
//--- В зависимости от типа данных вызываем соответствующий типу данных
//--- метод ячейки для установки значения, явно указывая требуемый тип
   switch(type)
     {
      case TYPE_DOUBLE  :  cell.SetValue((double)value);    break;
      case TYPE_LONG    :  cell.SetValue((long)value);      break;
      case TYPE_DATETIME:  cell.SetValue((datetime)value);  break;
      case TYPE_COLOR   :  cell.SetValue((color)value);     break;
      case TYPE_STRING  :  cell.SetValue((string)value);    break;
      default           :  break;
     }
  }

Сначала получаем указатель на требуемую ячейку по координатам её строки и столбца, а затем устанавливаем в неё значение. Каким бы ни было переданное в метод значение для установки его в ячейку, метод выберет только корректный тип для установки — double, long, datetime, color или string.

Метод, устанавливающий точность отображения данных в указанную ячейку:

//+------------------------------------------------------------------+
//| Устанавливает точность отображения данных в указанную ячейку     |
//+------------------------------------------------------------------+
void CTableModel::CellSetDigits(const uint row,const uint col,const int digits)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetDigits(digits);
  }

Метод актуален только для ячеек, хранящих вещественный тип значения. Служит для указания количества знаков после запятой для отображаемого ячейкой значения.

Метод, устанавливающий флаги отображения времени в указанную ячейку:

//+------------------------------------------------------------------+
//| Устанавливает флаги отображения времени в указанную ячейку       |
//+------------------------------------------------------------------+
void CTableModel::CellSetTimeFlags(const uint row,const uint col,const uint flags)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetDatetimeFlags(flags);
  }

Актуально для ячеек, отображающих datetime-значения. Устанавливает формат отображения времени ячейкой (один из TIME_DATE|TIME_MINUTES|TIME_SECONDS, либо их комбинации)

TIME_DATE получает результат в форме " yyyy.mm.dd " ,
TIME_MINUTES получает результат в форме " hh:mi " ,
TIME_SECONDS получает результат в форме " hh:mi:ss ".

Метод, устанавливающий флаг отображения имён цветов в указанную ячейку:

//+------------------------------------------------------------------+
//| Устанавливает флаг отображения имён цветов в указанную ячейку    |
//+------------------------------------------------------------------+
void CTableModel::CellSetColorNamesFlag(const uint row,const uint col,const bool flag)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetColorNameFlag(flag);
  }

Актуален только для ячеек, отображающих color-значения. Указывает о необходимости выводить наименования цветов, если цвет, хранящийся в ячейке, присутствует в таблице цветов.

Метод, назначающий объект в ячейку:

//+------------------------------------------------------------------+
//| Назначает объект в ячейку                                        |
//+------------------------------------------------------------------+
void CTableModel::CellAssignObject(const uint row,const uint col,CObject *object)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.AssignObject(object);
  }

Метод, отменяющий назначение объекта в ячейке:

//+------------------------------------------------------------------+
//| Отменяет назначение объекта в ячейке                             |
//+------------------------------------------------------------------+
void CTableModel::CellUnassignObject(const uint row,const uint col)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.UnassignObject();
  }

Два представленных выше метода позволяют назначить на ячейку какой-либо объект, или снять его назначение. Объект должен быть наследником класса CObject. В контексте статей о таблицах, объектом может быть, например, какой-то один из списка известных объектов из перечисления ENUM_OBJECT_TYPE. На данный момент в списке есть только объекты ячейки, строки и модели таблицы. Их назначение на ячейку не имеет смысла. Но перечисление будет расширяться по ходу написания статей о компоненте View, где будут создаваться элементы управления. Вот их-то и целесообразно будет назначать на ячейку, например, элемент управления "выпадающий список".

Метод, удаляющий указанную ячейку:

//+------------------------------------------------------------------+
//| Удаляет ячейку                                                   |
//+------------------------------------------------------------------+
bool CTableModel::CellDelete(const uint row,const uint col)
  {
//--- Получаем строку по индексу и возвращаем результат удаления ячейки из списка
   CTableRow *row_obj=this.GetRow(row);
   return(row_obj!=NULL ? row_obj.CellDelete(col) : false);
  }

Метод получает объект строки по его индексу и вызывает его метод удаления указанной ячейки. Для одиночной ячейки в одиночной строке метод пока не имеет смысла, так как уменьшит количество ячеек только в одной строке таблицы. Это вызовет рассинхронизацию ячеек с соседними строками. Пока нет обработки такого удаления, где необходимо "расширить" соседнюю с удалённой ячейку до размера двух ячеек — чтобы не нарушалась структура таблицы. Но этот метод используется в составе метода удаления столбца таблицы, где удаляются ячейки сразу во всех строках, не нарушая целостности всей таблицы.

Метод, для перемещения ячейки таблицы:

//+------------------------------------------------------------------+
//| Перемещает ячейку                                                |
//+------------------------------------------------------------------+
bool CTableModel::CellMoveTo(const uint row,const uint cell_index,const uint index_to)
  {
//--- Получаем строку по индексу и возвращаем результат перемещения ячейки на новую позицию
   CTableRow *row_obj=this.GetRow(row);
   return(row_obj!=NULL ? row_obj.CellMoveTo(cell_index,index_to) : false);
  }

Получаем объект строки по его индексу и вызываем его метод удаления указанной ячейки.

Метод, возвращающий количество ячеек в указанной строке:

//+------------------------------------------------------------------+
//| Возвращает количество ячеек в указанной строке                   |
//+------------------------------------------------------------------+
uint CTableModel::CellsInRow(const uint index)
  {
   CTableRow *row=this.GetRow(index);
   return(row!=NULL ? row.CellsTotal() : 0);
  }

Получаем строку по индексу и возвращаем количество ячеек в ней, вызвав метод строки CellsTotal().

Метод, возвращающий количество ячеек в таблице:

//+------------------------------------------------------------------+
//| Возвращает количество ячеек в таблице                            |
//+------------------------------------------------------------------+
uint CTableModel::CellsTotal(void)
  {
//--- подсчёт ячеек в цикле по строкам (медленно при большом количестве строк)
   uint res=0, total=this.RowsTotal();
   for(int i=0; i<(int)total; i++)
     {
      CTableRow *row=this.GetRow(i);
      res+=(row!=NULL ? row.CellsTotal() : 0);
     }
   return res;
  }

Метод проходит по всем строкам таблицы и складывает количество ячеек каждой строки в общий результат, который и возвращается. При большом количестве строк в таблице такой подсчёт может быть медленным. После того, как будут созданы все методы, влияющие на количество ячеек таблицы, сделаем их учёт только в момент изменения их количества.

Метод, возвращающий указанную ячейку таблицы:

//+------------------------------------------------------------------+
//| Возвращает указанную ячейку таблицы                              |
//+------------------------------------------------------------------+
CTableCell *CTableModel::GetCell(const uint row,const uint col)
  {
//--- Получаем строку по индексу row и возвращаем по индексу col ячейку строки
   CTableRow *row_obj=this.GetRow(row);
   return(row_obj!=NULL ? row_obj.GetCell(col) : NULL);
  }

Получаем строку по индексу row и возвращаем указатель на объект ячейки по индексу col методом объекта строки GetCell().

Метод, возвращающий описание ячейки:

//+------------------------------------------------------------------+
//| Возвращает описание ячейки                                       |
//+------------------------------------------------------------------+
string CTableModel::CellDescription(const uint row,const uint col)
  {
   CTableCell *cell=this.GetCell(row,col);
   return(cell!=NULL ? cell.Description() : "");
  }

Получаем строку по индексу, из строки получаем ячейку и возвращаем её описание.

Метод, выводящий в журнал описание ячейки:

//+------------------------------------------------------------------+
//| Выводит в журнал описание ячейки                                 |
//+------------------------------------------------------------------+
void CTableModel::CellPrint(const uint row,const uint col)
  {
//--- Получаем ячейку по индексу строки и колонки и возвращаем её описание
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.Print();
  }

Получаем указатель на ячейку по индексам строки и колонки и, используя метод Print() объекта ячейки, выводим в журнал её описание.

Метод, удаляющий указанную строку:

//+------------------------------------------------------------------+
//| Удаляет строку                                                   |
//+------------------------------------------------------------------+
bool CTableModel::RowDelete(const uint index)
  {
//--- Удаляем строку из списка по индексу
   if(!this.m_list_rows.Delete(index))
      return false;
//--- После удаления строки необходимо обновить все индексы всех ячеек таблицы
   this.CellsPositionUpdate();
   return true;
  }

При помощи метода Delete() класса CList удаляем по индексу объект строки из списка. После удаления строки, индексы оставшихся строк и ячеек в них не соответствуют действительности, и их требуется скорректировать методом CellsPositionUpdate().

Метод, перемещающий строку на указанную позицию:

//+------------------------------------------------------------------+
//| Перемещает строку на указанную позицию                           |
//+------------------------------------------------------------------+
bool CTableModel::RowMoveTo(const uint row_index,const uint index_to)
  {
//--- Получаем строку по индексу, делая её текущей
   CTableRow *row=this.GetRow(row_index);
//--- Перемещаем текущую строку на указанную позицию в списке
   if(row==NULL || !this.m_list_rows.MoveToIndex(index_to))
      return false;
//--- После перемещения строки необходимо обновить все индексы всех ячеек таблицы
   this.CellsPositionUpdate();
   return true;
  }

В классе CList множество методов работают с текущим объектом списка. Получаем указатель на требуемую строку, делая её текущей, и смещаем на нужную позицию при помощи метода MoveToIndex() класса CList. После смещения строки на новую позицию необходимо обновить индексы для остальных строк, что и делаем методом CellsPositionUpdate().

Метод, устанавливающий позиции строки и колонки всем ячейкам:

//+------------------------------------------------------------------+
//| Устанавливает позиции строки и колонки всем ячейкам              |
//+------------------------------------------------------------------+
void CTableModel::CellsPositionUpdate(void)
  {
//--- В цикле по списку строк
   for(int i=0;i<this.m_list_rows.Total();i++)
     {
      //--- получаем очередную строку
      CTableRow *row=this.GetRow(i);
      if(row==NULL)
         continue;
      //--- устанавливаем строке индекс, найденный методом IndexOf() списка
      row.SetIndex(this.m_list_rows.IndexOf(row));
      //--- Обновляем индексы позиций ячеек строки
      row.CellsPositionUpdate();
     }
  }

Проходим по списку всех строк таблицы, выбираем каждую последующую строку и устанавливаем для неё корректный индекс, найденный методом IndexOf() класса CList. Затем вызываем метод строки CellsPositionUpdate(), который для каждой ячейки строки устанавливает корректный индекс.

Метод, очищающий данные всех ячеек строки:

//+------------------------------------------------------------------+
//| Очищает строку (только данные в ячйках)                          |
//+------------------------------------------------------------------+
void CTableModel::RowResetData(const uint index)
  {
//--- Получаем строку из списка и очищаем данные ячеек строки методом ClearData()
   CTableRow *row=this.GetRow(index);
   if(row!=NULL)
      row.ClearData();
  }

Каждая ячейка строки сбрасывается в "пустое" значение. Пока для упрощения пустым значением для числовых ячеек является ноль, в дальнейшем это будет изменено, так как ноль — тоже значение, которое нужно отображать в ячейке. А сброс значения подразумевает отображение пустого поля ячейки.

Метод, очищающий данные всех ячеек таблицы:

//+------------------------------------------------------------------+
//| Очищает таблицу (данные всех ячеек)                              |
//+------------------------------------------------------------------+
void CTableModel::ClearData(void)
  {
//--- В цикле по всем строкам таблицы очищаем данные каждой строки
   for(uint i=0;i<this.RowsTotal();i++)
      this.RowResetData(i);
  }

Проходим по всем строкам таблицы, и для каждой вызываем метод RowResetData(), рассмотренный выше.

Метод, возвращающий описание строки:

//+------------------------------------------------------------------+
//| Возвращает описание строки                                       |
//+------------------------------------------------------------------+
string CTableModel::RowDescription(const uint index)
  {
//--- Получаем строку по индексу и возвращаем её описание
   CTableRow *row=this.GetRow(index);
   return(row!=NULL ? row.Description() : "");
  }

Получаем указатель на строку по индексу и возвращаем её описание.

Метод, выводящий в журнал описание строки:

//+------------------------------------------------------------------+
//| Выводит в журнал описание строки                                 |
//+------------------------------------------------------------------+
void CTableModel::RowPrint(const uint index,const bool detail)
  {
   CTableRow *row=this.GetRow(index);
   if(row!=NULL)
      row.Print(detail);
  }

Получаем указатель на строку и вызываем метод Print() полученного объекта.

Метод, удаляющий столбец таблицы:

//+------------------------------------------------------------------+
//| Удаляет столбец                                                  |
//+------------------------------------------------------------------+
bool CTableModel::ColumnDelete(const uint index)
  {
   bool res=true;
   for(uint i=0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         res &=row.CellDelete(index);
     }
   return res;
  }

В цикле по всем строкам таблицы получаем каждую очередную строку и удаляем в ней требуемую ячейку по индексу столбца. Таким образом удаляются все ячейки таблицы, имеющие одинаковые индексы столбца.

Метод, перемещающий столбец таблицы:

//+------------------------------------------------------------------+
//| Перемещает столбец                                               |
//+------------------------------------------------------------------+
bool CTableModel::ColumnMoveTo(const uint col_index,const uint index_to)
  {
   bool res=true;
   for(uint i=0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         res &=row.CellMoveTo(col_index,index_to);
     }
   return res;
  }

В цикле по всем строкам таблицы получаем каждую очередную строку и перемещаем требуемую ячейку в новую позицию. Таким образом, перемещаются все ячейки таблицы, имеющие одинаковые индексы столбца.

Метод, очищающий данные ячеек столбца:

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

В цикле по всем строкам таблицы получаем каждую очередную строку и в требуемой ячейке очищаем данные. Таким образом очищаются все ячейки таблицы, имеющие одинаковые индексы столбца.

Метод, возвращающий описание объекта:

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
string CTableModel::Description(void)
  {
   return(::StringFormat("%s: Rows %u, Cells in row %u, Cells Total %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.RowsTotal(),this.CellsInRow(0),this.CellsTotal()));
  }

Создаётся и возвращается строка из некоторых параметров модели таблицы в таком формате:

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

Метод, выводящий в журнал описание объекта:

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта                                |
//+------------------------------------------------------------------+
void CTableModel::Print(const bool detail)
  {
//--- Выводим в журнал заголовок
   ::Print(this.Description()+(detail ? ":" : ""));
//--- Если детализированное описание,
   if(detail)
     {
      //--- В цикле по всем строкам таблицы
      for(uint i=0; i<this.RowsTotal(); i++)
        {
         //--- получаем очередную строку и выводим в журнал её детализированное описание
         CTableRow *row=this.GetRow(i);
         if(row!=NULL)
            row.Print(true,false);
        }
     }
  }

Сначала распечатывается заголовок в виде описания модели, и далее, если стоит флаг детализированного вывода, в цикле распечатываются детализированные описания всех строк модели таблицы.

Метод, выводящий в журнал описание объекта в табличном виде:

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта в табличном виде               |
//+------------------------------------------------------------------+
void CTableModel::PrintTable(const int cell_width=10)
  {
//--- Получаем указатель на первую строку (индекс 0)
   CTableRow *row=this.GetRow(0);
   if(row==NULL)
      return;
   //--- По количеству ячеек первой строки таблицы создаём строку заголовка таблицы
   uint total=row.CellsTotal();
   string head=" n/n";
   string res=::StringFormat("|%*s |",cell_width,head);
   for(uint i=0;i<total;i++)
     {
      if(this.GetCell(0, i)==NULL)
         continue;
      string cell_idx=" Column "+(string)i;
      res+=::StringFormat("%*s |",cell_width,cell_idx);
     }
   //--- Выводим строку заголовка в журнал
   ::Print(res);
   
   //--- Пройдём в цикле по всем строкам таблицы и распечатаем их в табличном виде
   for(uint i=0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         row.Print(true,true,cell_width);
     }
  }

Сначала, по количеству ячеек самой первой строки таблицы создаём и распечатываем в журнале заголовок таблицы с наименованиями колонок таблицы. Затем, проходим в цикле по всем строкам таблицы и распечатываем каждую в табличном виде.

Метод, уничтожающий модель таблицы:

//+------------------------------------------------------------------+
//| Уничтожает модель                                                |
//+------------------------------------------------------------------+
void CTableModel::Destroy(void)
  {
//--- Очищаем список строк
   m_list_rows.Clear();
  }

Просто очищается список строк таблицы с уничтожением всех объектов при помощи метода Clear() класса CList.

Метод для сохранения модели таблицы в файл:

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
bool CTableModel::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_rows.Save(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }

После сохранения маркера начала данных и типа списка, сохраняем в файл список строк при помощи метода Save() класса CList.

Метод для загрузки модели таблицы из файла:

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
bool CTableModel::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_rows.Load(file_handle))
      return(false);
   
//--- Успешно
   return true;
  }

После загрузки и проверки маркера начала данных и типа списка, загружаем список строк из файла при помощи метода Load() класса CListObj, рассмотренного в начале статьи.

Все классы для создания модели таблицы готовы. Теперь напишем скрипт для тестирования работы модели.


Протестируем результат

Продолжим писать код в том же файле. Напишем скрипт, в котором создадим двумерный массив размерностью 4x4 (4 строки по 4 ячейки в каждой) с типом, например, long. Далее, откроем файл для записи в него данных модели таблицы и загрузки данных в таблицу из файла. Создадим модель таблицы и проверим работу некоторых её методов.

Каждый раз, при изменении таблицы, будем выводить в журнал получившийся результат при помощи функции TableModelPrint(), где производится выбор, как распечатать модель таблицы. При значении макроса PRINT_AS_TABLE равном true, вывод в журнал производится методом PrintTable() класса CTableModel, при значении false — методом Print() того же класса.

В скрипте создадим модель таблицы, распечатаем её в табличном виде и сохраним модель в файл. Затем добавим строки, удалим столбцы, поменяем разрешение на редактирование...

Затем опять загрузим из файла первоначальную оригинальную версию таблицы и распечатаем результат.

#define  PRINT_AS_TABLE    true  // Распечатывать модель как таблицу
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Объявляем и заполняем массив с размерностью 4x4
//--- Тип массива может быть double, long, datetime, color, string
   long array[4][4]={{ 1,  2,  3,  4},
                     { 5,  6,  7,  8},
                     { 9, 10, 11, 12},
                     {13, 14, 15, 16}};
     
//--- Создаём модель таблицы из вышесозданного long-массива array 4x4
   CTableModel *tm=new CTableModel(array);
   
//--- Если модель не создана - уходим
   if(tm==NULL)
      return;

//--- Распечатаем модель в табличном виде
   Print("The table model has been successfully created:");
   tm.PrintTable();
   
//--- Удалим объект модели таблицы
   delete tm;
  }

В итоге получим такой результат работы скрипта в журнале:

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

Чтобы проверить работу с моделью таблицы, добавление, удаление и перемещение строк и столбцов, работу с файлом, допишем скрипт:

#define  PRINT_AS_TABLE    true  // Распечатывать модель как таблицу
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Объявляем и заполняем массив с размерностью 4x4
//--- Тип массива может быть double, long, datetime, color, string
   long array[4][4]={{ 1,  2,  3,  4},
                     { 5,  6,  7,  8},
                     { 9, 10, 11, 12},
                     {13, 14, 15, 16}};
     
//--- Создаём модель таблицы из вышесозданного long-массива array 4x4
   CTableModel *tm=new CTableModel(array);
   
//--- Если модель не создана - уходим
   if(tm==NULL)
      return;

//--- Распечатаем модель в табличном виде
   Print("The table model has been successfully created:");
   tm.PrintTable();
   
   
//--- Проверим работу с файлами и функционал модели таблицы
//--- Открываем файл для записи в него данных модели таблицы
   int handle=FileOpen(MQLInfoString(MQL_PROGRAM_NAME)+".bin",FILE_READ|FILE_WRITE|FILE_BIN|FILE_COMMON);
   if(handle==INVALID_HANDLE)
      return;
      
   //--- Сохраним в файл оригинальную созданную таблицу
   if(tm.Save(handle))
      Print("\nThe table model has been successfully saved to file.");
   
//--- Теперь вставим в таблицу новую строку в позицию 2
//--- Получим последнюю ячейку созданной строки и сделаем её нередактируемой
//--- Распечатаем в журнале изменённую модель таблицы
   if(tm.RowInsertNewTo(2))
     {
      Print("\nInsert a new row at position 2 and set cell 3 to non-editable");
      CTableCell *cell=tm.GetCell(2,3);
      if(cell!=NULL)
         cell.SetEditable(false);
      TableModelPrint(tm);
     }
   
//--- Теперь удалим столбец таблицы с индексом 1 и
//--- распечатаем в журнале полученную модель таблицы
   if(tm.ColumnDelete(1))
     {
      Print("\nRemove column from position 1");
      TableModelPrint(tm);
     }
   
//--- При сохранении данных таблицы файловый указатель был смещён на последние записанные данные
//--- Поставим указатель в начало файла, загрузим ранее сохранённую оригинальную таблицу и распечатаем её
   if(FileSeek(handle,0,SEEK_SET) && tm.Load(handle))
     {
      Print("\nLoad the original table view from the file:");
      TableModelPrint(tm);
     }
   
//--- Закроем открытый файл и удалим объект модели таблицы
   FileClose(handle);
   delete tm;
  }
//+------------------------------------------------------------------+
//| Распечатывает модель таблицы                                     |
//+------------------------------------------------------------------+
void TableModelPrint(CTableModel *tm)
  {
   if(PRINT_AS_TABLE)
      tm.PrintTable();  // Распечатать модель как таблицу
   else
      tm.Print(true);   // Распечатать детализированные данные таблицы
  }

Получим такой результат в журнале:

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

The table model has been successfully saved to file.

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

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

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

Если нужно вывести в журнал данные модели таблицы не в табличном виде, то макросе PRINT_AS_TABLE нужно подставить значение false:

#define  PRINT_AS_TABLE    false  // Распечатывать модель как таблицу

В этом случае, получим такой вывод в журнал:

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

The table model has been successfully saved to file.

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

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

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

Такой вывод предоставляет больше отладочной информации. Например, здесь мы видим, что установка в ячейку флага запрета редактирования отображена в логах программы.

Весь заявленный функционал работает, как и ожидалось, строки добавляются, перемещаются, столбцы удаляются, мы можем менять свойства ячеек и работать с файлами.


Заключение

Что ж, это первая версия простой модели таблицы, которая позволяет создать таблицу из двумерного массива данных. Далее нас ждёт создание других, специализированных моделей таблиц, создание компонента View таблицы и полноценная работа с табличными данными, как с одним из элементов управления пользовательского графического интерфейса.

К статье прикреплён файл созданного сегодня скрипта с включённым в него классами. Вы можете загрузить его для самостоятельного изучения.

Прикрепленные файлы |
TableModelTest.mq5 (136.43 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (9)
Alexey Viktorov
Alexey Viktorov | 4 апр. 2025 в 15:31
Artyom Trishkin #:

Когда класс объекта SomeObject загружается из файла, вызвав метод Load() этого самого объекта SomeObject, то он проверяет, "а правда ли я себя прочитал из файла?" (ты именно об этом спрашиваешь). Если нет, то значит что-то пошло не так, соответственно, и загружать нет смысла дальше.

У меня же тут СПИСОК (CListObj) читает тип объекта из файла. Список не знает что там (какой объект) лежит в файле. Но он должен знать этот тип объекта - чтобы создать его в своём методе CreateElement(). Поэтому тут и не проверяется тип загруженного объекта из файла. Ведь будет сравнение с типом Type(), который в данном методе возвращает тип списка, а не объекта.

Спасибо, разобрался, понял.

Maxim Kuznetsov
Maxim Kuznetsov | 5 апр. 2025 в 08:05

прочитал..потом ещё раз перепрочёл

это всё что угодно кроме кроме "модели" в MVC. Некий ListStorage например

Rashid Umarov
Rashid Umarov | 5 апр. 2025 в 08:37
Давайте по делу. Свое мнение оставьте при себе.
Aleksey Nikolayev
Aleksey Nikolayev | 5 апр. 2025 в 09:38
Интересно. Можно ли таким образом получить некий аналог датафреймов питона и R? Это такие таблицы, где в разных столбцах могут быть данные разных типов (из ограниченного набора типов, но включая string).
Artyom Trishkin
Artyom Trishkin | 5 апр. 2025 в 11:29
Aleksey Nikolayev #:
Интересно. Можно ли таким образом получить некий аналог датафреймов питона и R? Это такие таблицы, где в разных столбцах могут быть данные разных типов (из ограниченного набора типов, но включая string).

Можно. Если речь о разных столбцах одной таблицы, то в описываемой реализации каждая ячейка таблицы может иметь свой тип данных.

Введение в Connexus (Часть 1): Как использовать функцию WebRequest? Введение в Connexus (Часть 1): Как использовать функцию WebRequest?
Настоящая статья является началом серии разработок для библиотеки под названием “Connexus”, предназначенной для облегчения выполнения HTTP-запросов с помощью MQL5. Цель настоящего проекта - предоставить конечному пользователю такую возможность и показать, как использовать эту вспомогательную библиотеку. Я намеревался сделать его как можно более простым, чтобы облегчить изучение и обеспечить возможность для будущих разработок.
Создание торговой панели администратора на MQL5 (Часть II): Повышение оперативности реагирования и быстрого обмена сообщениями Создание торговой панели администратора на MQL5 (Часть II): Повышение оперативности реагирования и быстрого обмена сообщениями
В настоящей статье улучшим оперативность работы панели администратора, созданную нами ранее. Кроме того, мы рассмотрим важность быстрого обмена сообщениями в контексте торговых сигналов.
Переходим на MQL5 Algo Forge (Часть 1): Создание основного репозитория Переходим на MQL5 Algo Forge (Часть 1): Создание основного репозитория
В процессе работы над проектами в MetaEditor разработчики сталкиваются с необходимостью управления версиями кода. Несмотря на планы по переходу на GIT и запуск MQL5 Algo Forge, интеграция еще не завершена. В статье рассматриваются возможные способы повышения удобства работы с текущими инструментами.
Оптимизация нейробоидами — Neuroboids Optimization Algorithm 2 (NOA2) Оптимизация нейробоидами — Neuroboids Optimization Algorithm 2 (NOA2)
Новый авторский алгоритм оптимизации NOA2 (Neuroboids Optimization Algorithm 2), объединяет принципы роевого интеллекта с нейронным управлением. NOA2 сочетает механику поведения стаи нейробоидов с адаптивной нейронной системой, позволяющей агентам самостоятельно корректировать свое поведение в процессе поиска оптимума. Алгоритм находится на стадии активной разработки и демонстрирует потенциал для решения сложных задач оптимизации.