
Реализация модели таблицы в MQL5: Применение концепции MVC
Содержание
- Введение
- Немного о концепции MVC (Model-View-Controller)
- Пишем классы для построения модели таблицы
- Связанные списки как основа хранения табличных данных
- Класс ячейки таблицы
- Класс строки таблицы
- Класс модели таблицы
- Протестируем результат
- Заключение
Введение
В программировании архитектура приложения играет ключевую роль в обеспечении надёжности, масштабируемости и удобстве поддержки. Одним из подходов, который помогает достичь таких целей, является использование архитектурного шаблона 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),
- В файл записывается тип объекта,
- В файл поочерёдно записываются все свойства объекта.
Первый и второй пункты присущи всем реализованным методам сохранения/загрузки, которые есть у объектов Стандартной Библиотеки. Соответственно, следуя той же логике, нам нужно знать тип объекта, сохранённого в списке, чтобы при чтении из файла создать объект с таким типом в виртуальном методе 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 таблицы и полноценная работа с табличными данными, как с одним из элементов управления пользовательского графического интерфейса.
К статье прикреплён файл созданного сегодня скрипта с включённым в него классами. Вы можете загрузить его для самостоятельного изучения.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Когда класс объекта SomeObject загружается из файла, вызвав метод Load() этого самого объекта SomeObject, то он проверяет, "а правда ли я себя прочитал из файла?" (ты именно об этом спрашиваешь). Если нет, то значит что-то пошло не так, соответственно, и загружать нет смысла дальше.
У меня же тут СПИСОК (CListObj) читает тип объекта из файла. Список не знает что там (какой объект) лежит в файле. Но он должен знать этот тип объекта - чтобы создать его в своём методе CreateElement(). Поэтому тут и не проверяется тип загруженного объекта из файла. Ведь будет сравнение с типом Type(), который в данном методе возвращает тип списка, а не объекта.
Спасибо, разобрался, понял.
прочитал..потом ещё раз перепрочёл
это всё что угодно кроме кроме "модели" в MVC. Некий ListStorage например
Интересно. Можно ли таким образом получить некий аналог датафреймов питона и R? Это такие таблицы, где в разных столбцах могут быть данные разных типов (из ограниченного набора типов, но включая string).
Можно. Если речь о разных столбцах одной таблицы, то в описываемой реализации каждая ячейка таблицы может иметь свой тип данных.