preview
Таблицы в парадигме MVC на MQL5: настраиваемые и сортируемые столбцы таблицы

Таблицы в парадигме MVC на MQL5: настраиваемые и сортируемые столбцы таблицы

MetaTrader 5Примеры |
412 2
Artyom Trishkin
Artyom Trishkin

Содержание



Введение

В предыдущей статье, посвящённой созданию таблиц на MQL5 в парадигме MVC, мы связали воедино табличные данные (Model) с их графическим представлением (View) в едином элементе управления (TableView), и на основе полученного объекта создали простую статичную таблицу. Таблицы — это удобный инструмент для классификации и отображения различных данных в удобном для пользователя представлении. Соответственно, таблица должна давать больше возможностей для пользователя по управлению отображением данных.

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

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



Дорабатываем классы библиотеки

Все файлы разрабатываемой библиотеки расположены по адресу \MQL5\Indicators\Tables\. Файл классов модели таблиц (Tables.mqh), вместе с файлом тестового индикатора (iTestTable.mq5), располагается в папке \MQL5\Indicators\Tables\. 

Файлы графической библиотеки (Base.mqh и Controls.mqh) расположены в подпапке \MQL5\Indicators\Tables\Controls\. Все необходимые для работы файлы можно загрузить одним архивом из прeдыдущей статьи.

Доработаем классы модели таблиц в файле \MQL5\Indicators\Tables\Tables.mqh.

Строки в модели таблицы по умолчанию имеют тип сортировки по идентификатору (индексу) строки. Самая первая строка имеет индекс 0. И ячейки в строке тоже начинаются с нулевого индекса. Для сортировки по индексам ячеек нам нужно обозначить некое число, которое будем прибавлять к индексу ячейки, и по которому будем определять, что задана сортировка по индексу ячейки. И ещё нам нужно обозначить направление сортировки. Значит, нам нужны два числа: первое будет определять, что требуется сортировка по индексу ячейки по возрастанию, второе число будет определять необходимость сортировки по индексу ячейки по убыванию.

Определим макроподстановки для этих чисел:

//+------------------------------------------------------------------+
//| Макросы                                                          |
//+------------------------------------------------------------------+
#define  __TABLES__                 // Идентификатор данного файла
#define  MARKER_START_DATA    -1    // Маркер начала данных в файле
#define  MAX_STRING_LENGTH    128   // Максимальная длина строки в ячейке
#define  CELL_WIDTH_IN_CHARS  19    // Ширина ячейки таблицы в символах
#define  ASC_IDX_CORRECTION   10000 // Смещение индекса столбца для сортировки по возрастанию
#define  DESC_IDX_CORRECTION  20000 // Смещение индекса столбца для сортировки по убыванию

Определив такие макроподстановки, мы указали, что строк в таблице и ячеек в одной строке может быть не более 10 000. Кажется, этого более чем достаточно для работы с таблицами (100 миллионов ячеек таблицы). 

В метод сортировки будем передавать в качестве параметра mode номер от нуля до 10 000 — это будет сортировка по индексам строк таблицы. Числа от 10 000 включительно до 19 999 будут указывать на сортировку по индексу столбца таблицы по возрастанию. Числа от 20 000 — по индексу столбца по убыванию:

/*
    Sort(0)                      -  по индексу строки
    
    Sort(ASC_IDX_CORRECTION)     -  по возрастанию по столбцу 0
    Sort(1+ASC_IDX_CORRECTION)   -  по возрастанию по столбцу 1
    Sort(2+ASC_IDX_CORRECTION)   -  по возрастанию по столбцу 2
    и т.д.
    Sort(DESC_IDX_CORRECTION)    -  по убыванию по столбцу 0
    Sort(1+DESC_IDX_CORRECTION)  -  по убыванию по столбцу 1
    Sort(2+DESC_IDX_CORRECTION)  -  по убыванию по столбцу 2
    и т.д.
*/  

Для того, чтобы метод сортировки списка Sort() правильно работал, необходимо переопределить виртуальный метод сравнения двух объектов Compare(), определённый в базовом объекте Стандартной Библиотеки. По умолчанию этот метод возвращает 0, что означает равенство сравниваемых объектов.

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

//+------------------------------------------------------------------+
//| Сравнение двух объектов                                          |
//+------------------------------------------------------------------+
int CTableRow::Compare(const CObject *node,const int mode=0) const
  {
   if(node==NULL)
      return -1;
   
//--- Сортировка по индексу строки
   if(mode==0)
     {
      const CTableRow *obj=node;
      return(this.Index()>obj.Index() ? 1 : this.Index()<obj.Index() ? -1 : 0);
     }
   
//--- Сортировка по индексу ячейки с возрастанием/убыванием
//--- Флаг направления сортировки и индекс ячейки для сортировки
   bool asc=(mode>=ASC_IDX_CORRECTION && mode<DESC_IDX_CORRECTION);
   int  col= mode%(asc ? ASC_IDX_CORRECTION : DESC_IDX_CORRECTION);
      
//--- Снимаем константность node
   CTableRow *nonconst_this=(CTableRow*)&this;
   CTableRow *nonconst_node=(CTableRow*)node;

//--- Получаем текущую и сравниваемую ячейки по индексу mode
   CTableCell *cell_current =nonconst_this.GetCell(col);
   CTableCell *cell_compared=nonconst_node.GetCell(col);
   if(cell_current==NULL || cell_compared==NULL)
      return -1;
   
//--- Сравниваем в зависимости от типа ячейки
   int cmp=0;
   switch(cell_current.Datatype())
     {
      case TYPE_DOUBLE  :  cmp=(cell_current.ValueD()>cell_compared.ValueD() ? 1 : cell_current.ValueD()<cell_compared.ValueD() ? -1 : 0); break;
      case TYPE_LONG    :
      case TYPE_DATETIME:
      case TYPE_COLOR   :  cmp=(cell_current.ValueL()>cell_compared.ValueL() ? 1 : cell_current.ValueL()<cell_compared.ValueL() ? -1 : 0); break;
      case TYPE_STRING  :  cmp=::StringCompare(cell_current.ValueS(),cell_compared.ValueS());                                              break;
      default           :  break;
     }
//--- Возвращаем результат сравнения ячеек с учётом направления сортировки
   return(asc ? cmp : -cmp);   
  }

Блок кода, выделенный цветом, выполняет сравнение двух ячеек таблицы по возрастанию (mode >= 10000 && <20000) и убыванию (mode>=20000). Так как нам для сравнения потребуется получить из объекта строки нужные объекты ячеек, а они не константные (при этом строка для сравнения в метод передана как константный указатель), то сначала необходимо снять константность для *node, объявив неконстантные объекты для сравнения. И уже из них получать объекты ячеек для сравнения.

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

В класс модели таблицы CTableModel добавим три новых метода для упрощения работы со столбцами таблицы и сортировки по ним:

public:
//--- Создаёт новую строку и (1) добавляет в конец списка, (2) вставляет в указанную позицию списка
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Удаляет (2) перемещает строку, (3) очищает данные строки
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Возвращает, (2) выводит в журнал описание строки
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Добавляет, (2) удаляет (3) перемещает столбец, (4) очищает данные, устанавливает (5) тип,
//--- (6) точность данных, флаги отображения (7) времени, (8) имён цветов столбца
   bool              ColumnAddNew(const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint col_index, const uint index_to);
   void              ColumnClearData(const uint index);
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   void              ColumnSetDigits(const uint index,const int digits);
   void              ColumnSetTimeFlags(const uint index, const uint flags);
   void              ColumnSetColorNamesFlag(const uint index, const bool flag);
  
//--- Сортирует таблицу по указанному столбцу и направлению
   void              SortByColumn(const uint column, const bool descending);
   
//--- (1) Возвращает, (2) выводит в журнал описание таблицы
   virtual string    Description(void);
   void              Print(const bool detail);
   void              PrintTable(const int cell_width=CELL_WIDTH_IN_CHARS);

За пределами тела класса напишем их реализацию.

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

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

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

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

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

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

//+------------------------------------------------------------------+
//| Сортирует таблицу по указанному столбцу и направлению            |
//+------------------------------------------------------------------+
void CTableModel::SortByColumn(const uint column,const bool descending)
  {
   if(this.m_list_rows.Total()==0)
      return;
   int mode=(int)column+(descending ? DESC_IDX_CORRECTION : ASC_IDX_CORRECTION);
   this.m_list_rows.Sort(mode);
   this.CellsPositionUpdate();   
  }

В метод передаётся индекс столбца таблицы, по значениям которого необходимо сортировать таблицу, и флаг направления сортировки. Если список строк пустой — уходим из метода. Далее определяем режим сортировки (mode). Если сортировка по убыванию (descending == true), то к индексу столбца прибавляем 20000, если же сортировка по возрастанию, то к индексу столбца прибавляем 10000. Далее вызываем метод сортировки с указанием режима и обновляем все ячейки таблицы в каждой строке.

Теперь добавим новые методы в класс таблицы CTable. Это одноимённые методы для только что добавленных в класс модели таблицы:

public:
//--- (1) Возвращает, (2) выводит в журнал описание ячейки, (3) назначенный в ячейку объект
   string            CellDescription(const uint row, const uint col);
   void              CellPrint(const uint row, const uint col);
//---Возвращает (1) назначенный в ячейку объект, (2) тип назначенного в ячейку объекта
   CObject          *CellGetObject(const uint row, const uint col);
   ENUM_OBJECT_TYPE  CellGetObjType(const uint row, const uint col);
   
//--- Создаёт новую строку и (1) добавляет в конец списка, (2) вставляет в указанную позицию списка
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Удаляет (2) перемещает строку, (3) очищает данные строки
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Возвращает, (2) выводит в журнал описание строки
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Добавляет новый, (2) удаляет, (3) перемещает столбец, (4) очищает данные столбца
   bool              ColumnAddNew(const string caption,const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint index, const uint index_to);
   void              ColumnClearData(const uint index);
   
//--- Устанавливает (1) значение указанному заголовку, (2) точность данных,
//--- флаги отображения (3) времени, (4) имён цвета указанному столбцу
   void              ColumnCaptionSetValue(const uint index,const string value);
   void              ColumnSetDigits(const uint index,const int digits);
   void              ColumnSetTimeFlags(const uint index,const uint flags);
   void              ColumnSetColorNamesFlag(const uint col, const bool flag);
   
//--- (1) Устанавливает, (2) возвращает тип данных для указанного столбца
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   ENUM_DATATYPE     ColumnDatatype(const uint index);
   
//--- (1) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   void              Print(const int column_width=CELL_WIDTH_IN_CHARS);
  
//--- Сортирует таблицу по указанному столбцу и направлению
   void              SortByColumn(const uint column, const bool descending)
                       {
                        if(this.m_table_model!=NULL)
                           this.m_table_model.SortByColumn(column,descending);
                       }
   
//--- Виртуальные методы (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);           }

 ...

//+------------------------------------------------------------------+
//| Устанавливает флаги отображения времени указанному столбцу       |
//+------------------------------------------------------------------+
void CTable::ColumnSetTimeFlags(const uint index,const uint flags)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetTimeFlags(index,flags);
  }
//+------------------------------------------------------------------+
//| Устанавливает флаги отображения имён цвета указанному столбцу    |
//+------------------------------------------------------------------+
void CTable::ColumnSetColorNamesFlag(const uint index,const bool flag)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetColorNamesFlag(index,flag);
  }

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

Доработаем классы в файле \MQL5\Indicators\Tables\Controls\Base.mqh.

Допишем форвард-декларацию классов, которые сделаны были в прошлый раз, но не были добавлены в список, и новый класс, который сегодня будем делать:

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#include <Canvas\Canvas.mqh>              // Класс СБ CCanvas
#include <Arrays\List.mqh>                // Класс СБ CList
#include "..\Tables.mqh"

//--- Форвард-декларация классов элементов управления
class    CBoundedObj;                     // Базовый класс, хранящий размеры объекта
class    CCanvasBase;                     // Базовый класс холста графических элементов
class    CCounter;                        // Класс счётчика задержки
class    CAutoRepeat;                     // Класс автоповтора событий
class    CImagePainter;                   // Класс рисования изображений
class    CVisualHint;                     // Класс подсказки
class    CLabel;                          // Класс текстовой метки
class    CButton;                         // Класс простой кнопки
class    CButtonTriggered;                // Класс двухпозиционной кнопки
class    CButtonArrowUp;                  // Класс кнопки со стрелкой вверх
class    CButtonArrowDown;                // Класс кнопки со стрелкой вниз
class    CButtonArrowLeft;                // Класс кнопки со стрелкой влево
class    CButtonArrowRight;               // Класс кнопки со стрелкой вправо
class    CCheckBox;                       // Класс элемента управления CheckBox
class    CRadioButton;                    // Класс элемента управления RadioButton
class    CScrollBarThumbH;                // Класс ползунка горизонтальной полосы прокрутки
class    CScrollBarThumbV;                // Класс ползунка вертикальной полосы прокрутки
class    CScrollBarH;                     // Класс горизонтальной полосы прокрутки
class    CScrollBarV;                     // Класс вертикальной полосы прокрутки
class    CTableCellView;                  // Класс визуального представления  ячейки таблицы
class    CTableRowView;                   // Класс визуального представления строки таблицы
class    CColumnCaptionView;              // Класс визуального представления заголовка столбца таблицы
class    CTableHeaderView;                // Класс визуального представления заголовка таблицы
class    CTableView;                      // Класс визуального представления таблицы
class    CTableControl;                   // Класс управления таблицами
class    CPanel;                          // Класс элемента управления Panel
class    CGroupBox;                       // Класс элемента управления GroupBox
class    CContainer;                      // Класс элемента управления Container

В перечисление типов графических элементов добавим новый тип:

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE                    // Перечисление типов графических элементов
  {
   ELEMENT_TYPE_BASE = 0x10000,           // Базовый объект графических элементов
   ELEMENT_TYPE_COLOR,                    // Объект цвета
   ELEMENT_TYPE_COLORS_ELEMENT,           // Объект цветов элемента графического объекта
   ELEMENT_TYPE_RECTANGLE_AREA,           // Прямоугольная область элемента
   ELEMENT_TYPE_IMAGE_PAINTER,            // Объект для рисования изображений
   ELEMENT_TYPE_COUNTER,                  // Объект счётчика
   ELEMENT_TYPE_AUTOREPEAT_CONTROL,       // Объект автоповтора событий
   ELEMENT_TYPE_BOUNDED_BASE,             // Базовый объект размеров графических элементов
   ELEMENT_TYPE_CANVAS_BASE,              // Базовый объект холста графических элементов
   ELEMENT_TYPE_ELEMENT_BASE,             // Базовый объект графических элементов
   ELEMENT_TYPE_HINT,                     // Подсказка
   ELEMENT_TYPE_LABEL,                    // Текстовая метка
   ELEMENT_TYPE_BUTTON,                   // Простая кнопка
   ELEMENT_TYPE_BUTTON_TRIGGERED,         // Двухпозиционная кнопка
   ELEMENT_TYPE_BUTTON_ARROW_UP,          // Кнопка со стрелкой вверх
   ELEMENT_TYPE_BUTTON_ARROW_DOWN,        // Кнопка со стрелкой вниз
   ELEMENT_TYPE_BUTTON_ARROW_LEFT,        // Кнопка со стрелкой влево
   ELEMENT_TYPE_BUTTON_ARROW_RIGHT,       // Кнопка со стрелкой вправо
   ELEMENT_TYPE_CHECKBOX,                 // Элемент управления CheckBox
   ELEMENT_TYPE_RADIOBUTTON,              // Элемент управления RadioButton
   ELEMENT_TYPE_SCROLLBAR_THUMB_H,        // Ползунок горизонтальной полосы прокрутки
   ELEMENT_TYPE_SCROLLBAR_THUMB_V,        // Ползунок вертикальной полосы прокрутки
   ELEMENT_TYPE_SCROLLBAR_H,              // Элемент управления ScrollBarHorisontal
   ELEMENT_TYPE_SCROLLBAR_V,              // Элемент управления ScrollBarVertical
   ELEMENT_TYPE_TABLE_CELL_VIEW,          // Ячейка таблицы (View)
   ELEMENT_TYPE_TABLE_ROW_VIEW,           // Строка таблицы (View)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW,// Заголовок столбца таблицы (View)
   ELEMENT_TYPE_TABLE_HEADER_VIEW,        // Заголовок таблицы (View)
   ELEMENT_TYPE_TABLE_VIEW,               // Таблица (View)
   ELEMENT_TYPE_TABLE_CONTROL_VIEW,       // Элемент управления таблицами (View)
   ELEMENT_TYPE_PANEL,                    // Элемент управления Panel
   ELEMENT_TYPE_GROUPBOX,                 // Элемент управления GroupBox
   ELEMENT_TYPE_CONTAINER,                // Элемент управления Container
  };

В функцию, возвращающую короткое имя элемента по типу, добавим новый тип объекта:

//+------------------------------------------------------------------+
//|  Возвращает короткое имя элемента по типу                        |
//+------------------------------------------------------------------+
string ElementShortName(const ENUM_ELEMENT_TYPE type)
  {
   switch(type)
     {
      case ELEMENT_TYPE_ELEMENT_BASE               :  return "BASE";    // Базовый объект графических элементов
      case ELEMENT_TYPE_HINT                       :  return "HNT";     // Подсказка
      case ELEMENT_TYPE_LABEL                      :  return "LBL";     // Текстовая метка
      case ELEMENT_TYPE_BUTTON                     :  return "SBTN";    // Простая кнопка
      case ELEMENT_TYPE_BUTTON_TRIGGERED           :  return "TBTN";    // Двухпозиционная кнопка
      case ELEMENT_TYPE_BUTTON_ARROW_UP            :  return "BTARU";   // Кнопка со стрелкой вверх
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN          :  return "BTARD";   // Кнопка со стрелкой вниз
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT          :  return "BTARL";   // Кнопка со стрелкой влево
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT         :  return "BTARR";   // Кнопка со стрелкой вправо
      case ELEMENT_TYPE_CHECKBOX                   :  return "CHKB";    // Элемент управления CheckBox
      case ELEMENT_TYPE_RADIOBUTTON                :  return "RBTN";    // Элемент управления RadioButton
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H          :  return "THMBH";   // Ползунок горизонтальной полосы прокрутки
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V          :  return "THMBV";   // Ползунок вертикальной полосы прокрутки
      case ELEMENT_TYPE_SCROLLBAR_H                :  return "SCBH";    // Элемент управления ScrollBarHorisontal
      case ELEMENT_TYPE_SCROLLBAR_V                :  return "SCBV";    // Элемент управления ScrollBarVertical
      case ELEMENT_TYPE_TABLE_CELL_VIEW            :  return "TCELL";   // Ячейка таблицы (View)
      case ELEMENT_TYPE_TABLE_ROW_VIEW             :  return "TROW";    // Строка таблицы (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  return "TCAPT";   // Заголовок столбца таблицы (View)
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  return "THDR";    // Заголовок таблицы (View)
      case ELEMENT_TYPE_TABLE_VIEW                 :  return "TABLE";   // Таблица (View)
      case ELEMENT_TYPE_TABLE_CONTROL_VIEW         :  return "TBLCTRL"; // Элемент управления таблицами (View)
      case ELEMENT_TYPE_PANEL                      :  return "PNL";     // Элемент управления Panel
      case ELEMENT_TYPE_GROUPBOX                   :  return "GRBX";    // Элемент управления GroupBox
      case ELEMENT_TYPE_CONTAINER                  :  return "CNTR";    // Элемент управления Container
      default                                      :  return "Unknown"; // Unknown
     }
  }

В базовый класс графических элементов добавим методы, возвращающие координаты курсора:

//+------------------------------------------------------------------+
//| Базовый класс графических элементов                              |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   int               m_id;                                     // Идентифткатор
   ushort            m_name[];                                 // Наименование
   
public:
//--- Устанавливает (1) наименование, (2) идентификатор
   void              SetName(const string name)                { ::StringToShortArray(name,this.m_name);          }
   virtual void      SetID(const int id)                       { this.m_id=id;                                    }
//--- Возвращает (1) наименование, (2) идентификатор
   string            Name(void)                          const { return ::ShortArrayToString(this.m_name);        }
   int               ID(void)                            const { return this.m_id;                                }

//--- Возвращает координаты курсора
   int               CursorX(void)                       const { return CCommonManager::GetInstance().CursorX();  }
   int               CursorY(void)                       const { return CCommonManager::GetInstance().CursorY();  }

//--- Виртуальные методы (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(ELEMENT_TYPE_BASE); }
   
//--- (1) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   virtual void      Print(void);
   
//--- Конструктор/деструктор
                     CBaseObj (void) : m_id(-1) { this.SetName(""); }
                    ~CBaseObj (void) {}
  };

Это позволит каждому графическому элементу в любой момент иметь доступ к координатам курсора. Класс-синглтон CCommonManager постоянно отслеживает координаты курсора, и обращение к нему из любого графического элемента даёт доступ элементу к этим координатам.

В классе прямоугольной области добавим метод, снимающий назначение объекта на область:

//+------------------------------------------------------------------+
//| Класс прямоугольной области                                      |
//+------------------------------------------------------------------+
class CBound : public CBaseObj
  {
protected:
   CBaseObj         *m_assigned_obj;                           // Назначенный на область объект
   CRect             m_bound;                                  // Структура прямоугольной области

public:
//--- Изменяет (1) ширину, (2) высоту, (3) размер ограничивающего прямоугольника
   void              ResizeW(const int size)                   { this.m_bound.Width(size);                                    }
   void              ResizeH(const int size)                   { this.m_bound.Height(size);                                   }
   void              Resize(const int w,const int h)           { this.m_bound.Width(w); this.m_bound.Height(h);               }
   
//--- Устанавливает координату (1) X, (2) Y, (3) обе координаты ограничивающего прямоугольника
   void              SetX(const int x)                         { this.m_bound.left=x;                                         }
   void              SetY(const int y)                         { this.m_bound.top=y;                                          }
   void              SetXY(const int x,const int y)            { this.m_bound.LeftTop(x,y);                                   }
   
//--- (1) Устанавливает, (2) смещает ограничивающий прямоугольник на указанные координаты/размер смещения
   void              Move(const int x,const int y)             { this.m_bound.Move(x,y);                                      }
   void              Shift(const int dx,const int dy)          { this.m_bound.Shift(dx,dy);                                   }
   
//--- Возвращает координаты, размеры и границы объекта
   int               X(void)                             const { return this.m_bound.left;                                    }
   int               Y(void)                             const { return this.m_bound.top;                                     }
   int               Width(void)                         const { return this.m_bound.Width();                                 }
   int               Height(void)                        const { return this.m_bound.Height();                                }
   int               Right(void)                         const { return this.m_bound.right-(this.m_bound.Width()  >0 ? 1 : 0);}
   int               Bottom(void)                        const { return this.m_bound.bottom-(this.m_bound.Height()>0 ? 1 : 0);}

//--- Возвращает флаг нахождения курсора внутри области
   bool              Contains(const int x,const int y)   const { return this.m_bound.Contains(x,y);                           }
   
//--- (1) Назначает, (2) снимает назначение, (3) возвращает указатель на назначенный элемент
   void              AssignObject(CBaseObj *obj)               { this.m_assigned_obj=obj;                                     }
   void              UnassignObject(void)                      { this.m_assigned_obj=NULL;                                    }           
   CBaseObj         *GetAssignedObj(void)                      { return this.m_assigned_obj;                                  }
   
//--- Возвращает описание объекта
   virtual string    Description(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(ELEMENT_TYPE_RECTANGLE_AREA);                         }
   
//--- Конструкторы/деструктор
                     CBound(void) { ::ZeroMemory(this.m_bound); }
                     CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h);             }
                    ~CBound(void) { ::ZeroMemory(this.m_bound); }
  };

Если какой-то графический элемент программно удаляется, то будучи назначенным на какую-либо область в другом графическом элементе, его указатель останется прописанным для этой области. Обращение к элементу по этому указателю, приведёт к критическому завершению программы. Поэтому при удалении какого-либо объекта, необходимо открепить его от области, если он был к ней прикреплён. Запись значения NULL в указатель даст нам возможность контролировать валидность указателя на уже удалённый графический элемент.

Классы графических элементов в разрабатываемой библиотеке построены так, что если объект прикреплён к какому-либо контейнеру, то при выходе элемента за пределы своего контейнера, элемент обрезается по границам своего контейнера. Если элемент находится полностью за пределами контейнера, то он попросту скрывается. Но если идёт команда для контейнера переместить его на передний план, то контейнер автоматически в цикле переносит на передний план и все прикреплённые к нему элементы. Соответственно, скрытые элементы оказываются видимыми, так как перенос объекта на передний план — это последовательное выполнение двух команд скрыть-отобразить. Чтобы не перемещать скрытые элементы на передний план, нужно дополнить свойства графического элемента флагом, что он скрыт из-за того, что находится за пределами своего контейнера. А в методе переноса объекта на передний план проверять этот флаг.

В базовом классе холста графических элементов CCanvasBase в защищённой секции объявим такой флаг:

protected:
   CCanvas          *m_background;                             // Канвас для рисования фона
   CCanvas          *m_foreground;                             // Канвас для рисования переднего плана
   CCanvasBase      *m_container;                              // Родительский объект-контейнер
   CColorElement     m_color_background;                       // Объект управления цветом фона
   CColorElement     m_color_foreground;                       // Объект управления цветом переднего плана
   CColorElement     m_color_border;                           // Объект управления цветом рамки
   
   CColorElement     m_color_background_act;                   // Объект управления цветом фона активированного элемента
   CColorElement     m_color_foreground_act;                   // Объект управления цветом переднего плана активированного элемента
   CColorElement     m_color_border_act;                       // Объект управления цветом рамки активированного элемента
   
   CAutoRepeat       m_autorepeat;                             // Объект управления автоповторами событий
   
   ENUM_ELEMENT_STATE m_state;                                 // Состояние элемента (напр., кнопки (вкл/выкл))
   long              m_chart_id;                               // Идентификатор графика
   int               m_wnd;                                    // Номер подокна графика
   int               m_wnd_y;                                  // Смещение координаты Y курсора в подокне
   int               m_obj_x;                                  // Координата X графического объекта
   int               m_obj_y;                                  // Координата Y графического объекта
   uchar             m_alpha_bg;                               // Прозрачность фона
   uchar             m_alpha_fg;                               // Прозрачность переднего плана
   uint              m_border_width_lt;                        // Ширина рамки слева
   uint              m_border_width_rt;                        // Ширина рамки справа
   uint              m_border_width_up;                        // Ширина рамки сверху
   uint              m_border_width_dn;                        // Ширина рамки снизу
   string            m_program_name;                           // Имя программы
   bool              m_hidden;                                 // Флаг скрытого объекта
   bool              m_blocked;                                // Флаг заблокированного элемента
   bool              m_movable;                                // Флаг перемещаемого элемента
   bool              m_resizable;                              // Флаг разрешения изменения размеров
   bool              m_focused;                                // Флаг элемента в фокусе
   bool              m_main;                                   // Флаг главного объекта
   bool              m_autorepeat_flag;                        // Флаг автоповтора отправки событий
   bool              m_scroll_flag;                            // Флаг прокрутки содержимого при помощи скроллбаров
   bool              m_trim_flag;                              // Флаг обрезки элемента по границам контейнера
   bool              m_cropped;                                // Флаг того, что объект скрыт за границами контейнера
   int               m_cursor_delta_x;                         // Дистанция от курсора до левого края элемента
   int               m_cursor_delta_y;                         // Дистанция от курсора до верхнего края элемента
   int               m_z_order;                                // Z-ордер графического объекта

В публичной секции напишем метод, возвращающий этот флаг:

public:
//--- (1) Устанавливает, (2) возвращает состояние
   void              SetState(ENUM_ELEMENT_STATE state)        { this.m_state=state; this.ColorsToDefault();                                       }
   ENUM_ELEMENT_STATE State(void)                        const { return this.m_state;                                                              }

//--- (1) Устанавливает, (2) возвращает z-ордер
   bool              ObjectSetZOrder(const int value);
   int               ObjectZOrder(void)                  const { return this.m_z_order;                                                            }
   
//--- Возвращает (1) принадлежность объекта программе, флаг (2) скрытого, (3) заблокированного,
//--- (4) перемещаемого, (5) изменяемого в размерах, (6) главного элемента, (7) в фокусе, (8, 9) имя графического объекта (фон, текст)
   bool              IsBelongsToThis(const string name)  const { return(::ObjectGetString(this.m_chart_id,name,OBJPROP_TEXT)==this.m_program_name);}
   bool              IsHidden(void)                      const { return this.m_hidden;                                                             }
   bool              IsBlocked(void)                     const { return this.m_blocked;                                                            }
   bool              IsMovable(void)                     const { return this.m_movable;                                                            }
   bool              IsResizable(void)                   const { return this.m_resizable;                                                          }
   bool              IsMain(void)                        const { return this.m_main;                                                               }
   bool              IsFocused(void)                     const { return this.m_focused;                                                            }
   bool              IsAutorepeat(void)                  const { return this.m_autorepeat_flag;                                                    }
   bool              IsScrollable(void)                  const { return this.m_scroll_flag;                                                        }
   bool              IsTrimmed(void)                     const { return this.m_trim_flag;                                                          }
   bool              IsCropped(void)                     const { return this.m_cropped;                                                            }
   string            NameBG(void)                        const { return this.m_background.ChartObjectName();                                       }
   string            NameFG(void)                        const { return this.m_foreground.ChartObjectName();                                       }

метод, устанавливающий флаг, и виртуальный метод, возвращающий флаг того, что элемент расположен полностью за пределами своего контейнера:

//--- Возврат границ объекта с учётом рамки
   int               LimitLeft(void)                     const { return this.ObjectX()+(int)this.m_border_width_lt;                                }
   int               LimitRight(void)                    const { return this.ObjectRight()-(int)this.m_border_width_rt;                            }
   int               LimitTop(void)                      const { return this.ObjectY()+(int)this.m_border_width_up;                                }
   int               LimitBottom(void)                   const { return this.ObjectBottom()-(int)this.m_border_width_dn;                           }
   
//--- Устанавливает объекту флаг (1) перемещаемости, (2) главного объекта, (3) возможности изменения размеров,
//--- (4) автоповтора событий, (5) прокрутки внутри контейнера, (6) обрезки по границам контейнера
   void              SetMovable(const bool flag)               { this.m_movable=flag;                                                              }
   void              SetAsMain(void)                           { this.m_main=true;                                                                 }
   virtual void      SetResizable(const bool flag)             { this.m_resizable=flag;                                                            }
   void              SetAutorepeat(const bool flag)            { this.m_autorepeat_flag=flag;                                                      }
   void              SetScrollable(const bool flag)            { this.m_scroll_flag=flag;                                                          }
   virtual void      SetTrimmered(const bool flag)             { this.m_trim_flag=flag;                                                            }
   void              SetCropped(const bool flag)               { this.m_cropped=flag;                                                              }
   
//--- Возвращает флаг того, что объект расположен за пределами своего контейнера
   virtual bool      IsOutOfContainer(void);
//--- Ограничивает графический объект по размерам контейнера
   virtual bool      ObjectTrim(void);

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

//+------------------------------------------------------------------+
//| CCanvasBase::Возвращает флаг того, что объект                    |
//| расположен за пределами своего контейнера                        |
//+------------------------------------------------------------------+
bool CCanvasBase::IsOutOfContainer(void)
  {
//--- Возвращаем результат проверки, что объект полностью выходит за пределы контейнера
   return(this.Right() <= this.ContainerLimitLeft() || this.X() >= this.ContainerLimitRight() ||
          this.Bottom()<= this.ContainerLimitTop()  || this.Y() >= this.ContainerLimitBottom());
  }

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

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

//+------------------------------------------------------------------+
//| CCanvasBase::Подрезает графический объект по контуру контейнера  |
//+------------------------------------------------------------------+
bool CCanvasBase::ObjectTrim()
  {
//--- Проверяем флаг разрешения обрезки элемента и,
//--- если элемент не должен обрезаться по границам контейнера - возвращаем false
   if(!this.m_trim_flag)
      return false;
//--- Получаем границы контейнера
   int container_left   = this.ContainerLimitLeft();
   int container_right  = this.ContainerLimitRight();
   int container_top    = this.ContainerLimitTop();
   int container_bottom = this.ContainerLimitBottom();
   
//--- Получаем текущие границы объекта
   int object_left   = this.X();
   int object_right  = this.Right();
   int object_top    = this.Y();
   int object_bottom = this.Bottom();

//--- Проверяем, полностью ли объект выходит за пределы контейнера и, если да - скрываем его
   if(this.IsOutOfContainer())
     {
      //--- Устанавливаем флаг, что объект за пределами контейнера
      this.m_cropped=true;
      //--- Скрываем объект и восстанавливаем его размеры
      this.Hide(false);
      if(this.ObjectResize(this.Width(),this.Height()))
         this.BoundResize(this.Width(),this.Height());
      return true;
     }
//--- Объект полностью или частично находится внутри видимой области контейнера
   else
     {
      //--- Снимаем флаг расположения объекта за пределами контейнера
      this.m_cropped=false;
      //--- Если элемент полностью внутри контейнера
      if(object_right<=container_right && object_left>=container_left &&
         object_bottom<=container_bottom && object_top>=container_top)
        {
         //--- Если ширина или высота графического объекта не совпадает с шириной или высотой элемента,
         //--- модифицируем графический объект по размерам элемента и возвращаем true
         if(this.ObjectWidth()!=this.Width() || this.ObjectHeight()!=this.Height())
           {
            if(this.ObjectResize(this.Width(),this.Height()))
               return true;
           }
        }
      //--- Если элемент частично находится в видимой области контейнера
      else
        {
         //--- Если элемент по вертикали полностью находится в видимой области контейнера
         if(object_bottom<=container_bottom && object_top>=container_top)
           {
            //--- Если высота графического объекта не совпадает с высотой элемента,
            //--- модифицируем графический объект по высоте элемента
            if(this.ObjectHeight()!=this.Height())
               this.ObjectResizeH(this.Height());
           }
         else
           {
            //--- Если элемент по горизонтали полностью находится в видимой области контейнера
            if(object_right<=container_right && object_left>=container_left)
              {
               //--- Если ширина графического объекта не совпадает с шириной элемента,
               //--- модифицируем графический объект по ширине элемента
               if(this.ObjectWidth()!=this.Width())
                  this.ObjectResizeW(this.Width());
              }
           }
        }
     }
     
//--- Проверяем выход объекта по горизонтали и вертикали за пределы контейнера
   bool modified_horizontal=false;     // Флаг изменений по горизонтали
   bool modified_vertical  =false;     // Флаг изменений по вертикали
   
//--- Обрезка по горизонтали
   int new_left = object_left;
   int new_width = this.Width();
//--- Если объект выходит за левую границу контейнера
   if(object_left<=container_left)
     {
      int crop_left=container_left-object_left;
      new_left=container_left;
      new_width-=crop_left;
      modified_horizontal=true;
     }
//--- Если объект выходит за правую границу контейнера
   if(object_right>=container_right)
     {
      int crop_right=object_right-container_right;
      new_width-=crop_right;
      modified_horizontal=true;
     }
//--- Если были изменения по горизонтали
   if(modified_horizontal)
     {
      this.ObjectSetX(new_left);
      this.ObjectResizeW(new_width);
     }

//--- Обрезка по вертикали
   int new_top=object_top;
   int new_height=this.Height();
//--- Если объект выходит за верхнюю границу контейнера
   if(object_top<=container_top)
     {
      int crop_top=container_top-object_top;
      new_top=container_top;
      new_height-=crop_top;
      modified_vertical=true;
     }
//--- Если объект выходит за нижнюю границу контейнера 
   if(object_bottom>=container_bottom)
     {
      int crop_bottom=object_bottom-container_bottom;
      new_height-=crop_bottom;
      modified_vertical=true;
     }
//--- Если были изменения по вертикали
   if(modified_vertical)
     {
      this.ObjectSetY(new_top);
      this.ObjectResizeH(new_height);
     }

//--- После рассчётов, объект может быть скрыт, но теперь находится в области контейнера - отображаем его
   this.Show(false);
      
//--- Если объект был изменен, перерисовываем его
   if(modified_horizontal || modified_vertical)
     {
      this.Update(false);
      this.Draw(false);
      return true;
     }
   return false;
  }

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

В методе, помещающем объект на передний план, проверяем флаг, устанавливаемый в методе, рассмотренном выше:

//+------------------------------------------------------------------+
//| CCanvasBase::Помещает объект на передний план                    |
//+------------------------------------------------------------------+
void CCanvasBase::BringToTop(const bool chart_redraw)
  {
   if(this.m_cropped)
      return;
   this.Hide(false);
   this.Show(chart_redraw);
  }

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

У каждого графического элемента есть общий обработчик прокрутки колёсика мышки. Этот общий обработчик вызывает виртуальный метод обработки прокрутки колёсика. В sparam при этом передаётся значение из sparam общего обработчика. Это ошибочно, так как при этом ни один из элементов управления не может определить, что колёсико мышки прокручивается именно над ним. Выход таков: в общем обработчике нам известно имя активного элемента — того, над которым находится курсор, значит, при вызове обработчика прокрутки колёсика в sparam нужно передать имя активного элемента. А в самом обработчике проверить имя элемента и значение sparam. Если они равны, то это именно тот объект, над которым прокручивается колёсико мышки. В общем обработчике событий пропишем это:

//--- Событие прокрутки колёсика мышки
   if(id==CHARTEVENT_MOUSE_WHEEL)
     {
      //--- Если это активный элемент - вызываем его обработчик события прокрутки колёсика
      if(this.IsCurrentActiveElement())
         this.OnWheelEvent(id,lparam,dparam,this.ActiveElementName());  // в sparam передаём имя активного элемента
     }

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

Откроем файл \MQL5\Indicators\Tables\Controls\Controls.mqh — теперь в него будем вносить доработки.

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

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W                50          // Ширина текстовой метки по умолчанию
#define  DEF_LABEL_H                16          // Высота текстовой метки по умолчанию
#define  DEF_BUTTON_W               60          // Ширина кнопки по умолчанию
#define  DEF_BUTTON_H               16          // Высота кнопки по умолчанию
#define  DEF_TABLE_ROW_H            16          // Высота строки таблицы по умолчанию
#define  DEF_TABLE_HEADER_H         20          // Высота заголовка таблицы по умолчанию
#define  DEF_TABLE_COLUMN_MIN_W     12          // Минимальная ширина колонки таблицы
#define  DEF_PANEL_W                80          // Ширина панели по умолчанию
#define  DEF_PANEL_H                80          // Высота панели по умолчанию
#define  DEF_PANEL_MIN_W            60          // Минимальная ширина панели
#define  DEF_PANEL_MIN_H            60          // Минимальная высота панели
#define  DEF_SCROLLBAR_TH           13          // Толщина полосы прокрутки по умолчанию
#define  DEF_THUMB_MIN_SIZE         8           // Минимальная толщина ползунка полосы прокрутки
#define  DEF_AUTOREPEAT_DELAY       500         // Задержка перед запуском автоповтора
#define  DEF_AUTOREPEAT_INTERVAL    100         // Частота автоповторов

#define  DEF_HINT_NAME_TOOLTIP      "HintTooltip"     // Наименование подсказки "тултип"
#define  DEF_HINT_NAME_HORZ         "HintHORZ"        // Наименование подсказки "Двойная горизонтальная стрелка"
#define  DEF_HINT_NAME_VERT         "HintVERT"        // Наименование подсказки "Двойная вертикальная стрелка"
#define  DEF_HINT_NAME_NWSE         "HintNWSE"        // Наименование подсказки "Двойная стрелка сверху-лево" --- низ-право (NorthWest-SouthEast)
#define  DEF_HINT_NAME_NESW         "HintNESW"        // Наименование подсказки "Двойная стрелка снизу-лево" --- верх-право (NorthEast-SouthWest)
#define  DEF_HINT_NAME_SHIFT_HORZ   "HintShiftHORZ"   // Наименование подсказки "Стрелка горизонтального смещения"
#define  DEF_HINT_NAME_SHIFT_VERT   "HintShiftVERT"   // Наименование подсказки "Стрелка вертикального смещения"

Ширина столбца таблицы не может быть менее 12 пикселей — чтобы при уменьшении размеров столбцы не были слишком узкими. Наименования подсказок удобнее задать директивой компилятора и подставлять в качестве имени, так как при необходимости изменения имени подсказки, мы изменяем только директиву, и нет необходимости искать и изменять все вхождения этого имени в разных местах кода. У нас появились два имени для двух новых подсказок — это будут подсказки, появляющиеся при наведении курсора на грань объекта, за которую можно "потянуть" для изменения размеров объекта.

Добавим новое перечисление режимов сортировки столбцов и новые константы перечислений:

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_SORT_BY                       // Сравниваемые свойства
  {
   ELEMENT_SORT_BY_ID   =  BASE_SORT_BY_ID,     // Сравнение по идентификатору элемента
   ELEMENT_SORT_BY_NAME =  BASE_SORT_BY_NAME,   // Сравнение по наименованию элемента
   ELEMENT_SORT_BY_X    =  BASE_SORT_BY_X,      // Сравнение по координате X элемента
   ELEMENT_SORT_BY_Y    =  BASE_SORT_BY_Y,      // Сравнение по координате Y элемента
   ELEMENT_SORT_BY_WIDTH=  BASE_SORT_BY_WIDTH,  // Сравнение по ширине элемента
   ELEMENT_SORT_BY_HEIGHT= BASE_SORT_BY_HEIGHT, // Сравнение по высоте элемента
   ELEMENT_SORT_BY_ZORDER= BASE_SORT_BY_ZORDER, // Сравнение по Z-order элемента
   ELEMENT_SORT_BY_TEXT,                        // Сравнение по тексту элемента
   ELEMENT_SORT_BY_COLOR_BG,                    // Сравнение по цвету фона элемента
   ELEMENT_SORT_BY_ALPHA_BG,                    // Сравнение по прозрачности фона элемента
   ELEMENT_SORT_BY_COLOR_FG,                    // Сравнение по цвету переднего плана элемента
   ELEMENT_SORT_BY_ALPHA_FG,                    // Сравнение по прозрачности переднего плана элемента
   ELEMENT_SORT_BY_STATE,                       // Сравнение по состоянию элемента
   ELEMENT_SORT_BY_GROUP,                       // Сравнение по группе элемента
  };

enum ENUM_TABLE_SORT_MODE                       // Режимы сортировки столбцов таблиц
  {
   TABLE_SORT_MODE_NONE,                        // Сортировка отсутствует
   TABLE_SORT_MODE_ASC,                         // Сортировка по возрастанию
   TABLE_SORT_MODE_DESC,                        // Сортировка по убыванию
  };

enum ENUM_HINT_TYPE                             // Типы подсказок
  {
   HINT_TYPE_TOOLTIP,                           // Тултип
   HINT_TYPE_ARROW_HORZ,                        // Двойная горизонтальная стрелка
   HINT_TYPE_ARROW_VERT,                        // Двойная вертикальная стрелка
   HINT_TYPE_ARROW_NWSE,                        // Двойная стрелка сверху-лево --- низ-право (NorthWest-SouthEast)
   HINT_TYPE_ARROW_NESW,                        // Двойная стрелка снизу-лево --- верх-право (NorthEast-SouthWest)
   HINT_TYPE_ARROW_SHIFT_HORZ,                  // Стрелка горизонтального смещения
   HINT_TYPE_ARROW_SHIFT_VERT,                  // Стрелка вертикального смещения
  };

 В классе рисования изображений CImagePainter объявим два новых метода, рисующих стрелки смещения по горизонтали и вертикали:

//--- Очищает область
   bool              Clear(const int x,const int y,const int w,const int h,const bool update=true);
//--- Рисует закрашенную стрелку (1) вверх, (2) вниз, (3) влево, (4) вправо
   bool              ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Рисует (1) горизонтальную 17х7, (2) вертикальную 7х17 двойную стрелку
   bool              ArrowHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); 
   bool              ArrowVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); 
   
//--- Рисует диагональную (1) сверху-слева --- вниз-вправо, (2) снизу-слева --- вверх-вправо 17х17 двойную стрелку
   bool              ArrowNWSE(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowNESW(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Рисует стрелку смещения 18x18 по (1) горизонтали, (2) вертикали
   bool              ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Рисует (1) отмеченный, (2) неотмеченный CheckBox
   bool              CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Рисует (1) отмеченный, (2) неотмеченный RadioButton
   bool              CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);

За пределами тела класса напишем реализацию объявленных методов.

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

//+------------------------------------------------------------------+
//| CImagePainter::Рисует стрелку 18х18 горизонтального смещения     |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Координаты фигуры
   int arrx[25]={0, 3, 4, 4, 7, 7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10,  7,  7,  4,  4,  3, 0, 0};
   int arry[25]={8, 5, 5, 7, 7, 0,  0,  7,  7,  5,  5,  8,  9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12, 9, 8};
   
//--- Рисуем белую подложку
   this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha));

//--- Рисуем линию стрелок
   this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha));
//--- Рисуем разделительную линию
   this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha));
//--- Рисуем левый треугольник
   this.m_canvas.Line(2,7, 2,10,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(3,6, 3,11,::ColorToARGB(clr,alpha));
//--- Рисуем правый треугольник
   this.m_canvas.Line(14,6, 14,11,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(15,7, 15,10,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }

Метод, рисующий стрелку 18х18 вертикального смещения:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует стрелку 18х18 вертикального смещения       |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Координаты фигуры
   int arrx[25]={0, 7, 7, 5, 5, 8, 9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12,  9,  8,  5,  5,  7,  7,  0, 0};
   int arry[25]={7, 7, 4, 4, 3, 0, 0,  3,  4,  4,  7,  7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10, 7};
   
//--- Рисуем белую подложку
   this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha));

//--- Рисуем разделительную линию
   this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha));
//--- Рисуем линию стрелок
   this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha));
//--- Рисуем верхний треугольник
   this.m_canvas.Line(7,2, 10,2,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(6,3, 11,3,::ColorToARGB(clr,alpha));
//--- Рисуем нижний треугольник
   this.m_canvas.Line(6,14, 11,14,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(7,15, 10,15,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);

   return true;
  }

Оба метода рисуют подсказки со стрелками для горизонтального ( ) и вертикального ( ) смещения грани элемента для изменения его размеров.

В базовом классе графического элемента CElementBase два метода AddHintsArrowed и ShowCursorHint сделаем виртуальными:

//--- Добавляет существующий объект-подсказку в список
   CVisualHint      *AddHint(CVisualHint *obj, const int dx, const int dy);
//--- (1) Добавляет в список, (2) удаляет из списка объекты-подсказки со стрелками
   virtual bool      AddHintsArrowed(void);
   bool              DeleteHintsArrowed(void);
//--- Отображает курсор изменения размеров
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
//--- Обработчик перетаскивания граней и углов элемента
   virtual void      ResizeActionDragHandler(const int x, const int y);

В деструкторе класса очистим список подсказок:

//--- Конструкторы/деструктор
                     CElementBase(void) { this.m_painter.CanvasAssign(this.GetForeground()); this.m_visible_in_container=true; }
                     CElementBase(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CElementBase(void) { this.m_list_hints.Clear(); }

При добавлении объектов в списки, сначала производится поиск точно такого же объекта в списке. При этом, для правильного поиска, списку устанавливается сортировка по сравниваемому свойству. Но изначально список может быть отсортирован по другому свойству. Чтобы не нарушать изначальную сортировку списков, будем запоминать её, затем устанавливать нужную для поиска сортировку, а по окончании — возвращать обратно ранее запомненную.

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

//+------------------------------------------------------------------+
//| CElementBase::Добавляет указанный объект-подсказку в список      |
//+------------------------------------------------------------------+
bool CElementBase::AddHintToList(CVisualHint *obj)
  {
//--- Если передан пустой указатель - сообщаем об этом и возвращаем false
   if(obj==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return false;
     }
//--- Запоминаем метод сортировки списка
   int sort_mode=this.m_list_hints.SortMode();
//--- Устанавливаем списку флаг сортировки по идентификатору
   this.m_list_hints.Sort(ELEMENT_SORT_BY_ID);
//--- Если такого элемента нет в списке,
   if(this.m_list_hints.Search(obj)==NULL)
     {
      //--- возвращаем списку изначальную сортировку и возвращаем результат его добавления в список
      this.m_list_hints.Sort(sort_mode);
      return(this.m_list_hints.Add(obj)>-1);
     }
//--- Возвращаем списку изначальную сортировку
   this.m_list_hints.Sort(sort_mode);
//--- Элемент с таким идентификатором уже есть в списке - возвращаем false
   return false;
  }

В методах, работающих с именами объектов-подсказок, теперь будем использовать имена объектов, ранее указанными директивами:

//+------------------------------------------------------------------+
//| CElementBase::Добавляет в список объекты-подсказки со стрелками  |
//+------------------------------------------------------------------+
bool CElementBase::AddHintsArrowed(void)
  {
//--- Массивы наименований и типов подсказок
   string array[4]={DEF_HINT_NAME_HORZ,DEF_HINT_NAME_VERT,DEF_HINT_NAME_NWSE,DEF_HINT_NAME_NESW};
   
   ENUM_HINT_TYPE type[4]={HINT_TYPE_ARROW_HORZ,HINT_TYPE_ARROW_VERT,HINT_TYPE_ARROW_NWSE,HINT_TYPE_ARROW_NESW};
   
//--- В цикле создаём четыре подсказки со стрелками
   bool res=true;
   for(int i=0;i<(int)array.Size();i++)
      res &=(this.CreateAndAddNewHint(type[i],array[i],0,0)!=NULL);
      
//--- Если были ошибки при создании - возвращаем false
   if(!res)
      return false;
      
//--- В цикле по массиву наименований объектов-подсказок
   for(int i=0;i<(int)array.Size();i++)
     {
      //--- получаем очередной объект по наименованию,
      CVisualHint *obj=this.GetHint(array[i]);
      if(obj==NULL)
         continue;
      //--- скрываем объект и рисуем внешний вид (стрелки в соответствии с типом объекта)
      obj.Hide(false);
      obj.Draw(false);
     }
//--- Всё успешно
   return true;
  }

...

//+------------------------------------------------------------------+
//| CElementBase::Отображает курсор изменения размеров               |
//+------------------------------------------------------------------+
bool CElementBase::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y)
  {
   CVisualHint *hint=NULL;          // Указатель на подсказку
   int hint_shift_x=0;              // Смещение подсказки по X
   int hint_shift_y=0;              // Смещение подсказки по Y
   
//--- В зависимости от расположения курсора на границах элемента
//--- указываем смещения подсказки относительно координат курсора,
//--- отображаем на графике требуемую подсказку и получаем указатель на этот объект
   switch(edge)
     {
      //--- Курсор на правой или левой границе - горизонтальная двойная стрелка
      case CURSOR_REGION_RIGHT         :
      case CURSOR_REGION_LEFT          :
         hint_shift_x=1;
         hint_shift_y=18;
         this.ShowHintArrowed(HINT_TYPE_ARROW_HORZ,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_HORZ);
        break;
    
      //--- Курсор на верхней или нижней границе - вертикальная двойная стрелка
      case CURSOR_REGION_TOP           :
      case CURSOR_REGION_BOTTOM        :
         hint_shift_x=12;
         hint_shift_y=4;
         this.ShowHintArrowed(HINT_TYPE_ARROW_VERT,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_VERT);
        break;
    
      //--- Курсор в левом верхнем или правом нижнем углу - диагональная двойная стрелка от лево-верх до право-низ
      case CURSOR_REGION_LEFT_TOP      :
      case CURSOR_REGION_RIGHT_BOTTOM  :
         hint_shift_x=10;
         hint_shift_y=2;
         this.ShowHintArrowed(HINT_TYPE_ARROW_NWSE,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_NWSE);
        break;
    
      //--- Курсор в левом нижнем или правом верхнем углу - диагональная двойная стрелка от лево-низ до право-верх
      case CURSOR_REGION_LEFT_BOTTOM   :
      case CURSOR_REGION_RIGHT_TOP     :
         hint_shift_x=5;
         hint_shift_y=12;
         this.ShowHintArrowed(HINT_TYPE_ARROW_NESW,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_NESW);
        break;
      
      //--- По умолчанию ничего не делаем
      default: break;
     }

//--- Возвращаем результат корректировки положения подсказки относительно курсора
   return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false);
  }

и т.д.

В классе объекта-подсказки объявим два новых метода, рисующих две новые подсказки:

//+------------------------------------------------------------------+
//| Класс подсказки                                                  |
//+------------------------------------------------------------------+
class CVisualHint : public CButton
  {
protected:
   ENUM_HINT_TYPE    m_hint_type;                              // Тип подсказки

//--- Рисует (1) тултип, (2) горизонтальную, (3) вертикальную стрелку,
//--- стрелки (4) сверху-лево --- низ-право, (5) снизу-лево --- верх-право,
//--- стрелки смещения по (6) горизонтали, (7) вертикали
   void              DrawTooltip(void);
   void              DrawArrHorz(void);
   void              DrawArrVert(void);
   void              DrawArrNWSE(void);
   void              DrawArrNESW(void);
   void              DrawArrShiftHorz(void);
   void              DrawArrShiftVert(void);
   
//--- Инициализация цветов для типа подсказки (1) Tooltip, (2) стрелки
   void              InitColorsTooltip(void);
   void              InitColorsArrowed(void);
   
public:
//--- (1) Устанавливает, (2) возвращает тип подсказки
   void              SetHintType(const ENUM_HINT_TYPE type);
   ENUM_HINT_TYPE    HintType(void)                      const { return this.m_hint_type;             }

//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);   }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);   }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_HINT);           }
   
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Конструкторы/деструктор
                     CVisualHint(void);
                     CVisualHint(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CVisualHint (void) {}
  };

За пределами тела класса напишем их реализацию:

//+------------------------------------------------------------------+
//| CVisualHint::Рисует стрелки смещения по горизонтали              |
//+------------------------------------------------------------------+
void CVisualHint::DrawArrShiftHorz(void)
  {
//--- Очищаем область рисунка
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Рисуем стрелки смещения по горизонтали
   this.m_painter.ArrowShiftHorz(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
  }
//+------------------------------------------------------------------+
//| CVisualHint::Рисует стрелки смещения по вертикали                |
//+------------------------------------------------------------------+
void CVisualHint::DrawArrShiftVert(void)
  {
//--- Очищаем область рисунка
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Рисуем стрелки смещения по горизонтали
   this.m_painter.ArrowShiftVert(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
  }

Добавим обработку новых методов рисования подсказок в методы рисования и установки типа подсказки:

//+------------------------------------------------------------------+
//| CVisualHint::Устанавливает тип подсказки                         |
//+------------------------------------------------------------------+
void CVisualHint::SetHintType(const ENUM_HINT_TYPE type)
  {
//--- Если переданный тип соответствует установленному - уходим
   if(this.m_hint_type==type)
      return;
//--- Устанавливаем новый тип подсказки
   this.m_hint_type=type;
//--- В зависимости от типа подсказки устанавливаем размеры объекта
   switch(this.m_hint_type)
     {
      case HINT_TYPE_ARROW_HORZ        :  this.Resize(17,7);   break;
      case HINT_TYPE_ARROW_VERT        :  this.Resize(7,17);   break;
      case HINT_TYPE_ARROW_NESW        :
      case HINT_TYPE_ARROW_NWSE        :  this.Resize(13,13);  break;
      case HINT_TYPE_ARROW_SHIFT_HORZ  :
      case HINT_TYPE_ARROW_SHIFT_VERT  :  this.Resize(18,18);  break;
      default                          :  break;
     }
//--- Устанавливаем смещение и размеры области изображенеия,
//--- инициализируем цвета по типу подсказки
   this.SetImageBound(0,0,this.Width(),this.Height());
   this.InitColors();
  }
//+------------------------------------------------------------------+
//| CVisualHint::Рисует внешний вид                                  |
//+------------------------------------------------------------------+
void CVisualHint::Draw(const bool chart_redraw)
  {
//--- В зависимости от типа подсказки вызываем соответствующий метод рисования
   switch(this.m_hint_type)
     {
      case HINT_TYPE_ARROW_HORZ        :  this.DrawArrHorz();        break;
      case HINT_TYPE_ARROW_VERT        :  this.DrawArrVert();        break;
      case HINT_TYPE_ARROW_NESW        :  this.DrawArrNESW();        break;
      case HINT_TYPE_ARROW_NWSE        :  this.DrawArrNWSE();        break;
      case HINT_TYPE_ARROW_SHIFT_HORZ  :  this.DrawArrShiftHorz();   break;
      case HINT_TYPE_ARROW_SHIFT_VERT  :  this.DrawArrShiftVert();   break;
      default                          :  this.DrawTooltip();        break;
     }

//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Доработаем класс панели CPanel.

Добавим вспомогательные методы для работы со списками прикреплённых элементов:

//+------------------------------------------------------------------+
//| Класс панели                                                     |
//+------------------------------------------------------------------+
class CPanel : public CLabel
  {
private:
   CElementBase      m_temp_elm;                // Временный объект для поиска элементов
   CBound            m_temp_bound;              // Временный объект для поиска областей
protected:
   CListElm          m_list_elm;                // Список прикреплённых элементов
   CListElm          m_list_bounds;             // Список областей
//--- Добавляет новый элемент в список
   bool              AddNewElement(CElementBase *element);

public:
//--- Возвращает указатель на список (1) прикреплённых элементов, (2) областей
   CListElm         *GetListAttachedElements(void)             { return &this.m_list_elm;                         }
   CListElm         *GetListBounds(void)                       { return &this.m_list_bounds;                      }
   
//--- Возвращает прикреплённый элемент по (1) индексу в списке, (2) идентификатору, (3) назначенному имени объекта
   CElementBase     *GetAttachedElementAt(const uint index)    { return this.m_list_elm.GetNodeAtIndex(index);    }
   CElementBase     *GetAttachedElementByID(const int id);
   CElementBase     *GetAttachedElementByName(const string name);
   
//--- Возвращает количество присоединённых элементов
   int               AttachedElementsTotal(void)         const { return this.m_list_elm.Total();                  }

//--- Возвращает область по (1) индексу в списке, (2) идентификатору, (3) назначенному имени области
   CBound           *GetBoundAt(const uint index)              { return this.m_list_bounds.GetNodeAtIndex(index); }
   CBound           *GetBoundByID(const int id);
   CBound           *GetBoundByName(const string name);
   
//--- Создаёт и добавляет (1) новый, (2) ранее созданный элемент в список
   virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h);
   virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy);
//--- Удаляет указанный элемент
   bool              DeleteElement(const int index)            { return this.m_list_elm.Delete(index);            }

//--- (1) Создаёт и добавляет в список новую область, (2) удаляет указанную область
   CBound           *InsertNewBound(const string name,const int dx,const int dy,const int w,const int h);
   bool              DeleteBound(const int index)              { return this.m_list_bounds.Delete(index);         }
   
//--- (1) Назначает объект на указанную область, (2) отменяет назначение объекта с указанной области
   bool              AssignObjectToBound(const int bound, CBaseObj *object);
   bool              UnassignObjectFromBound(const int bound);

//--- Изменяет размеры объекта
   virtual bool      ResizeW(const int w);
   virtual bool      ResizeH(const int h);
   virtual bool      Resize(const int w,const int h);
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);
   
//--- Виртуальные методы (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(ELEMENT_TYPE_PANEL);                      }
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);
   virtual void      InitColors(void);
   
//--- Устанавливает объекту новые координаты XY
   virtual bool      Move(const int x,const int y);
//--- Смещает объект по осям XY на указанное смещение
   virtual bool      Shift(const int dx,const int dy);
//--- Устанавливает одновременно координаты и размеры элемента
   virtual bool      MoveXYWidthResize(const int x,const int y,const int w,const int h);
   
//--- (1) Скрывает (2) отображает объект на всех периодах графика,
//--- (3) помещает объект на передний план, (4) блокирует, (5) разблокирует элемент,
   virtual void      Hide(const bool chart_redraw);
   virtual void      Show(const bool chart_redraw);
   virtual void      BringToTop(const bool chart_redraw);
   virtual void      Block(const bool chart_redraw);
   virtual void      Unblock(const bool chart_redraw);
   
//--- Выводит в журнал описание объекта
   virtual void      Print(void);
   
//--- Распечатывает список (1) присоединённых объектов, (2) областей
   void              PrintAttached(const uint tab=3);
   void              PrintBounds(void);

//--- Обработчик событий
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
   
//--- Обработчик события таймера
   virtual void      TimerEventHandler(void);
   
//--- Конструкторы/деструктор
                     CPanel(void);
                     CPanel(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CPanel (void) { this.m_list_elm.Clear(); this.m_list_bounds.Clear(); }
  };

В методе изменения ширины элемента ResizeW() есть потенциальная ошибка:

//--- Получаем указатель на базовый элемент и, если он есть, и его тип - контейнер,
//--- проверяем отношение размеров текущего элемента относительно размеров контейнера
//--- для отображения полос прокрутки в контейнере при необходимости
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);

В случае, если контейнером, получаемым методом GetContainer(), не является класс CContainer, то программа завершится по критической ошибке из-за невозможности преобразования типов объектов.

Исправим ситуацию:

//+------------------------------------------------------------------+
//| CPanel::Изменяет ширину объекта                                  |
//+------------------------------------------------------------------+
bool CPanel::ResizeW(const int w)
  {
   if(!this.ObjectResizeW(w))
      return false;
   this.BoundResizeW(w);
   this.SetImageSize(w,this.Height());
   if(!this.ObjectTrim())
     {
      this.Update(false);
      this.Draw(false);
     }
//--- Получаем указатель на базовый элемент и, если он есть, и его тип - контейнер,
//--- проверяем отношение размеров текущего элемента относительно размеров контейнера
//--- для отображения полос прокрутки в контейнере при необходимости
   CContainer  *container=NULL;
   CCanvasBase *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
     {
      container=base;
      container.CheckElementSizes(&this);
     }
      
//--- В цикле по присоединённым элементам обрезаем каждый элемент по границам контейнера
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- Всё успешно
   return true;
  }

Теперь мы можем безошибочно назначить указатель на правильный тип объекта.

В методе добавления нового элемента в список, запомним изначальную сортировку списка, и вернём её после добавления объекта в список:

//+------------------------------------------------------------------+
//| CPanel::Добавляет новый элемент в список                         |
//+------------------------------------------------------------------+
bool CPanel::AddNewElement(CElementBase *element)
  {
//--- Если передан пустой указатель - сообщаем об этом и возвращаем false
   if(element==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return false;
     }
//--- Запоминаем метод сортировки списка
   int sort_mode=this.m_list_elm.SortMode();
//--- Устанавливаем списку флаг сортировки по идентификатору
   this.m_list_elm.Sort(ELEMENT_SORT_BY_ID);
//--- Если такого элемента нет в списке,
   if(this.m_list_elm.Search(element)==NULL)
     {
      //--- возвращаем списку изначальную сортировку и возвращаем результат его добавления в список
      this.m_list_elm.Sort(sort_mode);
      return(this.m_list_elm.Add(element)>-1);
     }
//--- Возвращаем списку изначальную сортировку
   this.m_list_elm.Sort(sort_mode);
//--- Элемент с таким идентификатором уже есть в списке - возвращаем false
   return false;
  }

Доработаем метод, создающий и добавляющий в список новую область:

//+------------------------------------------------------------------+
//| Создаёт и добавляет в список новую область                       |
//+------------------------------------------------------------------+
CBound *CPanel::InsertNewBound(const string name,const int dx,const int dy,const int w,const int h)
  {
//--- Проверяем есть ли в списке область с указанным именем и, если да - сообщаем об этом и возвращаем NULL
   this.m_temp_bound.SetName(name);
//--- Запоминаем метод сортировки списка
   int sort_mode=this.m_list_bounds.SortMode();
//--- Устанавливаем списку флаг сортировки по наименованию
   this.m_list_bounds.Sort(ELEMENT_SORT_BY_NAME);
   if(this.m_list_bounds.Search(&this.m_temp_bound)!=NULL)
     {
      //--- Возвращаем списку изначальную сортировку, сообщаем, что такой объект уже существует и возвращаем NULL
      this.m_list_bounds.Sort(sort_mode);
      ::PrintFormat("%s: Error. An area named \"%s\" is already in the list",__FUNCTION__,name);
      return NULL;
     }
//--- Возвращаем списку изначальную сортировку
   this.m_list_bounds.Sort(sort_mode);
//--- Создаём новый объект-область; при неудаче - сообщаем об этом и возвращаем NULL
   CBound *bound=new CBound(dx,dy,w,h);
   if(bound==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create CBound object",__FUNCTION__);
      return NULL;
     }
//--- Устанавливаем имя области и идентификатор, и возвращаем указатель на объект
   bound.SetName(name);
   bound.SetID(this.m_list_bounds.Total());
//--- Если новый объект не удалось добавить в список - сообщаем об этом, удаляем объект и возвращаем NULL
   if(this.m_list_bounds.Add(bound)==-1)
     {
      ::PrintFormat("%s: Error. Failed to add CBound object to list",__FUNCTION__);
      delete bound;
      return NULL;
     }
   return bound;
  }

В классе есть два метода, которые были объявлены, но не были реализованы. Исправим.

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

//+------------------------------------------------------------------+
//| CPanel::Возвращает область по идентификатору                     |
//+------------------------------------------------------------------+
CBound *CPanel::GetBoundByID(const int id)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL && bound.ID()==id)
         return bound;
     }
   return NULL;
  }

В простом цикле по объектам областей элемента ищем область с указанным идентификатором и возвращаем указатель на найденный объект.

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

//+------------------------------------------------------------------+
//| CPanel::Возвращает область по назначенному имени области         |
//+------------------------------------------------------------------+
CBound *CPanel::GetBoundByName(const string name)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL && bound.Name()==name)
         return bound;
     }
   return NULL;
  }

В простом цикле по объектам областей элемента ищем область с указанным именем и возвращаем указатель на найденный объект.

Напишем реализацию двух объявленных методов.

Метод, назначающий объект на указанную область:

//+------------------------------------------------------------------+
//| CPanel::Назначает объект на указанную область                    |
//+------------------------------------------------------------------+
bool CPanel::AssignObjectToBound(const int bound,CBaseObj *object)
  {
   CBound *bound_obj=this.GetBoundAt(bound);
   if(bound_obj==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound);
      return false;
     }
   bound_obj.AssignObject(object);
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| CPanel::Отменяет назначение объекта с указанной области          |
//+------------------------------------------------------------------+
bool CPanel::UnassignObjectFromBound(const int bound)
  {
   CBound *bound_obj=this.GetBoundAt(bound);
   if(bound_obj==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound);
      return false;
     }
   bound_obj.UnassignObject();
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| CScrollBarThumbH::Обработчик прокрутки колёсика                  |
//+------------------------------------------------------------------+
void CScrollBarThumbH::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Получаем указатель на базовый объект (элемент управления "горизонтальная полоса прогрутки")
   CCanvasBase *base_obj=this.GetContainer();
   
//--- Получаем имя главного объекта в иерархии по значению в sparam
   string array_names[];
   string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : "");
   
//--- Если главный объект в иерархии не наш - уходим
   if(::StringFind(this.NameFG(),name_main)!=0)
      return;
      
//--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим
   if(!this.IsMovable() || base_obj==NULL)
      return;
   
//--- Получаем ширину базового объекта и рассчитываем границы пространства для ползунка
   int base_w=base_obj.Width();
   int base_left=base_obj.X()+base_obj.Height();
   int base_right=base_obj.Right()-base_obj.Height()+1;
   
//--- Задаём направление смещения в зависимости от направления вращения колёсика мышки
   int dx=(dparam<0 ? 2 : dparam>0 ? -2 : 0);
   if(dx==0)
      dx=(int)lparam;

//--- Если при смещении ползунок выйдет за левый край своей области - устанавливаем его на левый край
   if(dx<0 && this.X()+dx<=base_left)
      this.MoveX(base_left);
//--- иначе, если при смещении ползунок выйдет за правый край своей области - позиционируем его по правому краю
   else if(dx>0 && this.Right()+dx>=base_right)
      this.MoveX(base_right-this.Width());
//--- Иначе, если ползунок в пределах своей области - смещаем его на величину смещения
   else
     {
      this.ShiftX(dx);
     }

//--- Рассчитываем позицию ползунка
   int thumb_pos=this.X()-base_left;
   
//--- Получаем координаты курсора
   int x=CCommonManager::GetInstance().CursorX();
   int y=CCommonManager::GetInstance().CursorY();
   
//--- Если курсор попадает на ползунок - меняем цвет на "В фокусе",
   if(this.Contains(x,y))
      this.OnFocusEvent(id,lparam,dparam,sparam);
//--- иначе - возвращаем цвет на "По умолчанию"
   else
      this.OnReleaseEvent(id,lparam,dparam,sparam);
      
//--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG());
//--- Перерисовываем график
   if(this.m_chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| CScrollBarThumbV::Обработчик прокрутки колёсика                  |
//+------------------------------------------------------------------+
void CScrollBarThumbV::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Получаем указатель на базовый объект (элемент управления "вертикальная полоса прогрутки")
   CCanvasBase *base_obj=this.GetContainer();
   
//--- Получаем имя главного объекта в иерархии по значению в sparam
   string array_names[];
   string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : "");
   
//--- Если главный объект в иерархии не наш - уходим
   if(::StringFind(this.NameFG(),name_main)!=0)
      return;
      
//--- Если для ползунка не установлен флаг перемещаемости, либо указатель на базовый объект не получен - уходим
   if(!this.IsMovable() || base_obj==NULL)
      return;
   
//--- Получаем высоту базового объекта и рассчитываем границы пространства для ползунка
   int base_h=base_obj.Height();
   int base_top=base_obj.Y()+base_obj.Width();
   int base_bottom=base_obj.Bottom()-base_obj.Width()+1;
   
//--- Задаём направление смещения в зависимости от направления вращения колёсика мышки
   int dy=(dparam<0 ? 2 : dparam>0 ? -2 : 0);
   if(dy==0)
      dy=(int)lparam;

//--- Если при смещении ползунок выйдет за верхний край своей области - устанавливаем его на верхний край
   if(dy<0 && this.Y()+dy<=base_top)
      this.MoveY(base_top);
//--- иначе, если при смещении ползунок выйдет за нижний край своей области - позиционируем его по нижнему краю
   else if(dy>0 && this.Bottom()+dy>=base_bottom)
      this.MoveY(base_bottom-this.Height());
//--- Иначе, если ползунок в пределах своей области - смещаем его на величину смещения
   else
     {
      this.ShiftY(dy);
     }

//--- Рассчитываем позицию ползунка
   int thumb_pos=this.Y()-base_top;
   
//--- Получаем координаты курсора
   int x=CCommonManager::GetInstance().CursorX();
   int y=CCommonManager::GetInstance().CursorY();
   
//--- Если курсор попадает на ползунок - меняем цвет на "В фокусе",
   if(this.Contains(x,y))
      this.OnFocusEvent(id,lparam,dparam,sparam);
//--- иначе - возвращаем цвет на "По умолчанию"
   else
      this.OnReleaseEvent(id,lparam,dparam,sparam);
      
//--- Отправляем пользовательское событие на график с позицией ползунка в lparam и именем объекта в sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG());
//--- Перерисовываем график
   if(this.m_chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

//+-------------------------------------------------------------------+
//|CContainer::Смещает содержимое по горизонтали на указанное значение|
//+-------------------------------------------------------------------+
bool CContainer::ContentShiftHorz(const int value)
  {
//--- Получаем указатель на содержимое контейнера
   CElementBase *elm=this.GetAttachedElement();
   if(elm==NULL)
      return false;
   
//--- Для элемента CTableView получаем заголовок таблицы
   CElementBase     *elm_container=elm.GetContainer();
   CTableHeaderView *table_header=NULL;
   if(elm_container!=NULL && ::StringFind(elm.Name(),"Table")==0)
     {
      CElementBase *obj=elm_container.GetContainer();
      if(obj!=NULL && obj.Type()==ELEMENT_TYPE_TABLE_VIEW)
        {
         CTableView *table_view=obj;
         table_header=table_view.GetHeader();
        }
     }
//--- Рассчитываем величину смещения по положению ползунка
   int content_offset=this.CalculateContentOffsetHorz(value);

//--- Сдвигаем заголовок
   bool res=true;
   if(table_header!=NULL)
     {
      res &=table_header.MoveX(this.X()-content_offset);
     }
     
//--- Возвращаем результат сдвига содержимого на рассчитанную величину
   res &=elm.MoveX(this.X()-content_offset);
   return res;
  }

В методе, возвращающем тип элемента, отправившего событие, в классе объекта контейнера был неправильно выполнен поиск базового элемента:

//--- Получаем имена всех элементов в иерархии (при ошибке - возвращаем -1)
   string names[]={};
   int total = GetElementNames(name,"_",names);
   if(total==WRONG_VALUE)
      return WRONG_VALUE;
      
//--- Если имя базового элемента в иерархии не совпадает с именем контейнера, то это не наше событие - уходим
   string base_name=names[0];
   if(base_name!=this.NameFG())
      return WRONG_VALUE;

Не всегда базовый объект находится самым первым в иерархии графических элементов контейнера. Могут быть ситуации многократного вложения одного элемента в другой, и тогда для последних элементов в иерархии их базовый объект не будет первым в списке имён всех вложенных элементов контейнера. Выполним поиск базового объекта правильно:

//+------------------------------------------------------------------+
//| Возвращает тип элемента, отправившего событие                    |
//+------------------------------------------------------------------+
ENUM_ELEMENT_TYPE CContainer::GetEventElementType(const string name)
  {
//--- Получаем имена всех элементов в иерархии (при ошибке - возвращаем -1)
   string names[]={};
   int total = GetElementNames(name,"_",names);
   if(total==WRONG_VALUE)
      return WRONG_VALUE;
   
//--- Найдём в массиве наименование контейнера, ближайшее к имени элемента с событием
   int    cntr_index=-1;      // Индекс наименования контейнера в массиве имён в иерархии элементов
   string cntr_name="";       // Наименование контейнера в массиве имён в иерархии элементов
   
//--- Ищем в цикле самое первое с конца вхождение подстроки "CNTR"
   for(int i=total-1;i>=0;i--)
     {
      if(::StringFind(names[i],"CNTR")==0)
        {
         cntr_name=names[i];
         cntr_index=i;
         break;
        }
     }
//--- Если наименование контейнера не найдено в массиве (индекс равен -1) - возвращаем -1
   if(cntr_index==WRONG_VALUE)
      return WRONG_VALUE;
   
//--- Если в имени элемента нет подстроки с именем базового элемента, то это не наше событие - уходим
   string base_name=names[cntr_index];
   if(::StringFind(this.NameFG(),base_name)==WRONG_VALUE)
      return WRONG_VALUE;

//--- События, пришедшие не от скроллбаров, пропускаем
   string check_name=::StringSubstr(names[cntr_index+1],0,4);
   if(check_name!="SCBH" && check_name!="SCBV")
      return WRONG_VALUE;
      
//--- Получаем имя элемента, от которого пришло событие и инициализируем тип элемента
   string elm_name=names[names.Size()-1];
   ENUM_ELEMENT_TYPE type=WRONG_VALUE;
   
//--- Проверяем и записываем тип элемента
//--- Кнопка со стрелкой вверх
   if(::StringFind(elm_name,"BTARU")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_UP;
//--- Кнопка со стрелкой вниз
   else if(::StringFind(elm_name,"BTARD")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_DOWN;
//--- Кнопка со стрелкой влево
   else if(::StringFind(elm_name,"BTARL")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_LEFT;
//--- Кнопка со стрелкой вправо
   else if(::StringFind(elm_name,"BTARR")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_RIGHT;
//--- Ползунок горизонтальной полосы прокрутки
   else if(::StringFind(elm_name,"THMBH")==0)
      type=ELEMENT_TYPE_SCROLLBAR_THUMB_H;
//--- Ползунок вертикальной полосы прокрутки
   else if(::StringFind(elm_name,"THMBV")==0)
      type=ELEMENT_TYPE_SCROLLBAR_THUMB_V;
//--- Элемент управления ScrollBarHorisontal
   else if(::StringFind(elm_name,"SCBH")==0)
      type=ELEMENT_TYPE_SCROLLBAR_H;
//--- Элемент управления ScrollBarVertical
   else if(::StringFind(elm_name,"SCBV")==0)
      type=ELEMENT_TYPE_SCROLLBAR_V;
      
//--- Возвращаем тип элемента
   return type;
  }

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

Объявим новые переменные и методы:

//+------------------------------------------------------------------+
//| Класс визуального представления ячейки таблицы                   |
//+------------------------------------------------------------------+
class CTableCellView : public CBoundedObj
  {
protected:
   CTableCell       *m_table_cell_model;                       // Указатель на модель ячейки
   CImagePainter    *m_painter;                                // Указатель на объект рисования
   CTableRowView    *m_element_base;                           // Указатель на базовый элемент (строка таблицы)
   CCanvas          *m_background;                             // Указатель на канвас фона
   CCanvas          *m_foreground;                             // Указатель на канвас переднего плана
   int               m_index;                                  // Индекс в списке ячеек
   ENUM_ANCHOR_POINT m_text_anchor;                            // Точка привязки текста (выравнивание в ячейке)
   int               m_text_x;                                 // Координата X текста (смещение относительно левой границы области объекта)
   int               m_text_y;                                 // Координата Y текста (смещение относительно верхней границы области объекта)
   ushort            m_text[];                                 // Текст
   color             m_fore_color;                             // Цвет переднего плана
   
//--- Возвращает смещения начальных координат рисования на холсте относительно канваса и координат базового элемента
   int               CanvasOffsetX(void)     const { return(this.m_element_base.ObjectX()-this.m_element_base.X());  }
   int               CanvasOffsetY(void)     const { return(this.m_element_base.ObjectY()-this.m_element_base.Y());  }
   
//--- Возвращает скорректированную координату точки на холсте с учётом смещения холста относительно базового элемента
   int               AdjX(const int x)                            const { return(x-this.CanvasOffsetX());            }
   int               AdjY(const int y)                            const { return(y-this.CanvasOffsetY());            }

//--- Возвращает координаты X и Y текста в зависимости от точки привязки
   bool              GetTextCoordsByAnchor(int &x, int &y, int &dir_x, int dir_y);

//--- Возвращает указатель на контейнер панели строк таблицы
   CContainer       *GetRowsPanelContainer(void);
   
public:
//--- Возвращает указатель на назначенный канвас (1) фона, (2) переднего плана
   CCanvas          *GetBackground(void)                                { return this.m_background;                  }
   CCanvas          *GetForeground(void)                                { return this.m_foreground;                  }

//--- Получение границ родительского объекта-контейнера
   int               ContainerLimitLeft(void)   const { return(this.m_element_base==NULL ? this.X()      :  this.m_element_base.LimitLeft());   }
   int               ContainerLimitRight(void)  const { return(this.m_element_base==NULL ? this.Right()  :  this.m_element_base.LimitRight());  }
   int               ContainerLimitTop(void)    const { return(this.m_element_base==NULL ? this.Y()      :  this.m_element_base.LimitTop());    }
   int               ContainerLimitBottom(void) const { return(this.m_element_base==NULL ? this.Bottom() :  this.m_element_base.LimitBottom()); }

//--- Возвращает флаг того, что объект расположен за пределами своего контейнера
   virtual bool      IsOutOfContainer(void);

//--- (1) Устанавливает, (2) возвращает текст ячейки
   void              SetText(const string text)                         { ::StringToShortArray(text,this.m_text);    }
   string            Text(void)                                   const { return ::ShortArrayToString(this.m_text);  }

//--- (1) Устанавливает, (2) возвращает цвет текста ячейки
   void              SetForeColor(const color clr)                      { this.m_fore_color=clr;                     }
   color             ForeColor(void)                              const { return this.m_fore_color;                  }

//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Устанавливает, (2) возвращает индекс ячейки
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }

//--- (1) Устанавливает, (2) возвращает смещение текста по оси X
   void              SetTextShiftX(const int shift)                     { this.m_text_x=shift;                       }
   int               TextShiftX(void)                             const { return this.m_text_x;                      }
   
//--- (1) Устанавливает, (2) возвращает смещение текста по оси Y
   void              SetTextShiftY(const int shift)                     { this.m_text_y=shift;                       }
   int               TextShiftY(void)                             const { return this.m_text_y;                      }
   
//--- (1) Устанавливает, (2) возвращает точку привязки текста
   void              SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw);
   int               TextAnchor(void)                             const { return this.m_text_anchor;                 }
   
//--- Устанавливает точку привязки и смещения текста
   void              SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw);

//--- Назначает базовый элемент (строку таблицы)
   void              RowAssign(CTableRowView *base_element);
   
//--- (1) Назначает, (2) возвращает модель ячейки
   bool              TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h);
   CTableCell       *GetTableCellModel(void)                            { return this.m_table_cell_model;            }

//--- Распечатывает в журнале назначенную модель ячейки
   void              TableCellModelPrint(void);
   
//--- (1) Заливает объект цветом фона, (2) обновляет объект для отображения изменений, (3) рисует внешний вид
   virtual void      Clear(const bool chart_redraw);
   virtual void      Update(const bool chart_redraw);
   virtual void      Draw(const bool chart_redraw);
   
//--- Выводит текст
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0)const { return CBaseObj::Compare(node,mode);       }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_CELL_VIEW);      }
   
//--- Инициализация объекта класса
   void              Init(const string text);
   
//--- Возвращает описание объекта
   virtual string    Description(void);
   
//--- Конструкторы/деструктор
                     CTableCellView(void);
                     CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h);
                    ~CTableCellView (void){}
  };

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

//+------------------------------------------------------------------+
//| CTableCellView::Назначает строку, канвасы фона и переднего плана |
//+------------------------------------------------------------------+
void CTableCellView::RowAssign(CTableRowView *base_element)
  {
   if(base_element==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return;
     }
   this.m_element_base=base_element;
   this.m_background=this.m_element_base.GetBackground();
   this.m_foreground=this.m_element_base.GetForeground();
   this.m_painter=this.m_element_base.Painter();
   this.m_fore_color=this.m_element_base.ForeColor();
  }

В метод, возвращающий координаты X и Y текста в зависимости от точки привязки, добавим переменные, в которые будут записываться знаки направления смещения (+1 / -1) по осям X и Y:

//+------------------------------------------------------------------+
//| CTableCellView::Возвращает координаты X и Y текста               |
//| в зависимости от точки привязки                                  |
//+------------------------------------------------------------------+
bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y, int &dir_x,int dir_y)
  {
//--- Получаем размеры текста в ячейке
   int text_w=0, text_h=0;
   this.m_foreground.TextSize(this.Text(),text_w,text_h);
   if(text_w==0 || text_h==0)
      return false;
//--- В зависимости от точки привязки текста в ячейке
//--- рассчитываем его начальные координаты (верхний левый угол)
   switch(this.m_text_anchor)
     {
      //--- Точка привязки слева по центру
      case ANCHOR_LEFT :
        x=0;
        y=(this.Height()-text_h)/2;
        dir_x=1;
        dir_y=1;
        break;
      //--- Точка привязки в левом нижнем углу
      case ANCHOR_LEFT_LOWER :
        x=0;
        y=this.Height()-text_h;
        dir_x= 1;
        dir_y=-1;
        break;
      //--- Точка привязки снизу по центру
      case ANCHOR_LOWER :
        x=(this.Width()-text_w)/2;
        y=this.Height()-text_h;
        dir_x= 1;
        dir_y=-1;
        break;
      //--- Точка привязки в правом нижнем углу
      case ANCHOR_RIGHT_LOWER :
        x=this.Width()-text_w;
        y=this.Height()-text_h;
        dir_x=-1;
        dir_y=-1;
        break;
      //--- Точка привязки справа по центру
      case ANCHOR_RIGHT :
        x=this.Width()-text_w;
        y=(this.Height()-text_h)/2;
        dir_x=-1;
        dir_y= 1;
        break;
      //--- Точка привязки в правом верхнем углу
      case ANCHOR_RIGHT_UPPER :
        x=this.Width()-text_w;
        y=0;
        dir_x=-1;
        dir_y= 1;
        break;
      //--- Точка привязки сверху по центру
      case ANCHOR_UPPER :
        x=(this.Width()-text_w)/2;
        y=0;
        dir_x=1;
        dir_y=1;
        break;
      //--- Точка привязки строго по центру объекта
      case ANCHOR_CENTER :
        x=(this.Width()-text_w)/2;
        y=(this.Height()-text_h)/2;
        dir_x=1;
        dir_y=1;
        break;
      //--- Точка привязки в левом верхнем углу
      //---ANCHOR_LEFT_UPPER
      default:
        x=0;
        y=0;
        dir_x=1;
        dir_y=1;
        break;
     }
   return true;
  }

В методе, рисующем внешний вид, теперь используем полученные значения в качестве множителя для смещения текста относительно осей X и Y, а также не рисуем ячейку, если она находится за пределами своего контейнера:

//+------------------------------------------------------------------+
//| CTableCellView::Рисует внешний вид                               |
//+------------------------------------------------------------------+
void CTableCellView::Draw(const bool chart_redraw)
  {
//--- Если ячейка за пределами контейнера строк таблицы - уходим
   if(this.IsOutOfContainer())
      return;
      
//--- Получаем координаты текста и направление смещения в зависимости от точки привязки
   int text_x=0, text_y=0;
   int dir_horz=0, dir_vert=0;
   if(!this.GetTextCoordsByAnchor(text_x,text_y,dir_horz,dir_vert))
      return;
//--- Корректируем координаты текста
   int x=this.AdjX(this.X()+text_x);
   int y=this.AdjY(this.Y()+text_y);
   
//--- Устанавливаем координаты разделительной линии
   int x1=this.AdjX(this.X());
   int x2=this.AdjX(this.X());
   int y1=this.AdjY(this.Y());
   int y2=this.AdjY(this.Bottom());
   
//--- Выводим текст на канвасе переднего плана с учётом направления смещения без обновления графика
   this.DrawText(x+this.m_text_x*dir_horz,y+this.m_text_y*dir_vert,this.Text(),false);
   
//--- Если это не крайняя справа ячейка - рисуем у ячейки справа вертикальную разделительную полосу
   if(this.m_element_base!=NULL && this.Index()<this.m_element_base.CellsTotal()-1)
     {
      int line_x=this.AdjX(this.Right());
      this.m_background.Line(line_x,y1,line_x,y2,::ColorToARGB(this.m_element_base.BorderColor(),this.m_element_base.AlphaBG()));
     }
//--- Обновляем канвас фона с указанным флагом перерисовки графика
   this.m_background.Update(chart_redraw);
  }

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

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

//+------------------------------------------------------------------+
//| CTableCellView::Возвращает указатель                             |
//| на контейнер панели строк таблицы                                |
//+------------------------------------------------------------------+
CContainer *CTableCellView::GetRowsPanelContainer(void)
  {
//--- Проверяем строку
   if(this.m_element_base==NULL)
      return NULL;
//--- Получаем панель для размещения строк
   CPanel *rows_area=this.m_element_base.GetContainer();
   if(rows_area==NULL)
      return NULL;
//--- Возвращаем контейнер панели со строками
   return rows_area.GetContainer();
  }

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

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

//+------------------------------------------------------------------+
//| CTableCellView::Возвращает флаг того, что объект                 |
//| расположен за пределами своего контейнера                        |
//+------------------------------------------------------------------+
bool CTableCellView::IsOutOfContainer(void)
  {
//--- Проверяем строку
   if(this.m_element_base==NULL)
      return false;

//--- Получаем контейнер панели со строками
   CContainer *container=this.GetRowsPanelContainer();
   if(container==NULL)
      return false;
  
//--- Получаем границы ячейки по всем сторонам
   int cell_l=this.m_element_base.X()+this.X();
   int cell_r=this.m_element_base.X()+this.Right();
   int cell_t=this.m_element_base.Y()+this.Y();
   int cell_b=this.m_element_base.Y()+this.Bottom();
   
//--- Возвращаем результат проверки, что объект полностью выходит за пределы контейнера
   return(cell_r <= container.X() || cell_l >= container.Right() || cell_b <= container.Y() || cell_t >= container.Bottom());
  }

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

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

Объявим новые методы и в деструкторе очистим список ячеек:

//+------------------------------------------------------------------+
//| Класс визуального представления строки таблицы                   |
//+------------------------------------------------------------------+
class CTableRowView : public CPanel
  {
protected:
   CTableCellView    m_temp_cell;                                    // Временный объект ячейки для поиска
   CTableRow        *m_table_row_model;                              // Указатель на модель строки
   CListElm          m_list_cells;                                   // Список ячеек
   int               m_index;                                        // Индекс в списке строк
//--- Создаёт и добавляет в список новый объект представления ячейки
   CTableCellView   *InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h);
//--- Удаляет указанную область строки и ячейку с соответствующим индексом
   bool              BoundCellDelete(const int index);
   
public:
//--- Возвращает (1) список, (2) количество ячеек, (3) ячейку
   CListElm         *GetListCells(void)                                 { return &this.m_list_cells;                       }
   int               CellsTotal(void)                             const { return this.m_list_cells.Total();                }
   CTableCellView   *GetCellView(const uint index)                      { return this.m_list_cells.GetNodeAtIndex(index);  }
   
//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                       }
//--- (1) Устанавливает, (2) возвращает индекс строки
   void              SetIndex(const int index)                          { this.SetID(index);                               }
   int               Index(void)                                  const { return this.m_index;                             }

//--- (1) Устанавливает, (2) возвращает модель строки
   bool              TableRowModelAssign(CTableRow *row_model);
   CTableRow        *GetTableRowModel(void)                             { return this.m_table_row_model;                   }
//--- Обновляет строку с обновлённой моделью
   bool              TableRowModelUpdate(CTableRow *row_model);

//--- Перерассчитывает области ячеек
   bool              RecalculateBounds(CListElm *list_bounds);

//--- Распечатывает в журнале назначенную модель строки
   void              TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0)const { return CLabel::Compare(node,mode);               }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_ROW_VIEW);             }
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);
   virtual void      InitColors(void);

//--- Конструкторы/деструктор
                     CTableRowView(void);
                     CTableRowView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableRowView (void){ this.m_list_cells.Clear(); }
  };

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

//+------------------------------------------------------------------+
//| CTableRowView::Создаёт и добавляет в список                      |
//| новый объект представления ячейки                                |
//+------------------------------------------------------------------+
CTableCellView *CTableRowView::InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h)
  {
//--- Проверяем есть ли в списке объект с указанным идентификатором и, если да - сообщаем об этом и возвращаем NULL
   this.m_temp_cell.SetIndex(index);
//--- Запоминаем метод сортировки списка
   int sort_mode=this.m_list_cells.SortMode();
//--- Устанавливаем списку флаг сортировки по идентификатору
   this.m_list_cells.Sort(ELEMENT_SORT_BY_ID);
   if(this.m_list_cells.Search(&this.m_temp_cell)!=NULL)
     {
      //--- Возвращаем списку изначальную сортировку, сообщаем, что такой объект уже существует и возвращаем NULL
      this.m_list_cells.Sort(sort_mode);
      ::PrintFormat("%s: Error. The TableCellView object with index %d is already in the list",__FUNCTION__,index);
      return NULL;
     }
//--- Возвращаем списку изначальную сортировку
   this.m_list_cells.Sort(sort_mode);
//--- Создаём имя объекта ячейки
   string name="TableCellView"+(string)this.Index()+"x"+(string)index;

//--- Создаём новый объект TableCellView; при неудаче - сообщаем об этом и возвращаем NULL
   CTableCellView *cell_view=new CTableCellView(index,name,text,dx,dy,w,h);
   if(cell_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create CTableCellView object",__FUNCTION__);
      return NULL;
     }
//--- Если новый объект не удалось добавить в список - сообщаем об этом, удаляем объект и возвращаем NULL
   if(this.m_list_cells.Add(cell_view)==-1)
     {
      ::PrintFormat("%s: Error. Failed to add CTableCellView object to list",__FUNCTION__);
      delete cell_view;
      return NULL;
     }
//--- Назначаем базовый элемент (строку) и возвращаем указатель на объект
   cell_view.RowAssign(&this);
   return cell_view;
  }

В методе установки модели строки ширину столбцов будем рассчитывать по ширине панели строк, а не самой строки, так как строка немного уже панели. Ширина заголовка таблицы также равна ширине панели строк, поэтому для совпадения ширины ячеек и столбцов используем ширину панели:

//+------------------------------------------------------------------+
//| CTableRowView::Устанавливает модель строки                       |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelAssign(CTableRow *row_model)
  {
//--- Если передан пустой объект - сообщаем об этом и возвращаем false
   if(row_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Если в переданной модели строки нет ни одной ячейки - сообщаем об этом и возвращаем false
   int total=(int)row_model.CellsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Сохраняем указатель на переданную модель строки
   this.m_table_row_model=row_model;
//--- рассчитываем ширину ячейки по ширине панели строк
   CCanvasBase *base=this.GetContainer();
   int w=(base!=NULL ? base.Width() : this.Width());
   int cell_w=(int)::fmax(::round((double)w/(double)total),DEF_TABLE_COLUMN_MIN_W);

//--- В цикле по количеству ячеек в модели строки
   for(int i=0;i<total;i++)
     {
      //--- получаем модель очередной ячейки,
      CTableCell *cell_model=this.m_table_row_model.GetCell(i);
      if(cell_model==NULL)
         return false;
      //--- рассчитываем координату и создаём имя для области ячейки
      int x=cell_w*i;
      string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i;
      //--- Создаём новую область ячейки
      CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height());
      if(cell_bound==NULL)
         return false;
      //--- Создаём новый объект визуального представления ячейки
      CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height());
      if(cell_view==NULL)
         return false;
      //--- На текущую область ячейки назначаем соответствующий объект визуального представления ячейки
      cell_bound.AssignObject(cell_view);
     }
//--- Всё успешно
   return true;
  }

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

Создадим для этого специальный метод.

Метод, обновляющий строку с обновлённой моделью:

//+------------------------------------------------------------------+
//| CTableRowView::Обновляет строку с обновлённой моделью            |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelUpdate(CTableRow *row_model)
  {
//--- Если передан пустой объект - сообщаем об этом и возвращаем false
   if(row_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Если в переданной модели строки нет ни одной ячейки - сообщаем об этом и возвращаем false
   int total_model=(int)row_model.CellsTotal(); // Количество ячеек в модели строки
   if(total_model==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Сохраняем указатель на переданную модель строки
   this.m_table_row_model=row_model;

//--- Рассчитываем ширину ячейки по ширине панели строк
   CCanvasBase *base=this.GetContainer();
   int w=(base!=NULL ? base.Width() : this.Width());
   int cell_w=(int)::fmax(::round((double)w/(double)total_model),DEF_TABLE_COLUMN_MIN_W);
   
   CBound *cell_bound=NULL;
   int total_bounds=this.m_list_bounds.Total(); // Количество областей
   int diff=total_model-total_bounds;           // Разница между количеством областей в строке и ячеек в модели строки
   
//--- Если в модели больше ячеек, чем областей в списке - создадим недостающие области и ячейки в конце списков
   if(diff>0)
     {
      //--- В цикле по количеству недостающих областей
      for(int i=total_bounds;i<total_bounds+diff;i++)
        {
         //--- создаём и добавляем в список diff количество областей ячеек строки.
         //--- Получаем модель очередной ячейки,
         CTableCell *cell_model=this.m_table_row_model.GetCell(i);
         if(cell_model==NULL)
            return false;
         //--- рассчитываем координату и создаём имя для области ячейки
         int x=cell_w*i;
         string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i;
         //--- Создаём новую область ячейки
         CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height());
         if(cell_bound==NULL)
            return false;
            
         //--- Создаём новый объект визуального представления ячейки
         CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height());
         if(cell_view==NULL)
            return false;
        }
     }
 
//--- Если в списке больше областей, чем ячеек в модели - удалим лишние области в конце списка
   if(diff<0)
     {
      int  start=total_bounds-1;
      int  end=start-diff;
      bool res=true;
      for(int i=start;i>end;i--)
        {
         if(!this.BoundCellDelete(i))
            return false;
        }
     }
   
//--- В цикле по количеству ячеек в модели строки
   for(int i=0;i<total_model;i++)
     {
      //--- получаем модель очередной ячейки,
      CTableCell *cell_model=this.m_table_row_model.GetCell(i);
      if(cell_model==NULL)
         return false;
      
      //--- рассчитываем координату ячейки
      int x=cell_w*i;
      //--- Получаем очередную область ячейки
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         return false;
      
      //--- Получаем из списка объект визуального представления ячейки
      CTableCellView *cell_view=this.m_list_cells.GetNodeAtIndex(i);
      if(cell_view==NULL)
         return false;
      
      //--- На текущую область ячейки назначаем соответствующий объект визуального представления ячейки и его текст
      cell_bound.AssignObject(cell_view);
      cell_view.SetText(cell_model.Value());
     }
//--- Всё успешно
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Удаляет указанную область строки                  |
//| и ячейку с соответствующим индексом                              |
//+------------------------------------------------------------------+
bool CTableRowView::BoundCellDelete(const int index)
  {
   if(!this.m_list_cells.Delete(index))
      return false;
   return this.m_list_bounds.Delete(index);
  }

Если объект ячейки успешно удалён из списка — удаляем и соответствующую область из списка областей.

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

//+------------------------------------------------------------------+
//| CTableRowView::Рисует внешний вид                                |
//+------------------------------------------------------------------+
void CTableRowView::Draw(const bool chart_redraw)
  {
//--- Если строка за пределами контейнера - уходим
   if(this.IsOutOfContainer())
      return;

//--- Заливаем объект цветом фона, рисуем линию строки и обновляем канвас фона
   this.Fill(this.BackColor(),false);
   this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
  
//--- Рисуем ячейки строки
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Получаем область очередной ячейки
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Из области ячейки получаем присоединённый объект ячейки
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      //--- Рисуем визуальное представление ячейки
      if(cell_view!=NULL)
         cell_view.Draw(false);
     }
//--- Обновляем канвасы фона и переднего плана с указанным флагом перерисовки графика
   this.Update(chart_redraw);
  }

Строка, которая не находится в видимой области контейнера, рисоваться не должна.

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Перерассчитывает области ячеек                    |
//+------------------------------------------------------------------+
bool CTableRowView::RecalculateBounds(CListElm *list_bounds)
  {
//--- Проверяем список
   if(list_bounds==NULL)
      return false;

//--- В цикле по количеству областей в списке
   for(int i=0;i<list_bounds.Total();i++)
     {
      //--- получаем очередную область заголовка и соответствующую ей область ячейки
      CBound *capt_bound=list_bounds.GetNodeAtIndex(i);
      CBound *cell_bound=this.GetBoundAt(i);
      if(capt_bound==NULL || cell_bound==NULL)
         return false;

      //--- В область ячейки устанавливаем координату и размер области заголовка
      cell_bound.SetX(capt_bound.X());
      cell_bound.ResizeW(capt_bound.Width());
      
      //--- Из области ячейки получаем присоединённый объект ячейки
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      if(cell_view==NULL)
         return false;

      //--- В объект визуального представления ячейки устанавливаем координату и размер области ячейки
      cell_view.BoundSetX(cell_bound.X());
      cell_view.BoundResizeW(cell_bound.Width());
     }
//--- Всё успешно
   return true;
  }

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

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

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

Доработаем класс визуального представления заголовка столбца таблицы CColumnCaptionView. 

Заголовок столбца, как элемент управления, будет управлять размером (шириной) столбца и направлением сортировки, либо её отсутствием. При щелчке по заголовку на нём будет появляться стрелочка, указывающая направление сортировки. При отсутствии сортировки (если щёлкнуть по другому заголовку) стрелка не показывается. Если навести курсор на правую границу заголовка, то у курсора появится стрелка-подсказка направления смещения. При этом, если зажать кнопку мышки и тащить грань, то ширина столбца будет изменяться, а расположение столбцов справа будет корректироваться — они будут смещаться, следуя за правым краем изменяемого столбца.

Объявим новые переменные и методы:

//+------------------------------------------------------------------+
//| Класс визуального представления заголовка столбца таблицы        |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CButton
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Указатель на модель заголовка столбца
   CBound           *m_bound_node;                                   // Указатель на область заголовка
   int               m_index;                                        // Индекс в списке столбцов
   ENUM_TABLE_SORT_MODE m_sort_mode;                                 // Режим сортировки столбца таблицы
   
//--- Добавляет в список объекты-подсказки со стрелками
   virtual bool      AddHintsArrowed(void);
//--- Отображает курсор изменения размеров
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
public:
//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Устанавливает, (2) возвращает индекс ячейки
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }
   
//--- (1) Назначает, (2) возвращает область заголовка, которой назначен объект
   void              AssignBoundNode(CBound *bound)                     { this.m_bound_node=bound;                   }
   CBound           *GetBoundNode(void)                                 { return this.m_bound_node;                  }

//--- (1) Назначает, (2) возвращает модель заголовка столбца
   bool              ColumnCaptionModelAssign(CColumnCaption *caption_model);
   CColumnCaption   *ColumnCaptionModel(void)                           { return this.m_column_caption_model;        }

//--- Распечатывает в журнале назначенную модель заголовка столбца
   void              ColumnCaptionModelPrint(void);

//--- (1) Устанавливает, (2) возвращает режим сортировки
   void              SetSortMode(const ENUM_TABLE_SORT_MODE mode)       { this.m_sort_mode=mode;                     }
   ENUM_TABLE_SORT_MODE SortMode(void)                            const { return this.m_sort_mode;                   }
   
//--- Устанавливает противоположное направление сортировки
   void              SetSortModeReverse(void);
   
//--- Рисует (1) внешний вид, (2) стрелку направления сортировки
   virtual void      Draw(const bool chart_redraw);
protected:
   void              DrawSortModeArrow(void);
public:  
//--- Обработчик изменения размеров элемента по правой стороне
   virtual bool      ResizeZoneRightHandler(const int x, const int y);
   
//--- Обработчики изменения размеров элемента по сторонам и углам
   virtual bool      ResizeZoneLeftHandler(const int x, const int y)       { return false;                           }
   virtual bool      ResizeZoneTopHandler(const int x, const int y)        { return false;                           }
   virtual bool      ResizeZoneBottomHandler(const int x, const int y)     { return false;                           }
   virtual bool      ResizeZoneLeftTopHandler(const int x, const int y)    { return false;                           }
   virtual bool      ResizeZoneRightTopHandler(const int x, const int y)   { return false;                           }
   virtual bool      ResizeZoneLeftBottomHandler(const int x, const int y) { return false;                           }
   virtual bool      ResizeZoneRightBottomHandler(const int x, const int y){ return false;                           }
   
//--- Изменяет ширину объекта
   virtual bool      ResizeW(const int w);
   
//--- Обработчик событий нажатий кнопок мышки (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0)const { return CButton::Compare(node,mode);        }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW);}
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Возвращает описание объекта
   virtual string    Description(void);
   
//--- Конструкторы/деструктор
                     CColumnCaptionView(void);
                     CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); 
                    ~CColumnCaptionView (void){}
  };

Рассмотрим доработки уже написанных и реализацию новых методов класса.

В конструкторе установим сортировку по умолчанию как отсутствующую:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Конструктор по умолчанию. Строит объект      |
//| в главном окне текущего графика в координатах 0,0                |
//| с размерами по умолчанию                                         |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(void) : CButton("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE)
  {
//--- Инициализация
   this.Init("Caption");
   this.SetID(0);
   this.SetName("ColumnCaption");
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Конструктор параметрический.                 |
//| Строит объект в указанном окне указанного графика с              |
//| указанными текстом, координатами и размерами                     |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) :
   CButton(object_name,text,chart_id,wnd,x,y,w,h), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE)
  {
//--- Инициализация
   this.Init(text);
   this.SetID(0);
  }

В методе инициализации установим возможность изменения размеров, невозможность перемещения объекта, и зададим область изображения для стрелки направления сортировки:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Инициализация                                |
//+------------------------------------------------------------------+
void CColumnCaptionView::Init(const string text)
  {
//--- Смещения текста по умолчанию
   this.m_text_x=4;
   this.m_text_y=2;
//--- Устанавливаем цвета различных состояний
   this.InitColors();
//--- Возможно изменять размеры
   this.SetResizable(true);
   this.SetMovable(false);
   this.SetImageBound(this.ObjectWidth()-14,4,8,11);
  }

В методе рисования внешнего вида проверим, что объект не находится за пределами контейнера и вызовем метод рисования стрелки направления сортировки:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Рисует внешний вид                           |
//+------------------------------------------------------------------+
void CColumnCaptionView::Draw(const bool chart_redraw)
  {
//--- Если объект за пределами своего контейнера - уходим
   if(this.IsOutOfContainer())
      return;

//--- Заливаем объект цветом фона, рисуем слева светлую вертикальную линию, справа - тёмную
   this.Fill(this.BackColor(),false);
   color clr_dark =this.BorderColor();                                                       // "Тёмный цвет"
   color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100);   // "Светлый цвет"
   this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG()));                          // Линия слева
   this.m_background.Line(this.AdjX(this.Width()-1),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(clr_dark,this.AlphaBG())); // Линия справа
//--- обновляем канвас фона
   this.m_background.Update(false);
   
//--- Выводим текст заголовка
   CLabel::Draw(false);
      
//--- Рисуем стрелки направления сортировки
   this.DrawSortModeArrow();

//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

//+------------------------------------------------------------------+
//| CColumnCaptionView::Рисует стрелку направления сортировки        |
//+------------------------------------------------------------------+
void CColumnCaptionView::DrawSortModeArrow(void)
  {
//--- Задаём цвет стрелки для обычного и заблокированного состояний объекта
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   switch(this.m_sort_mode)
     {
      //--- Сортировка по возрастанию
      case TABLE_SORT_MODE_ASC   :  
         //--- Очищаем область рисунка и рисуем стрелку вниз
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         this.m_painter.ArrowDown(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
         break;
      //--- Сортировка по убыванию
      case TABLE_SORT_MODE_DESC  :  
         //--- Очищаем область рисунка и рисуем стрелку вверх
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         this.m_painter.ArrowUp(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
         break;
      //--- Нет сортировки
      default : 
         //--- Очищаем область рисунка
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         break;
     }
  }

В зависимости от направления сортировки рисуем соответствующую стрелку:

  • по возрастанию — стрелку вниз, 
  • по убыванию — стрелку вверх, 
  • нет сортировки — просто очищаем рисунок со стрелкой.

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

//+------------------------------------------------------------------+
//| CColumnCaptionView::Разворачивает направление сортировки         |
//+------------------------------------------------------------------+
void CColumnCaptionView::SetSortModeReverse(void)
  {
   switch(this.m_sort_mode)
     {
      case TABLE_SORT_MODE_ASC   :  this.m_sort_mode=TABLE_SORT_MODE_DESC; break;
      case TABLE_SORT_MODE_DESC  :  this.m_sort_mode=TABLE_SORT_MODE_ASC;  break;
      default                    :  break;
     }
  }

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

Метод, добавляющий в список объекты-подсказки со стрелками:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Добавляет в список                           |
//| объекты-подсказки со стрелками                                   |
//+------------------------------------------------------------------+
bool CColumnCaptionView::AddHintsArrowed(void)
  {
//--- Создаём подсказку стрелки горизонтального смещения
   CVisualHint *hint=this.CreateAndAddNewHint(HINT_TYPE_ARROW_SHIFT_HORZ,DEF_HINT_NAME_SHIFT_HORZ,18,18);
   if(hint==NULL)
      return false;

//--- Устанавливаем размер области изображения подсказки
   hint.SetImageBound(0,0,hint.Width(),hint.Height());
   
//--- скрываем подсказку и рисуем внешний вид
   hint.Hide(false);
   hint.Draw(false);
   
//--- Всё успешно
   return true;
  }

Создаётся новая подсказка и добавляется в список. Далее её можно получить из списка по имени.

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

//+------------------------------------------------------------------+
//| CColumnCaptionView::Отображает курсор изменения размеров         |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y)
  {
   CVisualHint *hint=NULL;          // Указатель на подсказку
   int hint_shift_x=0;              // Смещение подсказки по X
   int hint_shift_y=0;              // Смещение подсказки по Y
   
//--- В зависимости от расположения курсора на границах элемента
//--- указываем смещения подсказки относительно координат курсора,
//--- отображаем на графике требуемую подсказку и получаем указатель на этот объект
   if(edge!=CURSOR_REGION_RIGHT)
      return false;
   
   hint_shift_x=-8;
   hint_shift_y=-12;
   this.ShowHintArrowed(HINT_TYPE_ARROW_SHIFT_HORZ,x+hint_shift_x,y+hint_shift_y);
   hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ);

//--- Возвращаем результат корректировки положения подсказки относительно курсора
   return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false);
  }

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

Обработчик изменения размеров за правую грань:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Обработчик изменения размеров за правую грань|
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeZoneRightHandler(const int x,const int y)
  {
//--- Рассчитываем и устанавливаем новую ширину элемента
   int width=::fmax(x-this.X()+1,DEF_TABLE_COLUMN_MIN_W);
   if(!this.ResizeW(width))
      return false;
//--- Получаем указатель на подсказку
   CVisualHint *hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ);
   if(hint==NULL)
      return false;
//--- Смещаем подсказку на указанные величины относительно курсора
   int shift_x=-8;
   int shift_y=-12;
   
   CTableHeaderView *header=this.m_container;
   if(header==NULL)
      return false;
   
   bool res=header.RecalculateBounds(this.GetBoundNode(),this.Width());
   res &=hint.Move(x+shift_x,y+shift_y);
   if(res)
      ::ChartRedraw(this.m_chart_id);
   return res;
  }

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

Виртуальный метод, изменяющий ширину объекта:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Изменяет ширину объекта                      |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeW(const int w)
  {
   if(!CCanvasBase::ResizeW(w))
      return false;
//--- Очищаем область рисунка в прошлом месте
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Устанавливаем новую область рисунка
   this.SetImageBound(this.Width()-14,4,8,11);
   return true;
  }

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

Обработчик событий нажатий кнопок мышки:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Обработчик событий нажатий кнопок мышки      |
//+------------------------------------------------------------------+
void CColumnCaptionView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Если кнопка мышки отпущена в области перетаскивания правой грани элемента - уходим
   if(this.ResizeRegion()==CURSOR_REGION_RIGHT)
      return;
//--- Меняем стрелку направления сортировки на обратную и вызываем обработчик щелчка мышки
   this.SetSortModeReverse();
   CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
  }

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

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

//+------------------------------------------------------------------+
//| CColumnCaptionView::Сохранение в файл                            |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Save(const int file_handle)
  {
//--- Сохраняем данные родительского объекта
   if(!CButton::Save(file_handle))
      return false;
  
//--- Сохраняем номер заголовка
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем направление сортировки
   if(::FileWriteInteger(file_handle,this.m_sort_mode,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Загрузка из файла                            |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Load(const int file_handle)
  {
//--- Загружаем данные родительского объекта
   if(!CButton::Load(file_handle))
      return false;
      
//--- Загружаем номер заголовка
   this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем направление сортировки
   this.m_id=this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Всё успешно
   return true;
  }

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

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

Объявим новые методы класса:

//+------------------------------------------------------------------+
//| Класс визуального представления заголовка таблицы                |
//+------------------------------------------------------------------+
class CTableHeaderView : public CPanel
  {
protected:
   CColumnCaptionView m_temp_caption;                                // Временный объект заголовка столбца для поиска
   CTableHeader     *m_table_header_model;                           // Указатель на модель заголовка таблицы

//--- Создаёт и добавляет в список новый объект представления заголовка столбца
   CColumnCaptionView *InsertNewColumnCaptionView(const string text, const int x, const int y, const int w, const int h);
   
public:
//--- (1) Устанавливает, (2) возвращает модель заголовка таблицы
   bool              TableHeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetTableHeaderModel(void)                          { return this.m_table_header_model;    }

//--- Перерассчитывает области заголовков
   bool              RecalculateBounds(CBound *bound,int new_width);

//--- Распечатывает в журнале назначенную модель заголовка таблицы
   void              TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);
   
//--- Устанавливает заголовку столбца флаг сортировки
   void              SetSortedColumnCaption(const uint index);

//--- Получает заголовок столбца (1) по индексу, (2) с флагом сортировки
   CColumnCaptionView *GetColumnCaption(const uint index);
   CColumnCaptionView *GetSortedColumnCaption(void);
//--- Возвращает индекс заголовка столбца с флагом сортировки
   int               IndexSortedColumnCaption(void);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode);      }
   virtual bool      Save(const int file_handle)                        { return CPanel::Save(file_handle);       }
   virtual bool      Load(const int file_handle)                        { return CPanel::Load(file_handle);       }
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_HEADER_VIEW); }
   
//--- Обработчик пользовательского события элемента при щелчке на области объекта
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);
   virtual void      InitColors(void);

//--- Конструкторы/деструктор
                     CTableHeaderView(void);
                     CTableHeaderView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableHeaderView (void){}
  };

Рассмотрим доработки имеющихся и реализацию объявленных методов.

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Устанавливает модель заголовка                 |
//+------------------------------------------------------------------+
bool CTableHeaderView::TableHeaderModelAssign(CTableHeader *header_model)
  {
//--- Если передан пустой объект - сообщаем об этом и возвращаем false
   if(header_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Если в переданной модели заголовка нет ни одного заголовка столбца - сообщаем об этом и возвращаем false
   int total=(int)header_model.ColumnsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Header model does not contain any columns",__FUNCTION__);
      return false;
     }
//--- Сохраняем указатель на переданную модель заголовка таблицы и рассчитываем ширину каждого заголовка столбца
   this.m_table_header_model=header_model;
   int caption_w=(int)::fmax(::round((double)this.Width()/(double)total),DEF_TABLE_COLUMN_MIN_W);

//--- В цикле по количеству заголовков столбцов в модели заголовка таблицы
   for(int i=0;i<total;i++)
     {
      //--- получаем модель очередного заголовка столбца,
      CColumnCaption *caption_model=this.m_table_header_model.GetColumnCaption(i);
      if(caption_model==NULL)
         return false;
      //--- рассчитываем координату и создаём имя для области заголовка столбца
      int x=caption_w*i;
      string name="CaptionBound"+(string)i;
      //--- Создаём новую область заголовка столбца
      CBound *caption_bound=this.InsertNewBound(name,x,0,caption_w,this.Height());
      if(caption_bound==NULL)
         return false;
      caption_bound.SetID(i);
      //--- Создаём новый объект визуального представления заголовка столбца
      CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height());
      if(caption_view==NULL)
         return false;
         
      //--- На текущую область заголовка столбца назначаем соответствующий объект визуального представления заголовка столбца
      caption_bound.AssignObject(caption_view);
      caption_view.AssignBoundNode(caption_bound);
      
      //--- Для самого первого заголовка устанавливаем флаг сортировки по возрастанию
      if(i==0)
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
     }
//--- Всё успешно
   return true;
  }

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Перерассчитывает области заголовков            |
//+------------------------------------------------------------------+
bool CTableHeaderView::RecalculateBounds(CBound *bound,int new_width)
  {
//--- Если передан пустой объект области или его ширина не изменилась - возвращаем false
   if(bound==NULL || bound.Width()==new_width)
      return false;
      
//--- Получаем индекс области в списке
   int index=this.m_list_bounds.IndexOf(bound);
   if(index==WRONG_VALUE)
      return false;

//--- Вычисляем смещение и, если его нет - возвращаем false
   int delta=new_width-bound.Width();
   if(delta==0)
      return false;

//--- Изменяем ширину текущей области и назначенного на область объекта
   bound.ResizeW(new_width);
   CElementBase *assigned_obj=bound.GetAssignedObj();
   if(assigned_obj!=NULL)
      assigned_obj.ResizeW(new_width);

//--- Получаем следующую область после текущей
   CBound *next_bound=this.m_list_bounds.GetNextNode();
//--- Пересчитываем координаты X для всех последующих областей
   while(!::IsStopped() && next_bound!=NULL)
     {
      //--- Сдвигаем область на значение delta
      int new_x = next_bound.X()+delta;
      int prev_width=next_bound.Width();
      next_bound.SetX(new_x);
      next_bound.Resize(prev_width,next_bound.Height());
      
      //--- Если в области есть назначенный объект, обновляем его положение
      CElementBase *assigned_obj=next_bound.GetAssignedObj();
      if(assigned_obj!=NULL)
        {
         assigned_obj.Move(assigned_obj.X()+delta,assigned_obj.Y());
         
         //--- Этот блок кода - часть мероприятий по поиску и устранению артефактов при перетаскивании заголовков
         CCanvasBase *base_obj=assigned_obj.GetContainer();
         if(base_obj!=NULL)
           {
            if(assigned_obj.X()>base_obj.ContainerLimitRight())
               assigned_obj.Hide(false);
            else
               assigned_obj.Show(false);
           }
        }
      //--- Переходим к следующей области
      next_bound=this.m_list_bounds.GetNextNode();
     }
     
//--- Рассчитаем новую ширину заголовка таблицы по ширине заголовков столбцов
   int header_width=0;
   for(int i=0;i<this.m_list_bounds.Total();i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL)
         header_width+=bound.Width();
     }

//--- Если рассчитанная ширина заголовка таблицы отличается от текущей - изменяем ширину
   if(header_width!=this.Width())
     {
      if(!this.ResizeW(header_width))
         return false;
     }

//--- Получаем указатель на объект таблицы (View)
   CTableView *table_view=this.GetContainer();
   if(table_view==NULL)
      return false;

//--- Из объекта таблицы получаем указатель на панель со строками таблицы
   CPanel *table_area=table_view.GetTableArea();
   if(table_area==NULL)
      return false;
   
//--- Меняем размер панели строк таблицы под общий размер заголовков столбцов
   if(!table_area.ResizeW(header_width))
      return false;
   
//--- Получаем список строк таблицы и проходим в цикле по всем строкам
   CListElm *list=table_area.GetListAttachedElements();
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- Получаем очередную строку таблицы
      CTableRowView *row=table_area.GetAttachedElementAt(i);
      if(row!=NULL)
        {
         //--- Меняем размер строки под размер панели и перерассчитываем области ячеек
         row.ResizeW(table_area.Width());
         row.RecalculateBounds(&this.m_list_bounds);
        }
     }
//--- Перерисовываем все строки таблицы
   table_area.Draw(false);
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Устанавливает заголовку столбца флаг сортировки|
//+------------------------------------------------------------------+
void CTableHeaderView::SetSortedColumnCaption(const uint index)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Получаем область очередного заголовка столбца и
      //--- из неё получаем присоединённый объект заголовка столбца
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
      if(caption_view==NULL)
         continue;
      
      //--- Если индекс цикла равен требуемому индексу - устанавливаем флаг сортировки по возрастанию
      if(i==index)
        {
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
         caption_view.Draw(false);
        }
      //--- Иначе - сбрасываем флаг сортировки
      else
        {
         caption_view.SetSortMode(TABLE_SORT_MODE_NONE);
         caption_view.Draw(false);
        }
     }
   this.Draw(true);
  }

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

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Получает заголовок столбца по индексу          |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::GetColumnCaption(const uint index)
  {
//--- Получаем область заголовка столбца по индексу
   CBound *capt_bound=this.GetBoundAt(index);
   if(capt_bound==NULL)
      return NULL;
//--- Из области заголовка столбца возвращаем указатель на присоединённый объект заголовка столбца
   return capt_bound.GetAssignedObj();
  }

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Получает заголовок столбца с флагом сортировки |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::GetSortedColumnCaption(void)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Получаем область очередного заголовка столбца и
      //--- из неё получаем присоединённый объект заголовка столбца
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
      
      //--- Если объект получен и у него установлен флаг сортировки - возвращаем указатель на него
      if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE)
         return caption_view;
     }
   return NULL;
  }

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Возвращает индекс сортированного столбца       |
//+------------------------------------------------------------------+
int CTableHeaderView::IndexSortedColumnCaption(void)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Получаем область очередного заголовка столбца и
      //--- из неё получаем присоединённый объект заголовка столбца
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
     
      //--- Если объект получен и у него установлен флаг сортировки - возвращаем индекс области
      if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE)
         return i;
     }
   return WRONG_VALUE;
  }

Представленные выше три метода в простом цикле ищут заголовок столбца с искомым флагом, либо по искомому индексу, и возвращают соответствующие методу данные.

Обработчик пользовательского события элемента при щелчке на области объекта:

//+------------------------------------------------------------------+
//| CTableHeaderView::Обработчик пользовательского события элемента  |
//| при щелчке на области объекта                                    |
//+------------------------------------------------------------------+
void CTableHeaderView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Получаем из sparam наименование объекта заголовка таблицы
   int len=::StringLen(this.NameFG());
   string header_str=::StringSubstr(sparam,0,len);
//--- Если извлечённое имя не совпадает с именем этого объекта - не наше событие, уходим
   if(header_str!=this.NameFG())
      return;
   
//--- Найдём в sparam индекс заголовка столбца
   string capt_str=::StringSubstr(sparam,len+1);
   string index_str=::StringSubstr(capt_str,5,capt_str.Length()-7);
//--- Не нашли индекс в строке - уходим
   if(index_str=="")
      return;
//--- Записываем индекс заголовка столбца
   int index=(int)::StringToInteger(index_str);
   
//--- Получаем заголовок столбца по индексу
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- Если заголовок не имеет флага сортировки - ставим флаг сортировки по возрастанию
   if(caption.SortMode()==TABLE_SORT_MODE_NONE)
     {
      this.SetSortedColumnCaption(index);
     }
//--- Отправляем пользовательское событие на график с индексом заголовка в lparam, режимом сортировки в dparam и именем объекта в sparam
//--- Так как в стандартном событии OBJECT_CLICK в lparam и dparam передаются координаты курсора, то здесь будем передавать отрицательные значения
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, -(1000+index), -(1000+caption.SortMode()), this.NameFG());
   ::ChartRedraw(this.m_chart_id);
  }

Метод отправляет пользовательское событие о включении сортировки по столбцу. Так как в lparam и dparam в событии щелчка по объекту передаются координаты курсора, то, чтобы понимать, что это событие включения сортировки, будем передавать в lparam и dparam отрицательное значение:

  • В lparam  (-1000) + индекс столбца,
  • В dparam (-1000) + тип сортировки.

Доработаем класс визуального представления таблицы CTableView.

Класс представляет собой полноценный элемент управления "Таблица". Доработками будет подключение функционала по изменению ширины столбцов и сортировке по выбранному столбцу таблицы.

Объявим новые методы класса:

//+------------------------------------------------------------------+
//| Класс визуального представления таблицы                          |
//+------------------------------------------------------------------+
class CTableView : public CPanel
  {
protected:
//--- Получаемые данные таблицы
   CTable           *m_table_obj;                  // Указатель на объект таблицы (включает модели таблицы и заголовка)
   CTableModel      *m_table_model;                // Указатель на модель таблицы (получаем из CTable)
   CTableHeader     *m_header_model;               // Указатель на модель заголовка таблицы (получаем из CTable)
   
//--- Данные компонента View
   CTableHeaderView *m_header_view;                // Указатель на заголовок таблицы (View)
   CPanel           *m_table_area;                 // Панель для размещения строк таблицы
   CContainer       *m_table_area_container;       // Контейнер для размещения панели со строками таблицы
   
//--- (1) Устанавливает, (2) возвращает модель таблицы
   bool              TableModelAssign(CTableModel *table_model);
   CTableModel      *GetTableModel(void)                                { return this.m_table_model;           }
   
//--- (1) Устанавливает, (2) возвращает модель заголовка таблицы
   bool              HeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetHeaderModel(void)                               { return this.m_header_model;          }

//--- Создаёт из модели объект (1) заголовка, (2) таблицы, (3) обновляет изменённую таблицу
   bool              CreateHeader(void);
   bool              CreateTable(void);
   bool              UpdateTable(void);
   
public:
//--- (1) Устанавливает, (2) возвращает объект таблицы
   bool              TableObjectAssign(CTable *table_obj);
   CTable           *GetTableObj(void)                                  { return this.m_table_obj;             }

//--- Возвращает (1) заголовок, (2) область размещения таблицы, (3) контейнер области таблицы
   CTableHeaderView *GetHeader(void)                                    { return this.m_header_view;           }
   CPanel           *GetTableArea(void)                                 { return this.m_table_area;            }
   CContainer       *GetTableAreaContainer(void)                        { return this.m_table_area_container;  }

//--- Распечатывает в журнале назначенную модель (1) таблицы, (2) заголовка, (3) объекта таблицы
   void              TableModelPrint(const bool detail);
   void              HeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   void              TablePrint(const int column_width=CELL_WIDTH_IN_CHARS);
   
//--- Получает заголовок столбца (1) по индексу, (2) с флагом сортировки
   CColumnCaptionView *GetColumnCaption(const uint index)
                       { return(this.GetHeader()!=NULL ? this.GetHeader().GetColumnCaption(index) : NULL);     }
   CColumnCaptionView *GetSortedColumnCaption(void)
                       { return(this.GetHeader()!=NULL ? this.GetHeader().GetSortedColumnCaption(): NULL);     }

//--- Возвращает объект визуальгного представления указанной (1) строки, (2) ячейки
   CTableRowView    *GetRowView(const uint index)
                       { return(this.GetTableArea()!=NULL ? this.GetTableArea().GetAttachedElementAt(index) : NULL); }
   CTableCellView   *GetCellView(const uint row,const uint col)
                       { return(this.GetRowView(row)!=NULL ? this.GetRowView(row).GetCellView(col) : NULL);    }
                       
//--- Возвращает количество строк таблицы
   int               RowsTotal(void)
                       { return(this.GetTableArea()!=NULL ? this.GetTableArea().AttachedElementsTotal() : 0);  }

//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode);   }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_VIEW);     }
   
//--- Обработчик пользовательского события элемента при щелчке на области объекта
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Сортирует таблицу по значению столбца и направлению
   bool              Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode);
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);

//--- Конструкторы/деструктор
                     CTableView(void);
                     CTableView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableView (void){}
  };

Рассмотрим объявленные методы.

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

//+------------------------------------------------------------------+
//| CTableView::Обновляет изменённую таблицу                         |
//+------------------------------------------------------------------+
bool CTableView::UpdateTable(void)
  {
   if(this.m_table_area==NULL)
      return false;
   
   int total_model=(int)this.m_table_model.RowsTotal();        // Количество строк в модели
   int total_view =this.m_table_area.AttachedElementsTotal();  // Количество строк в визуальном представлении
   int diff=total_model-total_view;                            // Разница в количестве строк двух компонентов
   int y=1;                                                    // Смещение по вертикали
   int table_height=0;                                         // Рассчитываемая высота панели
   CTableRowView *row=NULL;                                    // Указатель на объект визуального представления строки
   
//--- Если в модели больше строк, чем в визуальном представлении - создадим недостающие строки в визуальном предаставлении в конце списка
   if(diff>0)
     {
      //--- Получаем последнюю строку визуального представления таблицы (на основе её координат будут размещены добавляемые строки)
      row=this.m_table_area.GetAttachedElementAt(total_view-1);
      //--- В цикле по количеству недостающих строк
      for(int i=total_view;i<total_view+diff;i++)
        {
         //--- создаём и присоединяем к панели diff количество объектов визуального представления строки таблицы
         row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW_VIEW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H);
         if(row==NULL)
            return false;
        }
     }
 
//--- Если в визуальном представлении больше строк, чем в модели - удалим лишние строки в визуальном предаставлении в конце списка
   if(diff<0)
     {
      CListElm *list=this.m_table_area.GetListAttachedElements();
      if(list==NULL)
         return false;
      
      int  start=total_view-1;
      int  end=start-diff;
      bool res=true;
      for(int i=start;i>end;i--)
         res &=list.Delete(i);
      if(!res)
         return false;
     }
   
//--- В цикле по списку строк модели таблицы
   for(int i=0;i<total_model;i++)
     {
      //--- получаем из списка панели строк очередной объект визуального представления строки таблицы
      row=this.m_table_area.GetAttachedElementAt(i);
      if(row==NULL)
         return false;
      //--- Проверим тип объекта
      if(row.Type()!=ELEMENT_TYPE_TABLE_ROW_VIEW)
         continue;
         
      //--- Устанавливаем идентификатор строки
      row.SetID(i);
      //--- В зависимости от номера строки (чет/нечет) устанавливаем цвет её фона
      if(row.ID()%2==0)
         row.InitBackColorDefault(clrWhite);
      else
         row.InitBackColorDefault(C'242,242,242');
      row.BackColorToDefault();
      row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4));
      
      //--- Получаем модель строки из объекта таблицы
      CTableRow *row_model=this.m_table_model.GetRow(i);
      if(row_model==NULL)
         return false;

      //--- Обновляем ячейки объекта строки таблицы по модели строки
      row.TableRowModelUpdate(row_model);
      //--- Рассчитываем новое значение высоты панели
      table_height+=row.Height();
     }
//--- Возвращаем результат изменения размера панели на рассчитанное в цикле значение
   return this.m_table_area.ResizeH(table_height+y);
  }

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

Обработчик пользовательского события элемента при щелчке на области объекта:

//+------------------------------------------------------------------+
//| CTableView::Обработчик пользовательского события элемента        |
//| при щелчке на области объекта                                    |
//+------------------------------------------------------------------+
void CTableView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam)
  {
   if(id==CHARTEVENT_OBJECT_CLICK && lparam>=0 && dparam>=0)
      return;
      
//--- Получаем из sparam наименование объекта заголовка таблицы
   int len=::StringLen(this.NameFG());
   string header_str=::StringSubstr(sparam,0,len);
//--- Если извлечённое имя не совпадает с именем этого объекта - не наше событие, уходим
   if(header_str!=this.NameFG())
      return;
   
//--- Записываем индекс заголовка столбца
//--- Так как в стандартном событии OBJECT_CLICK в lparam и dparam передаются координаты курсора,
//--- то для этого обработчика передаётся отрицательное значение индекса заголовка, по которому было событие
   int index=(int)::fabs(lparam+1000);
   
//--- Получаем заголовок столбца по индексу
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- Сортируем список строк по значению сортировки в заголовке столбца и обновляем таблицу
   this.Sort(index,caption.SortMode());
   if(this.UpdateTable())
      this.Draw(true);
  }

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

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


Класс для упрощенного создания таблиц

Продолжим писать код в файле \MQL5\Indicators\Tables\Controls\Controls.mqh.

//+------------------------------------------------------------------+
//| Класс управления таблицами                                       |
//+------------------------------------------------------------------+
class CTableControl : public CPanel
  {
protected:
   CListObj          m_list_table_model;
//--- Добавляет объект (1) модели (CTable), (2) визуального представления (CTableView) таблицы в список
   bool              TableModelAdd(CTable *table_model,const int table_id,const string source);
   CTableView       *TableViewAdd(CTable *table_model,const string source);
//--- Обновляет указанный столбец указанной таблицы
   bool              ColumnUpdate(const string source, CTable *table_model, const uint table, const uint col, const bool cells_redraw);
   
public:
//--- Возвращает (1) модель, (2) объект визуального представления таблицы, (3) тип объекта
   CTable           *GetTable(const uint index)                   { return this.m_list_table_model.GetNodeAtIndex(index);  }
   CTableView       *GetTableView(const uint index)               { return this.GetAttachedElementAt(index);               }
   
//--- Создание таблицы на основании переданных данных
template<typename T>
   CTableView       *TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const uint num_rows, const uint num_columns,const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE);
   
//--- Возвращает (1) строковое значение указанной ячейки (Model), указанную (2) строку, (3) ячейку таблицы (View)
   string            CellValueAt(const uint table, const uint row, const uint col);
   CTableRowView    *GetRowView(const uint table, const uint index);
   CTableCellView   *GetCellView(const uint table, const uint row, const uint col);
   
//--- Устанавливает (1) значение, (2) точность, (3) флаги отображения времени, (4) флаг отображения имён цветов в указанную ячейку (Model + View)
template<typename T>
   void              CellSetValue(const uint table, const uint row, const uint col, const T value, const bool chart_redraw);
   void              CellSetDigits(const uint table, const uint row, const uint col, const int digits, const bool chart_redraw);
   void              CellSetTimeFlags(const uint table, const uint row, const uint col, const uint flags, const bool chart_redraw);
   void              CellSetColorNamesFlag(const uint table, const uint row, const uint col, const bool flag, const bool chart_redraw);

//--- Устанавливает цвет переднего плана в указанную ячейку (View)
   void              CellSetForeColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw);
   
//--- (1) Устанавливает, (2) возвращает точку привязки текста в указанной ячейке (View)
   void              CellSetTextAnchor(const uint table, const uint row, const uint col, const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw);
   ENUM_ANCHOR_POINT CellTextAnchor(const uint table, const uint row, const uint col);
   
//--- Устанавливает (1) точность, (2) флаги отображения времени, (3) флаг отображения имён цветов, (4) точку привязки текста, (5) тип данных в указанном столбце (View)
   void              ColumnSetDigits(const uint table, const uint col, const int digits, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetTimeFlags(const uint table, const uint col, const uint flags, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetColorNamesFlag(const uint table, const uint col, const bool flag, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetTextAnchor(const uint table, const uint col, const ENUM_ANCHOR_POINT anchor, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetDatatype(const uint table, const uint col, const ENUM_DATATYPE type, const bool cells_redraw, const bool chart_redraw);

//--- Тип объекта
   virtual int       Type(void)                             const { return(ELEMENT_TYPE_TABLE_CONTROL_VIEW);               }

//--- Конструкторы/деструктор
                     CTableControl(void) { this.m_list_table_model.Clear(); }
                     CTableControl(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableControl(void) {}
  };

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

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

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

Рассмотрим объявленные методы класса.

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CTableControl::CTableControl(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,"",chart_id,wnd,x,y,w,h)
  {
   this.m_list_table_model.Clear();
   this.SetName("Table Control");
  }

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

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

//+------------------------------------------------------------------+
//| Добавляет объект модели таблицы (CTable) в список                |
//+------------------------------------------------------------------+
bool CTableControl::TableModelAdd(CTable *table_model,const int table_id,const string source)
  {
//--- Проверяем объект модели таблицы
   if(table_model==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to create Table Model object",source,__FUNCTION__);
      return false;
     }
//--- Устанавливаем в модель таблицы идентификатор - либо по размеру списка, либо заданный
   table_model.SetID(table_id<0 ? this.m_list_table_model.Total() : table_id);
//--- Если модель таблицы с установленным идентификатором есть в списке - сообщаем об этом, удаляем объект и возвращаем false
   this.m_list_table_model.Sort(0);
   if(this.m_list_table_model.Search(table_model)!=NULL)
     {
      ::PrintFormat("%s::%s: Error: Table Model object with ID %d already exists in the list",source,__FUNCTION__,table_id);
      delete table_model;
      return false;
     }
//--- Если модель таблицы не добавлена в список - сообщаем об этом, удаляем объект и возвращаем false
   if(this.m_list_table_model.Add(table_model)<0)
     {
      ::PrintFormat("%s::%s: Error. Failed to add Table Model object to list",source,__FUNCTION__);
      delete table_model;
      return false;
     }
//--- Всё успешно
   return true;
  }

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

//+------------------------------------------------------------------+
//| Создаёт новый и добавляет в список объект                        |
//| визуального представления таблицы (CTableView)                   |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableViewAdd(CTable *table_model,const string source)
  {
//--- Проверяем объект модели таблицы
   if(table_model==NULL)
     {
      ::PrintFormat("%s::%s: Error. An invalid Table Model object was passed",source,__FUNCTION__);
      return NULL;
     }
//--- Создаём новый элемент - визуальное представление таблицы, прикреплённый к панели
   CTableView *table_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_VIEW,"","TableView"+(string)table_model.ID(),1,1,this.Width()-2,this.Height()-2);
   if(table_view==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to create Table View object",source,__FUNCTION__);
      return NULL;
     }
//--- Графическому элементу "Таблица" (View) назначаем объект таблицы (Model) и его идентификатор
   table_view.TableObjectAssign(table_model);
   table_view.SetID(table_model.ID());
   return table_view;
  }

Оба вышерассмотренных метода используются в методах создания таблицы.

Метод, создающий таблицу с указанием массива таблицы и массива заголовков:

//+-------------------------------------------------------------------+
//| Создаёт таблицу с указанием массива таблицы и массива заголовков. | 
//| Определяет количество и наименования колонок согласно column_names|
//| Количество строк определены размером массива данных row_data,     |
//| который используется и для заполнения таблицы                     |
//+-------------------------------------------------------------------+
template<typename T>
CTableView *CTableControl::TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE)
  {
//--- Создаём объект таблицы по указанным параметрам
   CTable *table_model=new CTable(row_data,column_names);
//--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Создаём и возвращаем таблицу
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

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

//+------------------------------------------------------------------+
//| Создаёт таблицу с определением количества колонок и строк.       |
//| Колонки будут иметь Excel-наименования "A", "B", "C" и т.д.      |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const uint num_rows,const uint num_columns,const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(num_rows,num_columns);
//--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Создаём и возвращаем таблицу
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

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

//+------------------------------------------------------------------+
//| Создаёт таблицу с инициализацией колонок согласно column_names   |
//| Количество строк определены параметром row_data, с типом matrix  |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(row_data,column_names);
//--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Создаём и возвращаем таблицу
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

Метод, создающий таблицу на основе списка пользовательских параметров и массива заголовков столбцов:

//+------------------------------------------------------------------+
//| Создаёт таблицу с указанием массива таблицы на основе            |
//| списка row_data, содержащего объекты с данными полей структуры.  | 
//| Определяет количество и наименования колонок согласно количеству |
//| наименований столбцов в массиве column_names                     |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE)
  {
   CTableByParam *table_model=new CTableByParam(row_data,column_names);
//--- Если есть ошибки при создании или добавлении таблицы в список - возвращаем NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Создаём и возвращаем таблицу
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

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

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

//+------------------------------------------------------------------+
//| Устанавливает значение в указанную ячейку (Model + View)         |
//+------------------------------------------------------------------+
template<typename T>
void CTableControl::CellSetValue(const uint table,const uint row,const uint col,const T value,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Из модели таблицы получаем модель ячейки
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL)
      return;
      
//--- Получаем объект визуального представления ячейки
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
      
//--- Сравниваем установленное в ячейке значение с переданным
   bool equal=false;
   ENUM_DATATYPE datatype=cell_model.Datatype();
   switch(datatype)
     {
      case TYPE_LONG    :  
      case TYPE_DATETIME:  
      case TYPE_COLOR   :  equal=(cell_model.ValueL()==value);                                           break;
      case TYPE_DOUBLE  :  equal=(::NormalizeDouble(cell_model.ValueD()-value,cell_model.Digits())==0);  break;
      //---TYPE_STRING
      default           :  equal=(::StringCompare(cell_model.ValueS(),(string)value)==0);                break;
     }
//--- Если значения равны - уходим
   if(equal)
      return;
      
//--- В модель ячейки устанавливаем новое значение;
//--- в объект визуального представления ячейки вписываем значение из модели ячейки
//--- Перерисовываем ячейку с флагом обновления графика
   table_model.CellSetValue(row,col,value);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

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

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

//+------------------------------------------------------------------+
//| Устанавливает точность в указанную ячейку (Model + View)         |
//+------------------------------------------------------------------+
void CTableControl::CellSetDigits(const uint table,const uint row,const uint col,const int digits,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Из модели таблицы получаем модель ячейки
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.Digits()==digits)
      return;
      
//--- Получаем объект визуального представления ячейки
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- В модель ячейки устанавливаем новое значение точности;
//--- в объект визуального представления ячейки вписываем значение из модели ячейки
//--- Перерисовываем ячейку с флагом обновления графика
   table_model.CellSetDigits(row,col,digits);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

Метод подобен вышерассмотренному. Если указанная точность дробных чисел равна фактической, то уходим из метода. Далее точность записывается в модель ячейки, в визуальное представление записывается текст из модели ячейки (для дробных чисел там уже изменённая точность), и ячейка перерисовывается.

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

//+------------------------------------------------------------------+
//| Устанавливает флаги отображения времени                          |
//| в указанную ячейку (Model + View)                                |
//+------------------------------------------------------------------+
void CTableControl::CellSetTimeFlags(const uint table,const uint row,const uint col,const uint flags,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Из модели таблицы получаем модель ячейки
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.DatetimeFlags()==flags)
      return;
      
//--- Получаем объект визуального представления ячейки
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- В модель ячейки устанавливаем новое значение флагов отображения времени;
//--- в объект визуального представления ячейки вписываем значение из модели ячейки
//--- Перерисовываем ячейку с флагом обновления графика
   table_model.CellSetTimeFlags(row,col,flags);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

Логика метода идентична методу, устанавливающему точность в ячейку.

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

//+------------------------------------------------------------------+
//| Устанавливает флаг отображения имён цветов                       |
//| в указанную ячейку (Model + View)                                |
//+------------------------------------------------------------------+
void CTableControl::CellSetColorNamesFlag(const uint table,const uint row,const uint col,const bool flag,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Из модели таблицы получаем модель ячейки
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.ColorNameFlag()==flag)
      return;
      
//--- Получаем объект визуального представления ячейки
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- В модель ячейки устанавливаем новое значение флага отображения имён цветов;
//--- в объект визуального представления ячейки вписываем значение из модели ячейки
//--- Перерисовываем ячейку с флагом обновления графика
   table_model.CellSetColorNamesFlag(row,col,flag);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

Точно такой же метод, как и вышерассмотренные.

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

//+------------------------------------------------------------------+
//| Устанавливает цвет переднего плана в указанную ячейку (View)     |
//+------------------------------------------------------------------+
void CTableControl::CellSetForeColor(const uint table,const uint row,const uint col,const color clr,const bool chart_redraw)
  {
//--- Получаем объект визуального представления ячейки
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- В объект визуального представления ячейки устанавливаем цвет фона ячейки
//--- Перерисовываем ячейку с флагом обновления графика
   cell_view.SetForeColor(clr);
   cell_view.Draw(chart_redraw);
  }

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

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

//+------------------------------------------------------------------+
//| Устанавливает точку привязки текста в указанную ячейку (View)    |
//+------------------------------------------------------------------+
void CTableControl::CellSetTextAnchor(const uint table,const uint row,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw)
  {
//--- Получаем объект визуального представления ячейки
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- В объект визуального представления ячейки устанавливаем точку привязки текста
//--- Перерисовываем ячейку с флагом обновления графика
   cell_view.SetTextAnchor(anchor,cell_redraw,chart_redraw);
  }

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

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

//+------------------------------------------------------------------+
//| Возвращает точку привязки текста в указанной ячейке (View)       |
//+------------------------------------------------------------------+
ENUM_ANCHOR_POINT CTableControl::CellTextAnchor(const uint table,const uint row,const uint col)
  {
//--- Получаем объект визуального представления ячейки
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return ANCHOR_LEFT_UPPER;
   
//--- Возвращаем точку привязки текста
   return((ENUM_ANCHOR_POINT)cell_view.TextAnchor());
  }

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

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

//+------------------------------------------------------------------+
//| Обновляет указанный столбец указанной таблицы                    |
//+------------------------------------------------------------------+
bool CTableControl::ColumnUpdate(const string source,CTable *table_model,const uint table,const uint col,const bool cells_redraw)
  {
//--- Проверяем модель таблицы
   if(::CheckPointer(table_model)==POINTER_INVALID)
     {
      ::PrintFormat("%s::%s: Error. Invalid table model pointer passed",source,__FUNCTION__);
      return false;
     }
//--- Получаем визуальное представление таблицы
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to get CTableView object",source,__FUNCTION__);
      return false;
     }
   
//--- В цикле по строкам визуального представления таблицы
   int total=table_view.RowsTotal();
   for(int i=0;i<total;i++)
     {
      //--- получаем из очередной строки таблицы объект визуального представление ячейки в указанном столбце
      CTableCellView *cell_view=this.GetCellView(table,i,col);
      if(cell_view==NULL)
        {
         ::PrintFormat("%s::%s: Error. Failed to get CTableCellView object (row %d, col %u)",source,__FUNCTION__,i,col);
         return false;
        }
      //--- Получаем модель соответствующей ячейки из модели строки
      CTableCell *cell_model=table_model.GetCell(i,col);
      if(cell_model==NULL)
        {
         ::PrintFormat("%s::%s: Error. Failed to get CTableCell object (row %d, col %u)",source,__FUNCTION__,i,col);
         return false;
        }
      
      //--- В объект визуального представление ячейки записываем значение из модели ячейки
      cell_view.SetText(cell_model.Value());
      //--- Если указано - перерисовываем визуальное представление ячейки
      if(cells_redraw)
         cell_view.Draw(false);
     }
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| Устанавливает точность в указанном столбце (Model + View)        |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetDigits(const uint table,const uint col,const int digits,const bool cells_redraw,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Устанавливаем Digits для указанного столбца в модели таблицы 
   table_model.ColumnSetDigits(col,digits);

//--- Обновляем отображение данных столбца и, если указано, перерисовываем график
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

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

//+------------------------------------------------------------------+
//| Устанавливает флаги отображения времени                          |
//| в указанном столбце (Model + View)                               |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetTimeFlags(const uint table,const uint col,const uint flags,const bool cells_redraw,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Устанавливаем флаги отображения времени для указанного столбца в модели таблицы 
   table_model.ColumnSetTimeFlags(col,flags);

//--- Обновляем отображение данных столбца и, если указано, перерисовываем график
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

В модели таблицы устанавливаем для столбца указанные флаги отображения времени и вызываем метод обновления столбца таблицы.

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

//+------------------------------------------------------------------+
//| Устанавливает флаг отображения имён цвета                        |
//| в указанном столбце (Model + View)                               |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetColorNamesFlag(const uint table,const uint col,const bool flag,const bool cells_redraw,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Устанавливаем флаги отображения времени для указанного столбца в модели таблицы 
   table_model.ColumnSetColorNamesFlag(col,flag);

//--- Обновляем отображение данных столбца и, если указано, перерисовываем график
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

В модели таблицы устанавливаем для столбца указанный флаг отображения имён цвета и вызываем метод обновления столбца таблицы.

Метод, устанавливающий тип данных в указанном столбце:

//+------------------------------------------------------------------+
//| Устанавливает тип данных в указанном столбце ( (Model + View))   |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetDatatype(const uint table,const uint col,const ENUM_DATATYPE type,const bool cells_redraw,const bool chart_redraw)
  {
//--- Получаем модель таблицы
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Устанавливаем тип данных для указанного столбца в модели таблицы 
   table_model.ColumnSetDatatype(col,type);

//--- Обновляем отображение данных столбца и, если указано, перерисовываем график
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

В модели таблицы устанавливаем для столбца указанный тип данных и вызываем метод обновления столбца таблицы.

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

//+------------------------------------------------------------------+
//| Устанавливает точку привязки текста в указанном столбце (View)   |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetTextAnchor(const uint table,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cells_redraw,const bool chart_redraw)
  {
//--- Получаем визуальное представление таблицы
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return;
     }
//--- В цикле по всем строкам таблицы
   int total=table_view.RowsTotal();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередной объект визуального представления ячейки
      //--- и вписываем в объект новую точку привязки
      CTableCellView *cell_view=this.GetCellView(table,i,col);
      if(cell_view!=NULL && cell_view.TextAnchor()!=anchor)
         cell_view.SetTextAnchor(anchor,cells_redraw,false);
     }
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

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

//+------------------------------------------------------------------+
//| Возвращает строковое значение указанной ячейки (Model)           |
//+------------------------------------------------------------------+
string CTableControl::CellValueAt(const uint table,const uint row,const uint col)
  {
   CTable *table_model=this.GetTable(table);
   return(table_model!=NULL ? table_model.CellValueAt(row,col) : ::StringFormat("%s: Error. Failed to get table model",__FUNCTION__));
  }

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

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

//+------------------------------------------------------------------+
//| Возвращает указанную строку таблицы (View)                       |
//+------------------------------------------------------------------+
CTableRowView *CTableControl::GetRowView(const uint table,const uint index)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return NULL;
     }
   return table_view.GetRowView(index);
  }

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

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

//+------------------------------------------------------------------+
//| Возвращает указанную ячейку таблицы (View)                       |
//+------------------------------------------------------------------+
CTableCellView *CTableControl::GetCellView(const uint table,const uint row,const uint col)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return NULL;
     }
   return table_view.GetCellView(row,col);
  }

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

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

Давайте проверим, что у нас получилось.


Тестируем результат

Откроем файл тестового индикатора \MQL5\Indicators\Tables\iTestTable.mq5 и перезапишем его в таком виде (ранее подготовленные данные и создание таблицы выделены соответствующими цветами):

//+------------------------------------------------------------------+
//|                                                   iTestTable.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"
#property indicator_separate_window
#property indicator_buffers 0
#property indicator_plots   0

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#include "Controls\Controls.mqh"    // Библиотека элементов управления

//--- Указатель на объект CTableControl
CTableControl *table_ctrl;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Ищем подокно графика
   int wnd=ChartWindowFind();

//--- Создаём данные для таблицы
//--- Объявляем и заполняем массив заголовков столбцов с размерностью 4
   string captions[]={"Column 0","Column 1","Column 2","Column 3"};
   
//--- Объявляем и заполняем массив данных с размерностью 15x4
//--- Тип массива может быть double, long, datetime, color, string
   long array[15][4]={{ 1,  2,  3,  4},
                      { 5,  6,  7,  8},
                      { 9, 10, 11, 12},
                      {13, 14, 15, 16},
                      {17, 18, 19, 20},
                      {21, 22, 23, 24},
                      {25, 26, 27, 28},
                      {29, 30, 31, 32},
                      {33, 34, 35, 36},
                      {37, 38, 39, 40},
                      {41, 42, 43, 44},
                      {45, 46, 47, 48},
                      {49, 50, 51, 52},
                      {53, 54, 55, 56},
                      {57, 58, 59, 60}};
                      
//--- Создаём графический элемент управления таблицами
   table_ctrl=new CTableControl("TableControl0",0,wnd,30,30,460,184);
   if(table_ctrl==NULL)
      return INIT_FAILED;

//--- На графике обязательно должен быть один главный элемент
   table_ctrl.SetAsMain();

//--- Можно установить параметры элемента управления таблицами
   table_ctrl.SetID(0);                      // Идентификатор 
   table_ctrl.SetName("Table Control 0");    // Наименование

//--- Создаём объект таблицы 0 (компонент Model + View) из вышесозданного long-массива array 15x4 и string-массива заголовков столбцов
   if(table_ctrl.TableCreate(array,captions)==NULL)
      return INIT_FAILED;
      
//--- Дополнительно установим для столбцов 1,2,3 вывод текста по центру ячейки, а для столбца 0 - по левому краю
   table_ctrl.ColumnSetTextAnchor(0,0,ANCHOR_LEFT,true,false);
   table_ctrl.ColumnSetTextAnchor(0,1,ANCHOR_CENTER,true,false);
   table_ctrl.ColumnSetTextAnchor(0,2,ANCHOR_CENTER,true,false);
   table_ctrl.ColumnSetTextAnchor(0,3,ANCHOR_CENTER,true,false);

//--- Нарисуем таблицу
   table_ctrl.Draw(true);
   
//--- Получим таблицу с индексом 0 и распечатаем в журнале
   CTable *table=table_ctrl.GetTable(0);
   table.Print();
   
//--- Успешно
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom deindicator initialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Удаляем элемент управления таблицами и уничтожаем менеджер общих ресурсов библиотеки
   delete table_ctrl;
   CCommonManager::DestroyInstance();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Вызываем обработчик OnChartEvent элемента управления таблицами
   table_ctrl.OnChartEvent(id,lparam,dparam,sparam);
   
//--- Если событие - перемещение курсора мышки
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- получаем координаты курсора
      int x=table_ctrl.CursorX();
      int y=table_ctrl.CursorY();
      
      //--- значение координаты X устанавливаем в ячейку 0 строки 1
      table_ctrl.CellSetValue(0,1,0,x,false);
      
      //--- значение координаты Y устанавливаем в ячейку 1 строки 1
      //--- цвет текста в ячейке зависит от знака координаты Y (при отрицательном значении - красный текст)
      table_ctrl.CellSetForeColor(0,1,1,(y<0 ? clrRed : table_ctrl.ForeColor()),false);
      table_ctrl.CellSetValue(0,1,1,y,true);
     }
  }
//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
//--- Вызываем обработчик OnTimer элемента управления таблицами
   table_ctrl.OnTimer();
  }
//+------------------------------------------------------------------+

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

Скомпилируем индикатор и запустим его на графике:

Как видим, заявленное взаимодействие таблицы с пользователем работает. 

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



Заключение

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

Есть несколько вариантов данных для создания на их основе таблицы:

  • двумерный массив таблицы и массив заголовков столбцов,
  • количество колонок и строк,
  • матрица: заголовок автоматически создаётся в Excel-стиле,
  • список пользовательских параметров CList и массив заголовков столбцов.

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


Программы, используемые в статье:

#
 Имя Тип
Описание
 1  Tables.mqh  Библиотека классов  Классы для создания модели таблицы
 2  Base.mqh  Библиотека классов  Классы для создания базового объекта элементов управления
 3  Controls.mqh  Библиотека классов  Классы элементов управления
 4  iTestTable.mq5  Тестовый индикатор  Индикатор для тестирования работы с элементом управления TableView
 5  MQL5.zip  Архив  Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала
Все созданные файлы прилагаются к статье для самостоятельного изучения. Файл архива можно распаковать в папку терминала, и все файлы будут расположены в нужной папке: \MQL5\Indicators\Tables\.
Прикрепленные файлы |
Tables.mqh (272.47 KB)
Base.mqh (300.19 KB)
Controls.mqh (804.81 KB)
iTestTable.mq5 (12.11 KB)
MQL5.zip (130.08 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Alexey Viktorov
Alexey Viktorov | 28 окт. 2025 в 16:19
Спасибо Артём. Это действительно очень полезная статья.
Maxim Kuznetsov
Maxim Kuznetsov | 28 окт. 2025 в 18:19
MetaQuotes:
щелчок мышкой по заголовку столбца таблицы (работа компонента Controller) повлечёт за собой изменение в расположении данных в модели таблицы (реорганизация компонента Model),
Артём !! Во всам-делишнем MVC, Щелчок мышью по вьюхе НИЧЕГО не меняет в модели. На одной модели может быть много разных вьюх одновременно с разными сортировками и выборками. 
От новичка до эксперта: Анимированный советник News Headline с использованием MQL5 (XI) - Корреляция при торговле на новостях От новичка до эксперта: Анимированный советник News Headline с использованием MQL5 (XI) - Корреляция при торговле на новостях
В настоящем обсуждении рассмотрим, как концепция финансовой корреляции может быть применена для повышения эффективности принятия решений при торговле несколькими инструментами во время анонсов крупных экономических событий. Основное внимание уделяется решению проблемы повышенной подверженности риску, вызванной повышенной волатильностью во время выпуска новостей.
Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Окончание) Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Окончание)
В статье продолжается работа над реализацией подходов фреймворка STE-FlowNet, который сочетает многопоточную обработку с рекуррентными структурами для точного анализа сложных данных. Проведенные тесты подтвердили его стабильность и гибкость в разных сценариях. Архитектура ускоряет вычисления и позволяет глубже моделировать зависимости во временных рядах. Такой подход открывает новые возможности для практического применения в трейдинге и аналитике.
Нейросети в трейдинге: Модели многократного уточнения прогнозов (RAFT) Нейросети в трейдинге: Модели многократного уточнения прогнозов (RAFT)
Фреймворк RAFT предлагает принципиально иной подход к прогнозированию динамики рынка — не как разовый снимок, а как итеративное уточнение состояния в реальном времени. Он одновременно учитывает локальные и глобальные изменения, сохраняя высокую точность даже при сложных ценовых структурах.
От новичка до эксперта: Создание анимированного советника для новостей в MQL5 (X) — Представление графика с несколькими символами для торговли на новостях От новичка до эксперта: Создание анимированного советника для новостей в MQL5 (X) — Представление графика с несколькими символами для торговли на новостях
Сегодня мы разработаем систему просмотра нескольких диаграмм с использованием объектов диаграмм. Цель состоит в том, чтобы улучшить торговлю на новостях за счет применения алгоритмов на MQL5, которые помогают сократить время реакции трейдера в периоды высокой волатильности, такие как выход крупных новостей. В этом случае мы предоставляем трейдерам интегрированный способ мониторинга нескольких основных инструментов в рамках единого инструмента для торговли на новостях. Наша работа постоянно продвигается с появлением советника News Headline EA («Заголовки новостей»), который теперь обладает растущим набором функций, которые привносят действительное значение как для трейдеров, использующих полностью автоматизированные системы, так и для тех, кто предпочитает ручную торговлю с помощью алгоритмов. Ознакомьтесь с новыми знаниями, информацией и практическими идеями, перейдя по ссылке и присоединившись к настоящему обсуждению.