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

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

MetaTrader 5Примеры |
65 0
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

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

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

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


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

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

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

Доработаем базовый класс графической библиотеки \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    CCaptionView;                    // Класс базового объекта визуального представления заголовка
class    CColumnCaptionView;              // Класс визуального представления заголовка столбца таблицы
class    CRowCaptionView;                 // Класс визуального представления заголовка строки таблицы
class    CTableHeaderView;                // Класс визуального представления заголовка таблицы
class    CTableRowsHeaderView;            // Класс визуального представления заголовка строк таблицы
class    CTableView;                      // Класс визуального представления таблицы
class    CTableControl;                   // Класс управления таблицами
class    CPanel;                          // Класс элемента управления Panel
class    CGroupBox;                       // Класс элемента управления GroupBox
class    CContainer;                      // Класс элемента управления Container

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define  clrNULL              0x00FFFFFF  // Прозрачный цвет для CCanvas
#ifndef  __TABLES__
#define  MARKER_START_DATA    -1          // Маркер начала данных в файле
#endif 
#define  DEF_FONTNAME         "Calibri"   // Шрифт по умолчанию
#define  DEF_FONTSIZE         10          // Размер шрифта по умолчанию
#define  DEF_EDGE_THICKNESS   3           // Толщина зоны для захвата границы/угла

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
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_CAPTION_VIEW,       // Базовый объект заголовка (View)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW,// Заголовок столбца таблицы (View)
   ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW,   // Заголовок строки таблицы (View)
   ELEMENT_TYPE_TABLE_HEADER_VIEW,        // Заголовок таблицы (View)
   ELEMENT_TYPE_TABLE_ROWS_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_CAPTION_VIEW         :  return "TCAPT";   // Базовый объект заголовка (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  return "TCCAPT";  // Заголовок столбца таблицы (View)
      case ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW     :  return "TRCAPT";  // Заголовок строки таблицы (View)
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  return "TCHDR";   // Заголовок таблицы (View)
      case ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW     :  return "TRHDR";   // Заголовок строк таблицы (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
     }
  }

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

//+------------------------------------------------------------------+
//| Класс цветов элемента графического объекта                       |
//+------------------------------------------------------------------+
class CColorElement : public CBaseObj
  {
protected:
   CColor            m_current;                                // Текущий цвет. Может быть одним из нижеследующих
   CColor            m_default;                                // Цвет обычного состояния
   CColor            m_focused;                                // Цвет при наведении курсора
   CColor            m_pressed;                                // Цвет при нажатии
   CColor            m_blocked;                                // Цвет заблокированного элемента
   
//--- Преобразует RGB в color
   color             RGBToColor(const double r,const double g,const double b) const;
//--- Записывает в переменные значения компонентов RGB
   void              ColorToRGB(const color clr,double &r,double &g,double &b);
//--- Возвращает составляющую цвета (1) Red, (2) Green, (3) Blue
   double            GetR(const color clr)                     { return clr&0xFF;                           }
   double            GetG(const color clr)                     { return(clr>>8)&0xFF;                       }
   double            GetB(const color clr)                     { return(clr>>16)&0xFF;                      }
   
public:
//--- Возвращает новый цвет
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Возвращает интерполированный цвет между тремя цветами в зависимости от значения коэффициента (от -1 до +1)
   color             InterpolateColorByCoeff(const color color1, const color color2, const color color3, const double coeff);

//--- Инициализация класса
   void              Init(void);

//--- Инициализация цветов различных состояний
   bool              InitDefault(const color clr)              { return this.m_default.SetColor(clr);       }
   bool              InitFocused(const color clr)              { return this.m_focused.SetColor(clr);       }
   bool              InitPressed(const color clr)              { return this.m_pressed.SetColor(clr);       }
   bool              InitBlocked(const color clr)              { return this.m_blocked.SetColor(clr);       }
   
//--- Установка цветов для всех состояний
   void              InitColors(const color clr_default, const color clr_focused, const color clr_pressed, const color clr_blocked);
   void              InitColors(const color clr);
    
//--- Возврат цветов различных состояний
   color             GetCurrent(void)                    const { return this.m_current.Get();               }
   color             GetDefault(void)                    const { return this.m_default.Get();               }
   color             GetFocused(void)                    const { return this.m_focused.Get();               }
   color             GetPressed(void)                    const { return this.m_pressed.Get();               }
   color             GetBlocked(void)                    const { return this.m_blocked.Get();               }
   
//--- Устанавливает один из списка цветов как текущий
   bool              SetCurrentAs(const ENUM_COLOR_STATE color_state);

//--- Возвращает описание объекта
   virtual string    Description(void);
   
//--- Виртуальные методы (1) сохранения в файл, (2) загрузки из файла, (3) тип объекта
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_COLORS_ELEMENT);       }
   
//--- Конструкторы/деструктор
                     CColorElement(void);
                     CColorElement(const color clr);
                     CColorElement(const color clr_default,const color clr_focused,const color clr_pressed,const color clr_blocked);
                    ~CColorElement(void) {}
  };

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

//+------------------------------------------------------------------+
//| Возвращает интерполированный цвет между тремя цветами            |
//| в зависимости от значения коэффициента (от -1 до +1)             |
//+------------------------------------------------------------------+
color CColorElement::InterpolateColorByCoeff(const color color1,const color color2,const color color3,const double coeff)
  {
//--- Ограничиваем значение коэффициента
   double val=::fmax(-1.0,::fmin(1.0,coeff));

//--- Переменные для получения компонент RGB для каждого цвета
   double r1, g1, b1, r2, g2, b2;
   double r, g, b, t;

//--- Интерполяция между начальным и средним цветом
   if(val<0.0)
     {
      this.ColorToRGB(color1,r1,g1,b1);
      this.ColorToRGB(color2,r2,g2,b2);
      t=(val+1.0)/1.0;
      r=r1+(r2-r1)*t;
      g=g1+(g2-g1)*t;
      b=b1+(b2-b1)*t;
     }
//--- Интерполяция между средним и конечным цветом
   else
     {
      this.ColorToRGB(color3,r1,g1,b1); 
      this.ColorToRGB(color2,r2,g2,b2);
      t=val/1.0;
      r=r2+(r1-r2)*t;
      g=g2+(g1-g2)*t;
      b=b2+(b1-b2)*t;
     }
//--- Возвращаем рассчитанный цвет
   return this.RGBToColor(r,g,b);
  }

Метод вычисляет интерполированный цвет на основе трёх заданных цветов ( color1 , color2 , color3 ) и коэффициента coeff . Первый цвет — это цвет при значении коэффициента -1. Второй цвет — цвет при значении коэффициента 0, последний цвет — это цвет при значении коэффициента +1. Так как значения корреляции между символами колеблются в диапазоне от -1 до +1, то этот метод позволит плавно рассчитать значение цвета ячейки по значению коэффициента (корреляции символов) и вернёт рассчитанный цвет.

В обработчике событий базового класса CCanvasBase впишем контроль размера подокна графика:

//+------------------------------------------------------------------+
//| CCanvasBase::Обработчик событий                                  |
//+------------------------------------------------------------------+
void CCanvasBase::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если в момент запуска терминала с индикатором высота подокна ещё не определена,
//--- скорректируем дистанцию между верхней рамкой подокна индикатора и верхней рамкой главного окна
   if(this.m_wnd>0 && this.m_wnd_y==0)
      this.m_wnd_y=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);

//--- Событие изменения графика
//...
//...

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

Теперь создадим новые классы для вертикального заголовка таблицы в файле \MQL5\Indicators\Tables\Controls\Controls.mqh.

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

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#include "Base.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_ROWS_HEADER_W    24          // Минимальная ширина заголовков строк таблицы
#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"   // Наименование подсказки "Стрелка вертикального смещения"

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
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,                  // Стрелка вертикального смещения
  };

enum ENUM_ROWS_HIGHLIGHT_MODE                   // Режимы подсветки строк/ячеек таблицы
  {
   ROWS_HIGHLIGHT_MODE_CELLS,                   // Подсвечивать отдельные ячейки (режим ячеек)
   ROWS_HIGHLIGHT_MODE_ROW,                     // Подсвечивать всю строку целиком (режим строки)
  };  

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

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

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

  1. CColumnCaptionView — заголовок столбца,
  2. CTableHeaderView — горизонтальный заголовок таблицы, в которм в виде списка содержатся объекты заголовков столбцов.

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

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

  1. CCaptionView — базовый объект заголовка
    • CColumnCaptionView — класс объекта заголовка столбца, унаследованный от CCaptionView,
    • CRowCaptionView — класс объекта заголовка строки, унаследованный от CCaptionView.
  2. CTableHeaderView — горизонтальный заголовок таблицы, содержит список заголовков столбцов CColumnCaptionView,
  3. CTableRowsHeaderView — вертикальный заголовок таблицы, содержит список заголовков строк CRowCaptionView.

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

//+------------------------------------------------------------------+
//| Метод создания элемента списка                                   |
//+------------------------------------------------------------------+
CObject *CListElm::CreateElement(void)
  {
//--- В зависимости от типа объекта в m_element_type, создаём новый объект
   switch(this.m_element_type)
     {
      case ELEMENT_TYPE_BASE                       :  return new CBaseObj();           // Базовый объект графических элементов
      case ELEMENT_TYPE_COLOR                      :  return new CColor();             // Объект цвета
      case ELEMENT_TYPE_COLORS_ELEMENT             :  return new CColorElement();      // Объект цветов элемента графического объекта
      case ELEMENT_TYPE_RECTANGLE_AREA             :  return new CBound();             // Прямоугольная область элемента
      case ELEMENT_TYPE_IMAGE_PAINTER              :  return new CImagePainter();      // Объект для рисования изображений
      case ELEMENT_TYPE_CANVAS_BASE                :  return new CCanvasBase();        // Базовый объект холста графических элементов
      case ELEMENT_TYPE_ELEMENT_BASE               :  return new CElementBase();       // Базовый объект графических элементов
      case ELEMENT_TYPE_HINT                       :  return new CVisualHint();        // Подсказка
      case ELEMENT_TYPE_LABEL                      :  return new CLabel();             // Текстовая метка
      case ELEMENT_TYPE_BUTTON                     :  return new CButton();            // Простая кнопка
      case ELEMENT_TYPE_BUTTON_TRIGGERED           :  return new CButtonTriggered();   // Двухпозиционная кнопка
      case ELEMENT_TYPE_BUTTON_ARROW_UP            :  return new CButtonArrowUp();     // Кнопка со стрелкой вверх
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN          :  return new CButtonArrowDown();   // Кнопка со стрелкой вниз
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT          :  return new CButtonArrowLeft();   // Кнопка со стрелкой влево
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT         :  return new CButtonArrowRight();  // Кнопка со стрелкой вправо
      case ELEMENT_TYPE_CHECKBOX                   :  return new CCheckBox();          // Элемент управления CheckBox
      case ELEMENT_TYPE_RADIOBUTTON                :  return new CRadioButton();       // Элемент управления RadioButton
      case ELEMENT_TYPE_TABLE_CELL_VIEW            :  return new CTableCellView();     // Ячейка таблицы (View)
      case ELEMENT_TYPE_TABLE_ROW_VIEW             :  return new CTableRowView();      // Строка таблицы (View)
      case ELEMENT_TYPE_TABLE_CAPTION_VIEW         :  return new CCaptionView();       // Базовый объект заголовка (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  return new CColumnCaptionView(); // Заголовок столбца таблицы (View)
      case ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW     :  return new CRowCaptionView();    // Заголовок строки таблицы (View)
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  return new CTableHeaderView();   // Заголовок таблицы (View)
      case ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW     :  return new CTableRowsHeaderView();// Вертикальный заголовок таблицы (View)
      case ELEMENT_TYPE_TABLE_VIEW                 :  return new CTableView();         // Таблица (View)
      case ELEMENT_TYPE_PANEL                      :  return new CPanel();             // Элемент управления Panel
      case ELEMENT_TYPE_GROUPBOX                   :  return new CGroupBox();          // Элемент управления GroupBox
      case ELEMENT_TYPE_CONTAINER                  :  return new CContainer();         // Элемент управления GroupBox
      default                                      :  return NULL;
     }
  }

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

Сделаем такой же.

Все рисунки у нас рисуются специальным классом для рисования CImagePainter.

Объявим в классе методы для рисования треугольников, расположенных по всем углам прямоугольной области:

//+------------------------------------------------------------------+
//| Класс рисования изображений                                      |
//+------------------------------------------------------------------+
class CImagePainter : public CBaseObj
  {
protected:
   CCanvas          *m_canvas;                                 // Указатель на канвас, где рисуем
   CBound            m_bound;                                  // Координаты и границы изображения
   uchar             m_alpha;                                  // Прозрачность
   
//--- Проверяет валидность холста и корректность размеров
   bool              CheckBound(const string source);

public:
//--- (1) Назначает канвас для рисования, (2) устанавливает, (3) возвращает прозрачность
   void              CanvasAssign(CCanvas *canvas)             { this.m_canvas=canvas;                }
   void              SetAlpha(const uchar value)               { this.m_alpha=value;                  }
   uchar             Alpha(void)                         const { return this.m_alpha;                 }
   
//--- (1) Устанавливает координаты, (2) изменяет размеры области
   void              SetXY(const int x,const int y)            { this.m_bound.SetXY(x,y);             }
   void              SetSize(const int w,const int h)          { this.m_bound.Resize(w,h);            }
//--- Устанавливает координаты и размеры области
   void              SetBound(const int x,const int y,const int w,const int h)
                       {
                        this.SetXY(x,y);
                        this.SetSize(w,h);
                       }

//--- Возвращает границы и размеры рисунка
   int               X(void)                             const { return this.m_bound.X();             }
   int               Y(void)                             const { return this.m_bound.Y();             }
   int               Right(void)                         const { return this.m_bound.Right();         }
   int               Bottom(void)                        const { return this.m_bound.Bottom();        }
   int               Width(void)                         const { return this.m_bound.Width();         }
   int               Height(void)                        const { return this.m_bound.Height();        }
   
//--- Очищает область
   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);

//--- Рисует рамку группы элементов
   bool              FrameGroupElements(const int x,const int y,const int w,const int h,const string text,
                                        const color clr_text,const color clr_dark,const color clr_light,
                                        const uchar alpha,const bool update=true);
   
//--- Рисует закрашенный треугольник в (1) левом-верхнем, (2) левом-нижнем, (3) правом-верхнем, (4) правом-нижнем углу
   bool              TriangleLT(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              TriangleLB(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              TriangleRT(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              TriangleRB(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);

//--- Виртуальные методы (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_IMAGE_PAINTER);  }
   
//--- Конструкторы/деструктор
                     CImagePainter(void) : m_canvas(NULL)               { this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2); this.SetName("Image Painter");  }
                     CImagePainter(CCanvas *canvas) : m_canvas(canvas)  { this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2); this.SetName("Image Painter");  }
                     CImagePainter(CCanvas *canvas,const int id,const string name) : m_canvas(canvas)
                       {
                        this.m_id=id;
                        this.SetName(name);
                        this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2);
                       }
                     CImagePainter(CCanvas *canvas,const int id,const int dx,const int dy,const int w,const int h,const string name) : m_canvas(canvas)
                       {
                        this.m_id=id;
                        this.SetName(name);
                        this.SetBound(dx,dy,w,h);
                       }
                    ~CImagePainter(void) {}
  };

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

//+------------------------------------------------------------------+
//| Рисует закрашенный треугольник в левом-верхнем углу              |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleLT(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 x1=x;
   int y1=y+h;
   int x2=x1;
   int y2=y;
   int x3=x2+w;
   int y3=y2;
   
//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }
//+------------------------------------------------------------------+
//| Рисует закрашенный треугольник в левом-нижнем углу               |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleLB(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 x1=x;
   int y1=y;
   int x2=x1+w;
   int y2=y1+h;
   int x3=x1;
   int y3=y2;
   
//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }
//+------------------------------------------------------------------+
//| Рисует закрашенный треугольник в правом-верхнем углу             |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleRT(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 x1=x;
   int y1=y;
   int x2=x1+w;
   int y2=y;
   int x3=x2;
   int y3=y2+h;
   
//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }
//+------------------------------------------------------------------+
//| Рисует закрашенный треугольник в правом-нижнем углу              |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleRB(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 x1=x+w;
   int y1=y;
   int x2=x1;
   int y2=y+h;
   int x3=x;
   int y3=y2;
   
//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

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

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

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

//+------------------------------------------------------------------+
//| Класс панели                                                     |
//+------------------------------------------------------------------+
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);
   
//--- Возвращает количество (1) областей, (2) присоединённых элементов,
   int               BoundsTotal(void)                   const { return this.m_list_bounds.Total();               }
   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);

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

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

//+------------------------------------------------------------------+
//| CPanel::Создаёт и добавляет новый элемент в список               |
//+------------------------------------------------------------------+
CElementBase *CPanel::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)
  {
//--- Создаём имя графического объекта
   int elm_total=this.m_list_elm.Total();
   string obj_name=this.NameFG()+"_"+ElementShortName(type)+(string)elm_total;
//--- Рассчитываем координаты
   int x=this.X()+dx;
   int y=this.Y()+dy;
//--- В зависимости от типа объекта, создаём новый объект
   CElementBase *element=NULL;
   switch(type)
     {
      case ELEMENT_TYPE_LABEL                      :  element = new CLabel(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);             break;   // Текстовая метка
      case ELEMENT_TYPE_BUTTON                     :  element = new CButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);            break;   // Простая кнопка
      case ELEMENT_TYPE_BUTTON_TRIGGERED           :  element = new CButtonTriggered(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Двухпозиционная кнопка
      case ELEMENT_TYPE_BUTTON_ARROW_UP            :  element = new CButtonArrowUp(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);     break;   // Кнопка со стрелкой вверх
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN          :  element = new CButtonArrowDown(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Кнопка со стрелкой вниз
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT          :  element = new CButtonArrowLeft(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Кнопка со стрелкой влево
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT         :  element = new CButtonArrowRight(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);  break;   // Кнопка со стрелкой вправо
      case ELEMENT_TYPE_CHECKBOX                   :  element = new CCheckBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // Элемент управления CheckBox
      case ELEMENT_TYPE_RADIOBUTTON                :  element = new CRadioButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);       break;   // Элемент управления RadioButton
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H          :  element = new CScrollBarThumbH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Полоса прокрутки горизонтального ScrollBar
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V          :  element = new CScrollBarThumbV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Полоса прокрутки вертикального ScrollBar
      case ELEMENT_TYPE_SCROLLBAR_H                :  element = new CScrollBarH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Элемент управления горизонтальный ScrollBar
      case ELEMENT_TYPE_SCROLLBAR_V                :  element = new CScrollBarV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Элемент управления вертикальный ScrollBar
      case ELEMENT_TYPE_TABLE_ROW_VIEW             :  element = new CTableRowView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);      break;   // Объект визуального представления строки таблицы
      case ELEMENT_TYPE_TABLE_CAPTION_VIEW         :  element = new CCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);       break;   // Базовый объект заголовка (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  element = new CColumnCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break;   // Объект визуального представления заголовка столбца таблицы
      case ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW     :  element = new CRowCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);    break;   // Объект визуального представления заголовка строки таблицы
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  element = new CTableHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Объект визуального представления заголовка таблицы
      case ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW     :  element = new CTableRowsHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);break;  // Объект визуального представления заголовка строк таблицы
      case ELEMENT_TYPE_TABLE_VIEW                 :  element = new CTableView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Объект визуального представления таблицы
      case ELEMENT_TYPE_PANEL                      :  element = new CPanel(obj_name,"",this.m_chart_id,this.m_wnd,x,y,w,h);               break;   // Элемент управления Panel
      case ELEMENT_TYPE_GROUPBOX                   :  element = new CGroupBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // Элемент управления GroupBox
      case ELEMENT_TYPE_CONTAINER                  :  element = new CContainer(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Элемент управления Container
      default                                      :  element = NULL;
     }

//--- Если новый элемент не создан - сообщаем об этом и возвращаем NULL
   if(element==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create graphic element %s",__FUNCTION__,ElementDescription(type));
      return NULL;
     }
//--- Устанавливаем идентификатор, имя, контейнер и z-order элемента
   element.SetID(elm_total);
   element.SetName(user_name);
   element.SetContainerObj(&this);
   element.ObjectSetZOrder(this.ObjectZOrder()+1);
   
//--- Если созданный элемент не добавлен в список - сообщаем об этом, удаляем созданный элемент и возвращаем NULL
   if(!this.AddNewElement(element))
     {
      ::PrintFormat("%s: Error. Failed to add %s element with ID %d to list",__FUNCTION__,ElementDescription(type),element.ID());
      delete element;
      return NULL;
     }
//--- Получаем родительский элемент, к которому привязаны дочерние
   CElementBase *elm=this.GetContainer();
//--- Если родительский элемент имеет тип "Контейнер", значит, у него есть полосы прокрутки
   if(elm!=NULL && elm.Type()==ELEMENT_TYPE_CONTAINER)
     {
      //--- Преобразуем CElementBase в CContainer
      CContainer *container_obj=elm;
      //--- Если горизонтальная полоса прокрутки видима,
      if(container_obj.ScrollBarHorzIsVisible())
        {
         //--- получаем указатель на горизонтальный скроллбар и переносим его на передний план
         CScrollBarH *sbh=container_obj.GetScrollBarH();
         if(sbh!=NULL)
            sbh.BringToTop(false);
        }
      //--- Если вертикальная полоса прокрутки видима,
      if(container_obj.ScrollBarVertIsVisible())
        {
         //--- получаем указатель на вертикальный скроллбар и переносим его на передний план
         CScrollBarV *sbv=container_obj.GetScrollBarV();
         if(sbv!=NULL)
            sbv.BringToTop(false);
        }
     }
//--- Возвращаем указатель на созданный и присоединённый элемент
   return element;
  }

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

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

//+-------------------------------------------------------------------+
//|CContainer::Смещает содержимое по горизонтали на указанное значение|
//+-------------------------------------------------------------------+
bool CContainer::ContentShiftHorz(const int value)
  {
//--- Получаем указатель на содержимое контейнера
   CElementBase *elm=this.GetAttachedElement();
   if(elm==NULL)
      return false;
   
//--- Рассчитываем величину смещения по положению ползунка
   int content_offset=this.CalculateContentOffsetHorz(value);
   
//--- Для элемента CTableView получаем заголовок таблицы
   bool res=true;
   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();
         //--- Сдвигаем заголовок
         if(table_header!=NULL)
            res &=table_header.MoveX(this.X()-content_offset);
        }
     }

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

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

Теперь в методе, смещающем содержимое по вертикали, тоже добавим смещение вертикального заголовка:

//+------------------------------------------------------------------+
//| CContainer::Смещает содержимое по вертикали на указанное значение|
//+------------------------------------------------------------------+
bool CContainer::ContentShiftVert(const int value)
  {
//--- Получаем указатель на содержимое контейнера
   CElementBase *elm=this.GetAttachedElement();
   if(elm==NULL)
      return false;
   
//--- Рассчитываем величину смещения по положению ползунка
   int content_offset=this.CalculateContentOffsetVert(value);
   
//--- Для элемента CTableView получаем вертикальный заголовок таблицы
   bool res=true;
   CElementBase         *elm_container=elm.GetContainer();
   CTableRowsHeaderView *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.GetRowsHeader();
         //--- Сдвигаем заголовок
         if(table_header!=NULL)
            res &=table_header.MoveY(this.Y()-content_offset);
        }
     }

//--- Возвращаем результат сдвига содержимого на рассчитанную величину
   res &=elm.MoveY(this.Y()-content_offset);
   return res;
  }

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

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

//+------------------------------------------------------------------+
//| Класс визуального представления ячейки таблицы                   |
//+------------------------------------------------------------------+
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;                             // Цвет переднего плана
   color             m_back_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;                  }

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

//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_id=id;                              }
//--- (1) Устанавливает, (2) возвращает индекс ячейки
   void              SetIndex(const int index)                          { this.m_index=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();
   this.m_back_color=this.m_element_base.BackColor();
  }

По умолчанию цвет фона ячейки будет таким же, как и цвет фона строки.

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

//+------------------------------------------------------------------+
//| 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 y1=this.AdjY(this.Y());
   int x2=this.AdjX(this.X());
   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);
   
//--- Устанавливаем координаты прямоугольной заливки
   x1=this.AdjX(this.X());
   y1=this.AdjY(this.Y());
   x2=this.AdjX(this.Right());
   y2=this.AdjY(this.Bottom()-1);
   this.m_background.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.BackColor(),this.m_element_base.AlphaBG()));

//--- Если это не крайняя справа ячейка - рисуем у ячейки справа вертикальную разделительную полосу
   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);
  }

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

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

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

//+------------------------------------------------------------------+
//| Класс визуального представления строки таблицы                   |
//+------------------------------------------------------------------+
class CTableRowView : public CPanel
  {
protected:
   CTableCellView    m_temp_cell;                                    // Временный объект ячейки для поиска
   CTableRow        *m_table_row_model;                              // Указатель на модель строки
   CListElm          m_list_cells;                                   // Список ячеек
   int               m_index;                                        // Индекс в списке строк
   ENUM_ROWS_HIGHLIGHT_MODE m_highlight_mode;                        // Режим подсветки строк
   
//--- Создаёт и добавляет в список новый объект представления ячейки
   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);
//--- Возвращает визуальное представление (1) таблицы, заголовка (2) столбцов, (3) строк
   CTableView       *GetTableView(void);
   CTableHeaderView *GetHeaderView(void);
   CTableRowsHeaderView *GetRowsHeaderView(void);
   
//--- Устанавливает выбранным указанный заголовок (1) столбца, (2) строки
   void              SetColumnCaptionSelected(const uint index);
   void              SetRowCaptionSelected(const uint index);
//--- Снимает выделение со всех заголовков (1) столбца, (2) строки
   void              SetAllColumnCaptionsUnselected(const int exclude=-1);
   void              SetAllRowCaptionsUnselected(const int exclude=-1);
   
public:
//--- Возвращает (1) список, (2) количество ячеек, (3) ячейку, заголовок (4) столбца, (5) строки
   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);  }
   CColumnCaptionView *GetColumnCaption(const uint index);
   CRowCaptionView  *GetRowCaption(const uint index);
   
//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_id=id;                                    }
//--- (1) Устанавливает, (2) возвращает индекс строки
   void              SetIndex(const int index)                          { this.m_index=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);

//--- (1) Устанавливает, (2) возвращает режим подсветки строки
   void              SetHighlightMode(const ENUM_ROWS_HIGHLIGHT_MODE mode) { this.m_highlight_mode=mode;                   }
   ENUM_ROWS_HIGHLIGHT_MODE HighlightMode(void)                   const { return this.m_highlight_mode;                    }
   
//--- Перерассчитывает области ячеек
   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);

//--- Обработчики событий (1) наведения курсора (Focus), (2) нажатий кнопок мышки (Press),
   virtual void      OnFocusEvent(const int id, const long lparam, const double dparam, const string sparam);
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Конструкторы/деструктор
                     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::Конструктор по умолчанию. Строит объект в главном |
//| окне текущего графика в координатах 0,0 с размерами по умолчанию |
//+------------------------------------------------------------------+
CTableRowView::CTableRowView(void) : CPanel("TableRow","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1), m_highlight_mode(ROWS_HIGHLIGHT_MODE_ROW)
  {
//--- Инициализация
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableRowView::Конструктор параметрический. Строит объект в      |
//| указанном окне указанного графика с указанными текстом,          |
//| координатами и размерами                                         |
//+------------------------------------------------------------------+
CTableRowView::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) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h), m_index(-1), m_highlight_mode(ROWS_HIGHLIGHT_MODE_ROW)
  {
//--- Инициализация
   this.Init();
  }

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Возвращает визуальное представление таблицы       |
//+------------------------------------------------------------------+
CTableView *CTableRowView::GetTableView(void)
  {
   CTableView *obj=NULL;
//--- Получаем панель со строками таблицы
   CElementBase *base0=this.GetContainer();
   if(base0==NULL)
      return NULL;
   
//--- Получаем контейнер панели строк таблицы
   CElementBase *base1=base0.GetContainer();
   if(base1==NULL)
      return NULL;
   
//--- Получаем объект визуального представления таблицы
   CElementBase *base2=base1.GetContainer();
   if(base2!=NULL && base2.Type()==ELEMENT_TYPE_TABLE_VIEW)
     {
      obj=base2;
      return obj;
     }
   return NULL;
  }

Таблица организована следующим образом: 

На панели с типом "Таблица" (1) размещён заголовок таблицы и контейнер (2), внутри которого размещена прокручиваемая панель (3), на которой в свою очередь расположены строки таблицы (4), являющиеся текущим рассматриваемым классом.
Для получения объекта таблицы (1), необходимо получить базовый объект-панель (3), к которому прикреплена строка. Далее из панели (3) получаем его базовый объект-контейнер (2), а из контейнера — его базовый объект-таблицу (1).

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

//+------------------------------------------------------------------+
//| CTableRowView::Возвращает визуальное представление               |
//| заголовка столбцов                                               |
//+------------------------------------------------------------------+
CTableHeaderView *CTableRowView::GetHeaderView(void)
  {
   CTableView *table=this.GetTableView();
   return(table!=NULL ? table.GetHeader() : NULL);
  }

Здесь: получаем объект "таблица" методом, рассмотренным выше, а из него — объект горизонтальный заголовок.

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

//+------------------------------------------------------------------+
//|CTableRowView::Возвращает визуальное представление заголовка строк|
//+------------------------------------------------------------------+
CTableRowsHeaderView *CTableRowView::GetRowsHeaderView(void)
  {
   CTableView *table=this.GetTableView();
   return(table!=NULL ? table.GetRowsHeader() : NULL);
  }

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Возвращает заголовок столбца                      |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableRowView::GetColumnCaption(const uint index)
  {
   CTableHeaderView *header=this.GetHeaderView();
   return(header!=NULL ? header.GetColumnCaption(index) : NULL);
  }

Здесь: сначала получаем объект горизонтального заголовка при помощи метода, рассмотренного выше, а из него — указанный по индексу заголовок столбца.

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

//+------------------------------------------------------------------+
//| CTableRowView::Возвращает заголовок строки                       |
//+------------------------------------------------------------------+
CRowCaptionView *CTableRowView::GetRowCaption(const uint index)
  {
   CTableRowsHeaderView *header=this.GetRowsHeaderView();
   return(header!=NULL ? header.GetRowCaption(index) : NULL);
  }

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

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

//+------------------------------------------------------------------+
//|CTableRowView::Устанавливает выбранным указанный заголовок столбца|
//+------------------------------------------------------------------+
void CTableRowView::SetColumnCaptionSelected(const uint index)
  {
   CColumnCaptionView *capt=this.GetColumnCaption(index);
   if(capt==NULL || capt.State()==ELEMENT_STATE_ACT)
      return;
   capt.SetState(ELEMENT_STATE_ACT);
   capt.GetBackground().FillRectangle(0,capt.Height()-2,capt.Width()-1,capt.Height()-1,ColorToARGB(clrCadetBlue));
   capt.GetBackground().Update(false);
  }

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Устанавливает выбранным указанный заголовок строки|
//+------------------------------------------------------------------+
void CTableRowView::SetRowCaptionSelected(const uint index)
  {
   CRowCaptionView *capt=this.GetRowCaption(index);
   if(capt==NULL || capt.State()==ELEMENT_STATE_ACT)
      return;
   capt.SetState(ELEMENT_STATE_ACT);
   capt.GetBackground().FillRectangle(capt.Width()-2,2,capt.Width()-1,capt.Height()-0,ColorToARGB(clrCadetBlue));
   capt.GetBackground().Update(false);
  }

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Снимает выделение со всех заголовков столбца      |
//+------------------------------------------------------------------+
void CTableRowView::SetAllColumnCaptionsUnselected(const int exclude=-1)
  {
   CTableHeaderView *header=this.GetHeaderView();
   if(header==NULL)
      return;
   int total=header.BoundsTotal();
   for(int i=0;i<total;i++)
     {
      CColumnCaptionView *capt=this.GetColumnCaption(i);
      if(capt==NULL || (exclude>-1 && i==exclude))
         continue;
      if(capt.State()!=ELEMENT_STATE_DEF)
        {
         capt.SetState(ELEMENT_STATE_DEF);
         capt.Draw(false);
        }
     }
  }

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Снимает выделение со всех заголовков строки       |
//+------------------------------------------------------------------+
void CTableRowView::SetAllRowCaptionsUnselected(const int exclude=-1)
  {
   CTableRowsHeaderView *header=this.GetRowsHeaderView();
   if(header==NULL)
      return;
   int total=header.BoundsTotal();
   for(int i=0;i<total;i++)
     {
      CRowCaptionView *capt=this.GetRowCaption(i);
      if(capt==NULL || (exclude>-1 && capt.ID()==exclude))
         continue;
      if(capt.State()!=ELEMENT_STATE_DEF)
        {
         capt.SetState(ELEMENT_STATE_DEF);
         capt.Draw(false);
        }
     }
  }

Логика метода идентична логике вышерассмотренного метода.

Обработчик наведения курсора:

//+------------------------------------------------------------------+
//| CTableRowView::Обработчик наведения курсора                      |
//+------------------------------------------------------------------+
void CTableRowView::OnFocusEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Если обрабатывается целиком строка - вызываем обработчик событий родительского класса
   if(this.m_highlight_mode==ROWS_HIGHLIGHT_MODE_ROW)
     {
      CCanvasBase::OnFocusEvent(id,lparam,dparam,sparam);
      return;
     }

//--- Получаем координаты курсора
   int x=int(lparam-this.X());
   int y=int(dparam-this.m_wnd_y-this.Y());

//--- В цикле по областям ячеек строки
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередную область
      CBound *bound=this.GetBoundAt(i);
      if(bound==NULL)
         continue;

      //--- Из текущей области получаем назначенный на неё элемент
      CBaseObj *obj=bound.GetAssignedObj();
      CTableCellView *cell=NULL;
      
      //--- Если полученный элемент не является ячейкой таблицы - идём дальше
      if(obj==NULL || obj.Type()!=ELEMENT_TYPE_TABLE_CELL_VIEW)
         continue;

      //--- Это ячейка таблицы. Определяем её координаты в таблице (строка/столбец)
      cell=obj;
      int row=this.ID();
      int col=obj.ID();
      
      //--- Получаем соответствующие заголовки строки и столбца
      CColumnCaptionView *col_capt=this.GetColumnCaption(col);
      CRowCaptionView    *row_capt=this.GetRowCaption(row);
      if(col_capt==NULL || row_capt==NULL)
         continue;
      
      //--- Если курсор находится на области ячейки
      if(bound.Contains(x,y))
        {
         //--- Устанавливаем заголовок столбца и строки выбранными,
         this.SetColumnCaptionSelected(i);
         this.SetRowCaptionSelected(this.ID());
         //--- со всех заголовков строк, кроме текущего, снимаем флаг выбранного заголовка
         this.SetAllRowCaptionsUnselected(this.ID());
        }
      //--- Если курсор за пределами области ячейки
      else
        {
         //--- Если заголовок выбран,
         if(col_capt.State()!=ELEMENT_STATE_DEF)
           {
            //--- снимаем с него выделение и перерисовываем объект не выбранным
            col_capt.SetState(ELEMENT_STATE_DEF);
            col_capt.Draw(false);
           }
        }
     }
  }

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

Обработчик нажатия на объект:

//+------------------------------------------------------------------+
//| CTableRowView::Обработчик нажатия на объект                      |
//+------------------------------------------------------------------+
void CTableRowView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Если обрабатывается целиком строка - вызываем обработчик событий родительского класса
   if(this.m_highlight_mode==ROWS_HIGHLIGHT_MODE_ROW)
     {
      CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
      return;
     }

//--- В цикле по всем областям строки
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередную область
      CBound *bound=this.GetBoundAt(i);
      if(bound==NULL)
         continue;

      //--- Получаем координаты курсора и
      int x=int(lparam-this.X());
      int y=int(dparam-this.m_wnd_y-this.Y());
      //--- проверяем, что курсор находится внутри области
      if(bound.Contains(x,y))
        {
         //--- Получаем из области прикреплённый объект (ячейка)
         CBaseObj *obj=bound.GetAssignedObj();
         if(obj!=NULL)
           {
            //--- Записываем адрес ячейки в таблице (строка/столбец)
            int row=this.ID();
            int col=obj.ID();
            
            //--- На основе идентификаторов строки и столбца получаем указатели на соответствующие заголовки
            CRowCaptionView    *row_capt=this.GetRowCaption(row);
            CColumnCaptionView *col_capt=this.GetColumnCaption(col);
            if(row_capt==NULL || col_capt==NULL)
               return;
            
            //--- Создаём текстовое значение для пользовательского события из имени строки и текстов заголовков
            string sprm=obj.Name()+";"+row_capt.Text()+";"+col_capt.Text();
            //--- Посылаем пользовательское событие щелчка по объекту с координатами строки и столбца и текстом
            ::EventChartCustom(this.m_chart_id,CHARTEVENT_OBJECT_CLICK,row,col,sprm);
           }
        }
     }
  }

Логика метода расписана в его комментариях. Если обрабатывается полностью вся строка, то вызывается обработчик "объект в фокусе" родительского класса. Иначе — ищется ячейка по её координатам в таблице и, если курсор на неё наведён, то создаём строку из текстов заголовков строки и столбца (имя символа в контексте данной статьи) с разделителем ";" и отсылается пользовательское событие щелчка по объекту с указанием индексов строки (lparam) и столбца (dparam) и созданной строкой из текстов заголовков в sparam. Далее это событие можно получить в программе и определить по какой ячейке был щелчок, чтобы обработать это событие.

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

//+------------------------------------------------------------------+
//| CTableRowView::Сохранение в файл                                 |
//+------------------------------------------------------------------+
bool CTableRowView::Save(const int file_handle)
  {
//--- Сохраняем данные родительского объекта
   if(!CPanel::Save(file_handle))
      return false;

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

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

Абстрактный класс визуального представления заголовка:

//+------------------------------------------------------------------+
//| Абстрактный класс визуального представления заголовка            |
//+------------------------------------------------------------------+
class CCaptionView : public CButton
  {
protected:
   CBound           *m_bound_node;                                   // Указатель на область заголовка
   int               m_index;                                        // Индекс в списке строк
   
public:
//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_id=id;                              }
//--- (1) Устанавливает, (2) возвращает индекс строки
   void              SetIndex(const int index)                          { this.m_index=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) стрелку направления сортировки
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (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_CAPTION_VIEW);   }
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Возвращает описание объекта
   virtual string    Description(void);
   
//--- Конструкторы/деструктор
                     CCaptionView(void);
                     CCaptionView(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); 
                    ~CCaptionView (void){}
  };
//+------------------------------------------------------------------+
//| CCaptionView::Конструктор по умолчанию. Строит объект            |
//| в главном окне текущего графика в координатах 0,0                |
//| с размерами по умолчанию                                         |
//+------------------------------------------------------------------+
CCaptionView::CCaptionView(void) : CButton("Caption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0)
  {
//--- Инициализация
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   this.SetName("Caption");
  }
//+------------------------------------------------------------------+
//| CCaptionView::Конструктор параметрический.                       |
//| Строит объект в указанном окне указанного графика с              |
//| указанными текстом, координатами и размерами                     |
//+------------------------------------------------------------------+
CCaptionView::CCaptionView(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)
  {
//--- Инициализация
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Инициализация                                      |
//+------------------------------------------------------------------+
void CCaptionView::Init(const string text)
  {
//--- Смещения текста по умолчанию
   this.m_text_x=4;
   this.m_text_y=2;
//--- Устанавливаем цвета различных состояний
   this.InitColors();
//--- Возможно изменять размеры
   this.SetResizable(false);
   this.SetMovable(false);
   this.SetImageBound(this.ObjectWidth()-14,4,8,11);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Инициализация цветов объекта по умолчанию          |
//+------------------------------------------------------------------+
void CCaptionView::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(C'230,230,230',C'159,213,183',this.GetBackColorControl().NewColor(C'159,213,183',-6,-6,-6),clrSilver);
   this.InitBackColorsAct(C'230,230,230',C'159,213,183',this.GetBackColorControl().NewColor(C'159,213,183',-6,-6,-6),clrSilver);
   this.BackColorToDefault();
   
//--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки
   this.InitBorderColors(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.InitBorderColorsAct(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.BorderColorToDefault();
   
//--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента
   this.InitBorderColorBlocked(clrNULL);
   this.InitForeColorBlocked(clrSilver);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Рисует внешний вид                                 |
//+------------------------------------------------------------------+
void CCaptionView::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);
      
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Возвращает описание объекта                        |
//+------------------------------------------------------------------+
string CCaptionView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height());
  }
//+------------------------------------------------------------------+
//| CCaptionView::Сохранение в файл                                  |
//+------------------------------------------------------------------+
bool CCaptionView::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;
      
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CCaptionView::Загрузка из файла                                  |
//+------------------------------------------------------------------+
bool CCaptionView::Load(const int file_handle)
  {
//--- Загружаем данные родительского объекта
   if(!CButton::Load(file_handle))
      return false;
      
//--- Загружаем номер заголовка
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Всё успешно
   return true;
  }

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

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

Рассмотрим класс целиком:

//+------------------------------------------------------------------+
//| Класс визуального представления заголовка столбца таблицы        |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CCaptionView
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Указатель на модель заголовка столбца
   ENUM_TABLE_SORT_MODE m_sort_mode;                                 // Режим сортировки столбца таблицы
   bool              m_sortable;                                     // Флаг управления сортировкой
   
//--- Добавляет в список объекты-подсказки со стрелками
   virtual bool      AddHintsArrowed(void);
//--- Отображает курсор изменения размеров
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
public:
//--- (1) Назначает, (2) возвращает модель заголовка столбца
   bool              ColumnCaptionModelAssign(CColumnCaption *caption_model);
   CColumnCaption   *ColumnCaptionModel(void)                           { return this.m_column_caption_model;        }

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

//--- (1) Устанавливает, (2) возвращает флаг возможности сортировки
   void              SetSortableFlag(const bool flag)
                       {
                        this.m_sortable=flag;
                        this.SetSortMode(flag ? TABLE_SORT_MODE_ASC : TABLE_SORT_MODE_NONE);
                       }
   bool              IsSortabe(void)                              const { return this.m_sortable;                    }

//--- (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) : CCaptionView("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Инициализация
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   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) :
   CCaptionView(object_name,text,chart_id,wnd,x,y,w,h),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Инициализация
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Инициализация                                |
//+------------------------------------------------------------------+
void CColumnCaptionView::Init(const string text)
  {
//--- Инициализация родительского объекта
   CCaptionView::Init(text);

//--- Возможно изменять размеры
   this.SetResizable(true);
   this.SetMovable(false);
  }
//+------------------------------------------------------------------+
//| 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(), 20, 20, 20);      // "Светлый цвет"
   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())); // Линия справа

//--- Выводим текст заголовка
   CLabel::Draw(false);
      
//--- Рисуем стрелки направления сортировки
   this.DrawSortModeArrow();

//--- обновляем канвас фона
   this.m_background.Update(false);
   
//--- Если указано - обновляем график
   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::Возвращает описание объекта                  |
//+------------------------------------------------------------------+
string CColumnCaptionView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   string sort=(this.SortMode()==TABLE_SORT_MODE_ASC ? "ascending" : this.SortMode()==TABLE_SORT_MODE_DESC ? "descending" : "none");
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d, sort %s",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height(),sort);
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Назначает модель заголовка столбца           |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ColumnCaptionModelAssign(CColumnCaption *caption_model)
  {
//--- Если передан невалидный объект модели заголовка столбца - сообщаем об этом и возвращаем false
   if(caption_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Сохраняем модель заголовка столбца
   this.m_column_caption_model=caption_model;
//--- Устанавливаем размеры области рисования визуального представления заголовка столбца
   this.m_painter.SetBound(0,0,this.Width(),this.Height());
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Распечатывает в журнале                      |
//| назначенную модель заголовка столбца                             |
//+------------------------------------------------------------------+
void CColumnCaptionView::ColumnCaptionModelPrint(void)
  {
   if(this.m_column_caption_model!=NULL)
      this.m_column_caption_model.Print();
  }
//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+
//| 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;
//--- Меняем стрелку направления сортировки на обратную и вызываем обработчик щелчка мышки
   if(this.m_sortable)
      this.SetSortModeReverse();
   CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
   ::EventChartCustom(this.m_chart_id,CHARTEVENT_OBJECT_CLICK,this.ID(),-(10000+this.SortMode()),this.NameFG());
  }
//+------------------------------------------------------------------+
//| 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;
//--- Сохраняем флаг управления сортировкой
   if(::FileWriteInteger(file_handle,this.m_sortable,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_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем направление сортировки
   this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем флаг управления сортировкой
   this.m_sortable=(bool)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Всё успешно
   return true;
  }

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

class CColumnCaptionView : public CCaptionView
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Указатель на модель заголовка столбца
   ENUM_TABLE_SORT_MODE m_sort_mode;                                 // Режим сортировки столбца таблицы
   bool              m_sortable;                                     // Флаг управления сортировкой
   
//--- Добавляет в список объекты-подсказки со стрелками
   virtual bool      AddHintsArrowed(void);
//--- Отображает курсор изменения размеров
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
public:
//--- (1) Назначает, (2) возвращает модель заголовка столбца
   bool              ColumnCaptionModelAssign(CColumnCaption *caption_model);
   CColumnCaption   *ColumnCaptionModel(void)                           { return this.m_column_caption_model;        }

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

//--- (1) Устанавливает, (2) возвращает флаг возможности сортировки
   void              SetSortableFlag(const bool flag)
                       {
                        this.m_sortable=flag;
                        this.SetSortMode(flag ? TABLE_SORT_MODE_ASC : TABLE_SORT_MODE_NONE);
                       }
   bool              IsSortabe(void)                              const { return this.m_sortable;                    }

//--- (1) Устанавливает, (2) возвращает режим сортировки

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

В конструкторах класса его значение по умолчанию установлено в true:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Конструктор по умолчанию. Строит объект      |
//| в главном окне текущего графика в координатах 0,0                |
//| с размерами по умолчанию                                         |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(void) : CCaptionView("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Инициализация
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   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) :
   CCaptionView(object_name,text,chart_id,wnd,x,y,w,h),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Инициализация
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
  }

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

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

В пользовательском событии в lparam указываем идентификатор заголовка, а в dparam — отрицательное значение режима сортировки, увеличенное на 10000, чтобы точно знать, что это не координата курсора. В 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;
//--- Сохраняем флаг управления сортировкой
   if(::FileWriteInteger(file_handle,this.m_sortable,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_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем направление сортировки
   this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем флаг управления сортировкой
   this.m_sortable=(bool)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Всё успешно
   return true;
  }

На основании этого доработанного класса создадим новый класс заголовка строки, также унаследованного от абстрактного класса заголовка:

//+------------------------------------------------------------------+
//| Класс визуального представления заголовка строки таблицы         |
//+------------------------------------------------------------------+
class CRowCaptionView : public CCaptionView
  {
protected:
   
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

public:  
//--- Виртуальные методы (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_ROW_CAPTION_VIEW);}
  
//--- Инициализация объекта класса
   void              Init(const string text);
   
//--- Возвращает описание объекта
   virtual string    Description(void);
   
//--- Конструкторы/деструктор
                     CRowCaptionView(void);
                     CRowCaptionView(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); 
                    ~CRowCaptionView (void){}
  };
//+------------------------------------------------------------------+
//| CRowCaptionView::Конструктор по умолчанию. Строит объект         |
//| в главном окне текущего графика в координатах 0,0                |
//| с размерами по умолчанию                                         |
//+------------------------------------------------------------------+
CRowCaptionView::CRowCaptionView(void) : CCaptionView("RowCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H)
  {
//--- Инициализация
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   this.SetName("RowCaption");
   this.SetTextShiftH(8);
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Конструктор параметрический.                    |
//| Строит объект в указанном окне указанного графика с              |
//| указанными текстом, координатами и размерами                     |
//+------------------------------------------------------------------+
CRowCaptionView::CRowCaptionView(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) :
   CCaptionView(object_name,text,chart_id,wnd,x,y,w,h)
  {
//--- Инициализация
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
   this.SetTextShiftH(8);
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Инициализация                                   |
//+------------------------------------------------------------------+
void CRowCaptionView::Init(const string text)
  {
//--- Инициализация родительского объекта
   CCaptionView::Init(text);
//--- Размеры не изменяемые
   this.SetResizable(false);
   this.SetMovable(false);
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Рисует внешний вид                              |
//+------------------------------------------------------------------+
void CRowCaptionView::Draw(const bool chart_redraw)
  {
//--- Если объект за пределами своего контейнера - уходим
   if(this.IsOutOfContainer())
      return;

//--- Заливаем объект цветом фона, рисуем слева светлую вертикальную линию, справа - тёмную
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(2),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
//--- обновляем канвас фона
   this.m_background.Update(false);
   
//--- Выводим текст заголовка
   CLabel::Draw(false);
      
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Возвращает описание объекта                     |
//+------------------------------------------------------------------+
string CRowCaptionView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height());
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Сохранение в файл                               |
//+------------------------------------------------------------------+
bool CRowCaptionView::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;
      
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Загрузка из файла                               |
//+------------------------------------------------------------------+
bool CRowCaptionView::Load(const int file_handle)
  {
//--- Загружаем данные родительского объекта
   if(!CButton::Load(file_handle))
      return false;
      
//--- Загружаем номер заголовка
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Всё успешно
   return true;
  }

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

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

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

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

//--- Создаёт и добавляет в список новый объект представления заголовка столбца
   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);

//--- (1) Устанавливает, (2) возвращает флаг возможности сортировки
   void              SetSortableFlag(const bool flag);
   bool              IsSortabe(void)                              const { return this.m_sortable;                 }

//--- Устанавливает заголовку столбца флаг сортировки
   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::Конструктор по умолчанию. Строит объект в      |
//| главном окне  текущего графика в координатах 0,0                 |
//| с размерами по умолчанию                                         |
//+------------------------------------------------------------------+
CTableHeaderView::CTableHeaderView(void) : CPanel("TableHeader","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H),m_sortable(true)
  {
//--- Инициализация
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableHeaderView::Конструктор параметрический. Строит объект в   |
//| указанном окне указанного графика с указанными текстом,          |
//| координатами и размерами                                         |
//+------------------------------------------------------------------+
CTableHeaderView::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) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_sortable(true)
  {
//--- Инициализация
   this.Init();
  }

В методе установки модели заголовка теперь контролируем этот флаг:

//+------------------------------------------------------------------+
//| 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_view.SetIndex(i);
      
      //--- На текущую область заголовка столбца назначаем соответствующий объект визуального представления заголовка столбца
      caption_bound.AssignObject(caption_view);
      caption_view.AssignBoundNode(caption_bound);
      
      //--- Для самого первого заголовка устанавливаем флаг сортировки по возрастанию
      if(i==0 && caption_view.IsSortabe())
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
     }
//--- Всё успешно
   return true;
  }

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Устанавливает флаг возможности сортировки      |
//+------------------------------------------------------------------+
void CTableHeaderView::SetSortableFlag(const bool flag)
  {
//--- Записываем значение флага
   this.m_sortable=flag;
   
//--- В цикле по количеству заголовков столбцов 
   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.SetSortableFlag(flag);
     }
//--- Если сортируемая таблица - ставим нулевой столбец сортированным по возрастанию
   if(this.m_sortable)
      this.SetSortedColumnCaption(0);

//--- Перерисовываем заголовок
   this.Draw(true);
  }

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

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

//+------------------------------------------------------------------+
//| 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,6,capt_str.Length()-8);
   
//--- Первый символ перед "FG" (последняя цифра искомого индекса)
   int pos=(int)capt_str.Length()-3;
   int end=pos;
   
//--- Ищем все цифры слева до первой "не цифры"
   while(!::IsStopped() && pos>=0 && capt_str.GetChar(pos)>='0' && capt_str.GetChar(pos)<='9')
      pos--;

//--- Начало цифр искомого индекса
   int start=pos+1;
//--- Если цифры индекса не найдены - уходим
   if(start>end)
      return;

//--- Получаем индекс из строки
   index_str=StringSubstr(capt_str,start,end-start+1);

//--- Записываем индекс заголовка столбца
   int index=(int)::StringToInteger(index_str);
   
//--- Получаем заголовок столбца по индексу
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- Если заголовок не имеет флага сортировки - ставим флаг сортировки по возрастанию
   if(caption.IsSortabe() && 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, -(10000+index), -(10000+caption.SortMode()), this.NameFG());
   ::ChartRedraw(this.m_chart_id);
  }

На основании доработанного класса горизонтального заголовка таблицы создадим новый класс вертикального заголовка таблицы:

//+------------------------------------------------------------------+
//| Класс визуального представления заголовка строк таблицы          |
//+------------------------------------------------------------------+
class CTableRowsHeaderView : public CPanel
  {
protected:
   CRowCaptionView   m_temp_caption;                                 // Временный объект заголовка строки для поиска
   string            m_table_row_columns[];                          // Массив заголовков строк таблицы

//--- Создаёт и добавляет в список новый объект представления заголовка строки
   CRowCaptionView *InsertNewRowCaptionView(const string text, const int x, const int y, const int w, const int h);
   
public:
//--- (1) Устанавливает массив заголовков строк таблицы
   bool              TableRowCaptionsAssign(string &captions_array[]);

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

//--- Распечатывает в журнале назначенную модель заголовка таблицы
   void              TableRowHeaderModelPrint(void)                     { ::ArrayPrint(this.m_table_row_columns);       }
   
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);
   
//--- Получает заголовок строки по индексу
   CRowCaptionView  *GetRowCaption(const uint index);
   
//--- Виртуальные методы (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_ROWS_HEADER_VIEW);  }
   
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);
   virtual void      InitColors(void);

//--- Конструкторы/деструктор
                     CTableRowsHeaderView(void);
                     CTableRowsHeaderView(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);
                    ~CTableRowsHeaderView (void){}
  };
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Конструктор по умолчанию. Строит объект в  |
//| главном окне  текущего графика в координатах 0,0                 |
//| с размерами по умолчанию                                         |
//+------------------------------------------------------------------+
CTableRowsHeaderView::CTableRowsHeaderView(void) : CPanel("TableRowHeader","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H)
  {
//--- Инициализация
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Конструктор параметрический. Строит объект |
//| в указанном окне указанного графика с указанными текстом,        |
//| координатами и размерами                                         |
//+------------------------------------------------------------------+
CTableRowsHeaderView::CTableRowsHeaderView(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(object_name,text,chart_id,wnd,x,y,w,h)
  {
//--- Инициализация
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Инициализация                              |
//+------------------------------------------------------------------+
void CTableRowsHeaderView::Init(void)
  {
//--- Инициализация родительского объекта
   CPanel::Init();
//--- Цвет фона - непрозрачный
   this.SetAlphaBG(255);
//--- Ширина рамки
   this.SetBorderWidth(1);
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Инициализация цветов объекта по умолчанию  |
//+------------------------------------------------------------------+
void CTableRowsHeaderView::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrWhiteSmoke);
   this.InitBackColorsAct(C'230,230,230',C'230,230,230',C'230,230,230',clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки
   this.InitBorderColors(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.InitBorderColorsAct(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.BorderColorToDefault();
   
//--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента
   this.InitBorderColorBlocked(clrSilver);
   this.InitForeColorBlocked(clrSilver);
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Создаёт и добавляет в список               |
//| новый объект представления заголовка строки                      |
//+------------------------------------------------------------------+
CRowCaptionView *CTableRowsHeaderView::InsertNewRowCaptionView(const string text,const int x,const int y,const int w,const int h)
  {
//--- Создаём наименование объекта и возвращаем результат создания нового заголовка столбца
   string user_name="RowCaptionView"+(string)this.m_list_elm.Total();
   CRowCaptionView *caption_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW,text,user_name,x,y,w,h);
   return(caption_view!=NULL ? caption_view : NULL);
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Устанавливает вертикальный заголовок       |
//+------------------------------------------------------------------+
bool CTableRowsHeaderView::TableRowCaptionsAssign(string &captions_array[])
  {
//--- Получаем указатель на объект таблицы (View)
   CPanel *obj=this.GetContainer();
   if(obj==NULL)
      return false;
   CTableView *table_view=obj.GetContainer();
   if(table_view==NULL)
      return false;
      
//--- Из объекта таблицы получаем указатель на панель со строками таблицы
   CPanel *table_area=table_view.GetTableArea();
   if(table_area==NULL)
      return false;
   
//--- Получаем список строк таблицы
   CListElm *list=table_area.GetListAttachedElements();
   int total_rows=list.Total();

//--- Сохраняем переданный массив заголовков строк таблицы
   ::ArrayCopy(this.m_table_row_columns,captions_array);
   int total_captions=(int)this.m_table_row_columns.Size();
//---
   int total=::fmax(total_rows,total_captions);
//--- Проходим в цикле по количеству создаваемых заголовков
   for(int i=0;i<total;i++)
     {
      //--- получаем очередную строку
      CTableRowView *row=table_area.GetAttachedElementAt(i);
      if(row==NULL)
         continue;
      
      //--- рассчитываем координату и создаём имя для области заголовка строки
      int y=row.Height()*i;
      string name="CaptionBound"+(string)i;
      //--- Создаём новую область заголовка строки
      CBound *caption_bound=this.InsertNewBound(name,0,y,this.Width(),row.Height());
      if(caption_bound==NULL)
         return false;
      caption_bound.SetID(row.ID());
      //--- Определяем текст для заголовка строки
      //--- Если массив заголовков меньше, чем строк в таблице, то сначала заголовки будут иметь значения из массива, а затем - номера строк
      //--- Если массив заголовков не имеет размера, то все строки будут озаглавлены порядковыми номерами
      string text=(this.m_table_row_columns.Size()>0 ? (i<(int)this.m_table_row_columns.Size() ? this.m_table_row_columns[i] : string(i+1)) : string(i+1));
      //--- Создаём новый объект визуального представления заголовка строки
      CRowCaptionView *caption_view=this.InsertNewRowCaptionView(text,0,y,this.Width(),row.Height());
      if(caption_view==NULL)
         return false;
      caption_view.SetIndex(i);
      
      //--- На текущую область заголовка строки назначаем соответствующий объект визуального представления заголовка строки
      caption_bound.AssignObject(caption_view);
      caption_view.AssignBoundNode(caption_bound);
     }
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Перерассчитывает области заголовков        |
//+------------------------------------------------------------------+
bool CTableRowsHeaderView::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)
   CPanel *obj=this.GetContainer();
   if(obj==NULL)
      return false;
   CTableView *table_view=obj.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;
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Получает заголовок строки по индексу       |
//+------------------------------------------------------------------+
CRowCaptionView *CTableRowsHeaderView::GetRowCaption(const uint index)
  {
//--- Получаем область заголовка строки по индексу
   CBound *capt_bound=this.GetBoundAt(index);
   if(capt_bound==NULL)
      return NULL;
//--- Из области заголовка строки возвращаем указатель на присоединённый объект заголовка строки
   return capt_bound.GetAssignedObj();
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Рисует внешний вид                         |
//+------------------------------------------------------------------+
void CTableRowsHeaderView::Draw(const bool chart_redraw)
  {
//--- Заливаем объект цветом фона, рисуем линию строки и обновляем канвас фона
   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()));
   this.m_background.Update(false);
   
//--- Рисуем заголовки строк
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Получаем объект заголовка строки по индексу цикла
      CRowCaptionView *caption_view=this.GetRowCaption(i);
      //--- Рисуем визуальное представление заголовка строки
      if(caption_view!=NULL)
        {
         caption_view.Draw(false);
        }
     }
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

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

//+------------------------------------------------------------------+
//| Класс визуального представления таблицы                          |
//+------------------------------------------------------------------+
class CTableView : public CPanel
  {
private:
   int               m_rows_header_panel_w;        // Ширина при создании панели заголовка строк таблицы
   
protected:
//--- Получаемые данные таблицы
   CTable           *m_table_obj;                  // Указатель на объект таблицы (включает модели таблицы и заголовка)
   CTableModel      *m_table_model;                // Указатель на модель таблицы (получаем из CTable)
   CTableHeader     *m_header_model;               // Указатель на модель заголовка таблицы (получаем из CTable)
   
//--- Данные компонента View
   CPanel           *m_header_panel;               // Панель для размещения заголовка таблицы
   CTableHeaderView *m_header_view;                // Указатель на заголовок таблицы (View)
   CPanel           *m_rows_header_panel;          // Панель для размещения заголовка строк таблицы
   CTableRowsHeaderView *m_rows_header_view;       // Указатель на заголовок строк таблицы (View)
   CPanel           *m_table_area;                 // Панель для размещения строк таблицы
   CContainer       *m_table_area_container;       // Контейнер для размещения панели со строками таблицы
   bool              m_sortable;                   // Флаг сортируемой таблицы
   
//--- (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) возвращает ширину заголовка строк таблицы
   void              SetRowsHeaderPanelSize(const int width)            { this.m_rows_header_panel_w=width;    }
   int               RowsHeaderWidth(void) const
                       {
                        return(this.m_rows_header_view!=NULL ? this.m_rows_header_view.Width() : 0);
                       }

//--- Создаёт из модели объект (1) таблицы, (2-3) заголовка, (4) обновляет изменённую таблицу
   bool              CreateTable(void);
   bool              CreateHeader(void);
public:
   bool              CreateRowsHeader(string &captions_array[]);
   bool              UpdateTable(void);
   

//--- (1) Устанавливает, (2) возвращает объект таблицы
   bool              TableObjectAssign(CTable *table_obj);
   CTable           *GetTableObj(void)                                  { return this.m_table_obj;             }

//--- Возвращает (1-2) заголовок, (3) область размещения таблицы, (4) контейнер области таблицы
   CTableHeaderView *GetHeader(void)                                    { return this.m_header_view;           }
   CTableRowsHeaderView *GetRowsHeader(void)                            { return this.m_rows_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);  }
                       
//--- Возвращает количество ячеек в указанной строке таблицы
   int               CellsInRow(const uint row)
                       { return(this.GetRowView(row)!=NULL ? this.GetRowView(row).CellsTotal() : 0);           }

//--- Устанавливает метод подсветки строк
   void              SetRowsHighlightMode(const ENUM_ROWS_HIGHLIGHT_MODE mode);
   
//--- (1) Устанавливает, (2) возвращает флаг сортируемой таблицы
   void              SetSortable(const bool flag);
   bool              IsSortable(void)                             const { return this.m_sortable;              }
   
//--- Рисует внешний вид
   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::Конструктор по умолчанию.                            |
//| Строит элемент в главном окне текущего графика                   |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
CTableView::CTableView(void) : CPanel("TableView","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H),
   m_table_model(NULL),m_header_model(NULL),m_table_obj(NULL),m_header_view(NULL),m_rows_header_view(NULL),
   m_table_area(NULL),m_table_area_container(NULL),m_rows_header_panel_w(0),m_sortable(true)
  {
//--- Инициализация
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableView::Конструктор параметрический.                         |
//| Строит элемент в указанном окне указанного графика               |
//| с указанными текстом, координатами и размерами                   |
//+------------------------------------------------------------------+
CTableView::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) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_table_model(NULL),m_header_model(NULL),m_rows_header_view(NULL),m_table_obj(NULL),m_header_view(NULL),
   m_table_area(NULL),m_table_area_container(NULL),m_rows_header_panel_w(0),m_sortable(true)
  {
//--- Инициализация
   this.Init();
  }

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

//+------------------------------------------------------------------+
//| CTableView::Инициализация                                        |
//+------------------------------------------------------------------+
void CTableView::Init(void)
  {
//--- Инициализация родительского объекта
   CPanel::Init();
//--- Ширина рамки, непрозрачность
   this.SetBorderWidth(1);
   this.SetAlphaBG(255);
   this.SetAlphaFG(255);
//--- Инициализируем цвета заднего плана панели и делаем его текущим цветом фона
   this.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrSilver);
   this.BackColorToDefault();
//--- Инициализируем цвета рамки панели и делаем его текущим цветом рамки
   this.InitBorderColors(C'180,180,180',C'180,180,180',C'180,180,180',clrSilver);
   this.BorderColorToDefault();
   
//--- Смещение координаты X для заголовка и строк таблицы (ширина вертикального заголовка строк)
   int dx=(int)::StringToInteger(this.Text());

   this.m_rows_header_panel_w=dx;
   this.SetText("");
   if(dx>DEF_TABLE_ROWS_HEADER_W)
      dx+=12;
   
//--- Координаты и размеры панели заголовка таблицы (заголовок таблицы горизонтальный)
   int x=1+dx;
   int y=1;
   int w=this.Width()-2-dx;
   int h=DEF_TABLE_HEADER_H;
//--- Создаём панель для заголовка таблицы
   this.m_header_panel=this.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableHeaderPanel",x,y,w,h);
   if(this.m_header_panel==NULL)
      return;
//--- Инициализируем цвета заднего плана панели и делаем его текущим цветом фона
   this.m_header_panel.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrSilver);
   this.m_header_panel.BackColorToDefault();
   this.m_header_panel.SetBorderWidth(0);
   this.m_header_panel.SetAlphaBG(255);
//--- Создаём заголовок таблицы
   this.m_header_view=this.m_header_panel.InsertNewElement(ELEMENT_TYPE_TABLE_HEADER_VIEW,"","TableHeader",0,0,this.m_header_panel.Width(),this.m_header_panel.Height());
   if(this.m_header_view==NULL)
      return;
   this.m_header_view.SetBorderWidth(0);
   
//--- Координаты и размеры панели для заголовка строк таблицы (заголовок таблицы вертикальный)
   x=1;
   y=DEF_TABLE_HEADER_H;
   w=(dx>0 ? dx : 1);
   h=this.Height()-2-DEF_TABLE_HEADER_H;
//--- Создаём панель
   this.m_rows_header_panel=this.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableRowsHeaderPanel",x,y,w,h);
   if(this.m_rows_header_panel==NULL)
      return;
//--- Инициализируем цвета заднего плана панели и делаем его текущим цветом фона
   this.m_rows_header_panel.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrSilver);
   this.m_rows_header_panel.BackColorToDefault();
   this.m_rows_header_panel.SetBorderWidth(0);
   this.m_rows_header_panel.SetAlphaBG(255);
//--- Создаём заголовок строк таблицы
   this.m_rows_header_view=this.m_rows_header_panel.InsertNewElement(ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW,"","TableRowsHeader",0,0,this.m_rows_header_panel.Width(),this.m_rows_header_panel.Height());
   if(this.m_rows_header_view==NULL)
      return;
   this.m_rows_header_view.SetBorderWidth(0);
   this.m_rows_header_view.SetAlphaBG(0);
   if(this.m_rows_header_panel_w==0)
      this.m_rows_header_view.Hide(false);
   
//--- Координаты и размеры контейнера, в котором будет находиться панель строк таблицы
   x=1+dx;
   y=1+DEF_TABLE_HEADER_H;
   w=this.Width()-2-dx;
   h=this.Height()-2-DEF_TABLE_HEADER_H;
//--- Создаём контейнер
   this.m_table_area_container=this.InsertNewElement(ELEMENT_TYPE_CONTAINER,"","TableAreaContainer",x,y,w,h);
   if(this.m_table_area_container==NULL)
      return;
   this.m_table_area_container.SetBorderWidth(0);
   this.m_table_area_container.SetScrollable(true);
   
//--- Присоединяем к контейнеру панель для хранения строк таблицы
   this.m_table_area=this.m_table_area_container.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableAreaPanel",0,0,this.m_table_area_container.Width()-0,this.m_table_area_container.Height()-0);

   if(m_table_area==NULL)
      return;
   this.m_table_area.SetBorderWidth(0);
  }

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

//+------------------------------------------------------------------+
//| CTableView::Создаёт объект заголовка строк                       |
//+------------------------------------------------------------------+
bool CTableView::CreateRowsHeader(string &captions_array[])
  {
   if(this.m_rows_header_view==NULL)
     {
      ::PrintFormat("%s: Error. Table rows header object not created",__FUNCTION__);
      return false;
     }
   return this.m_rows_header_view.TableRowCaptionsAssign(captions_array);
  }

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

Доработаем метод рисования внешнего вида:

//+------------------------------------------------------------------+
//| CTableView::Рисует внешний вид                                   |
//+------------------------------------------------------------------+
void CTableView::Draw(const bool chart_redraw)
  {
//--- Рисуем основу
   CPanel::Draw(false);
//--- Рисуем заголовок и строки таблицы
   if(this.m_header_view!=NULL)
      this.m_header_view.Draw(false);
   if(this.m_table_area_container!=NULL)
      this.m_table_area_container.Draw(false);
      
//--- Устанавливаем смещение и размеры области изображенеия
   int x=this.m_rows_header_panel.Width()-16;
   int y=this.m_header_panel.Height()-16;
   int w=11;
   int h=w;
//--- Очищаем область и рисуем угол
   m_painter.Clear(x,y,w,h,false);
   m_painter.TriangleRB(x,y,w,h,BorderColor(),AlphaFG(),true);
      
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Устанавливает метод подсветки строк                  |
//+------------------------------------------------------------------+
void CTableView::SetRowsHighlightMode(const ENUM_ROWS_HIGHLIGHT_MODE highlight_mode)
  {
   int total=this.RowsTotal();
   for(int i=0;i<total;i++)
     {
      CTableRowView *row=this.GetRowView(i);
      if(row!=NULL)
         row.SetHighlightMode(highlight_mode);
     }
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Устанавливает флаг сортируемой таблицы               |
//+------------------------------------------------------------------+
void CTableView::SetSortable(const bool flag)
  {
   this.m_sortable=flag;
   CTableHeaderView *header=this.GetHeader();
   if(header!=NULL)
      header.SetSortableFlag(flag);
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Сортирует таблицу по значению столбца и направлению  |
//+------------------------------------------------------------------+
bool CTableView::Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode)
  {
//--- Если модель таблицы не назначена, сообщаем об этом и возвращаем false
   if(this.m_table_model==NULL)
     {
      ::PrintFormat("%s: Error. The table model is not assigned. Please use the TableObjectAssign() method first",__FUNCTION__);
      return false;
     }

//--- Если у таблицы нет заголовка или сортировка отсутствует - возвращаем false
   if(this.m_header_model==NULL || !this.m_sortable || sort_mode==TABLE_SORT_MODE_NONE)
      return false;
   
//--- Устанавливаем флаг направления сортировки и сортируем модель таблицы по указанному столбцу и направлению
   bool descending=(sort_mode==TABLE_SORT_MODE_DESC);
   this.m_table_model.SortByColumn(column,descending);
//--- Успешно
   return true;
  }

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

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

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

//+------------------------------------------------------------------+
//| Класс управления таблицами                                       | 
//+------------------------------------------------------------------+
class CTableControl : public CPanel
  {
private:
//--- Возвращает максимальное значение целочисленного массива
   bool              ArrayMaximumValue(int &array[],int &value);
//--- Возвращает максимальную ширину текста в массиве заголовков строк
   int               GetMaximumRowCaptionTextSize(string &array_row_captions[]);
   
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,string &row_names[],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           *GetTableModel(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);
template<typename T>
   CTableView       *TableCreate(T &row_data[][],const string &column_names[],string &row_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const uint num_rows, const uint num_columns,string &row_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const matrix &row_data,const string &column_names[],string &row_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(CList &row_data,const string &column_names[],string &row_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);

//--- Устанавливает цвет (1) переднего, (2) заднего плана в указанную ячейку (View)
   void              CellSetForeColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw);
   void              CellSetBackColor(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);

//--- Возвращает количество (1) строк, (2) ячеек в строке в указанной таблице
   uint              RowsTotal(const uint table);
   uint              CellsInRow(const uint table,const uint row);

//--- Устанавливает (1) режим подсветки строк, (2) возможность сортировки указанной таблицы
   void              SetRowsHighlightMode(const uint table,const ENUM_ROWS_HIGHLIGHT_MODE highlight_mode);
   void              SetSortable(const uint table,const bool flag);
   
//--- Тип объекта
   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) {}
  };

Рассмотрим реализацию новых методов.

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

//+------------------------------------------------------------------+
//| Возвращает максимальное значение целочисленного массива          |
//+------------------------------------------------------------------+
bool CTableControl::ArrayMaximumValue(int &array[],int &value)
  {
   ::ResetLastError();
   int index=::ArrayMaximum(array);
   if(index<0)
     {
      ::PrintFormat("%s: ArrayMaximum() failed. Error %d",__FUNCTION__,::GetLastError());
      return false;
     }
   value=array[index];
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| Возвращает максимальную ширину текста в массиве заголовков строк |
//+------------------------------------------------------------------+
int CTableControl::GetMaximumRowCaptionTextSize(string &row_captions[])
  {
   int total=(int)row_captions.Size();
   if(total==0)
      return 0;

   int array[]={};
   ::ArrayResize(array,total);
   for(int i=0;i<total;i++)
     {
      string text=row_captions[i];
      text.TrimLeft();
      text.TrimRight();
      array[i]=this.m_foreground.TextWidth(text);
     }
   int value=0;
   return(this.ArrayMaximumValue(array,value) ? value : 0);
  }

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

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

//+------------------------------------------------------------------+
//| Создаёт новый и добавляет в список объект                        |
//| визуального представления таблицы (CTableView)                   |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableViewAdd(CTable *table_model,string &row_names[],const string source)
  {
//--- Проверяем объект модели таблицы
   if(table_model==NULL)
     {
      ::PrintFormat("%s::%s: Error. An invalid Table Model object was passed",source,__FUNCTION__);
      return NULL;
     }
//--- Получаем максимальную ширину текста заголовков строк
   int w=this.GetMaximumRowCaptionTextSize(row_names);
   if(w>0 && w<DEF_TABLE_ROWS_HEADER_W)
      w=DEF_TABLE_ROWS_HEADER_W;
      
//--- Создаём новый элемент - визуальное представление таблицы, прикреплённый к панели
   CTableView *table_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_VIEW,(string)w,"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.CreateRowsHeader(row_names);
   table_view.SetID(table_model.ID());
   return table_view;
  }

Теперь в метод передаётся дополнительно массив наименований заголовков строк. Далее рассчитывается ширина для объекта вертикального заголовка таблицы (либо 0 при пустом массиве, либо ширина не менее значения DEF_TABLE_ROWS_HEADER_W). При создании объекта визуального представления таблицы, рассчитанная ширина вертикального заголовка передаётся в параметре текста панели в виде строкового значения (после создания таблицы с использованием ширины вертикального заголовка, в параметр текста панели вписывается пустая строка). После создания объекта таблицы вызывается его метод создания вертикального заголовка.

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

//| Создаёт таблицу с указанием массива таблицы и массива заголовков. | 
//| Определяет количество и наименования колонок согласно 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;
   
//--- Создаём и возвращаем таблицу с пустым массивом заголовков строк
   string array[]={};
   return this.TableViewAdd(table_model,array,__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;
   
//--- Создаём и возвращаем таблицу с пустым массивом заголовков строк
   string array[]={};
   return this.TableViewAdd(table_model,array,__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;
   
//--- Создаём и возвращаем таблицу с пустым массивом заголовков строк
   string array[]={};
   return this.TableViewAdd(table_model,array,__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;
   
//--- Создаём и возвращаем таблицу с пустым массивом заголовков строк
   string array[]={};
   return this.TableViewAdd(table_model,array,__FUNCTION__);
  }

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

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

//+-------------------------------------------------------------------+
//| Создаёт таблицу с указанием массива таблицы и массива заголовков. | 
//| Определяет количество и наименования колонок согласно column_names|
//| Количество строк определены размером массива данных row_data,     |
//| который используется и для заполнения таблицы                     |
//+-------------------------------------------------------------------+
template<typename T>
CTableView *CTableControl::TableCreate(T &row_data[][],const string &column_names[],string &row_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,row_names,__FUNCTION__);
  }
//+------------------------------------------------------------------+
//| Создаёт таблицу с определением количества колонок и строк.       |
//| Колонки будут иметь Excel-наименования "A", "B", "C" и т.д.      |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const uint num_rows,const uint num_columns,string &row_names[],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,row_names,__FUNCTION__);
  }
//+------------------------------------------------------------------+
//| Создаёт таблицу с инициализацией колонок согласно column_names   |
//| Количество строк определены параметром row_data, с типом matrix  |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],string &row_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,row_names,__FUNCTION__);
  }
//+------------------------------------------------------------------+
//| Создаёт таблицу с указанием массива таблицы на основе            |
//| списка row_data, содержащего объекты с данными полей структуры.  | 
//| Определяет количество и наименования колонок согласно количеству |
//| наименований столбцов в массиве column_names                     |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(CList &row_data,const string &column_names[],string &row_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,row_names,__FUNCTION__);
  }

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

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

//+------------------------------------------------------------------+
//| Устанавливает цвет заднего плана в указанную ячейку (View)       |
//+------------------------------------------------------------------+
void CTableControl::CellSetBackColor(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.SetBackColor(clr);
   cell_view.Draw(chart_redraw);
  }

Логика метода полностью прокомментирована в коде.

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

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

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

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

//+------------------------------------------------------------------+
//| Возвращает количество ячеек в строке в указанной таблице         |
//+------------------------------------------------------------------+
uint CTableControl::CellsInRow(const uint table,const uint row)
  {
   CTableRowView *row_view=this.GetRowView(table,row);
   return(row_view!=NULL ? row_view.CellsTotal() : 0);
  }

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

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

//+------------------------------------------------------------------+
//| Устанавливает режим подсветки строк указанной таблицы            |
//+------------------------------------------------------------------+
void CTableControl::SetRowsHighlightMode(const uint table,const ENUM_ROWS_HIGHLIGHT_MODE highlight_mode)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return;
     }
   table_view.SetRowsHighlightMode(highlight_mode);
  }

Получаем объект таблицы по её индексу и устанавливаем для неё режим подсветки строк таблицы.

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

//+------------------------------------------------------------------+
//| Устанавливает возможность сортировки указанной таблицы           |
//+------------------------------------------------------------------+
void CTableControl::SetSortable(const uint table,const bool flag)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return;
     }
   table_view.SetSortable(flag);
  }

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

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


Табличный индикатор корреляции символов

Индикатор будет расположен в папке \MQL5\Indicators\Tables\.

Создадим в ней новый индикатор с именем iCorrelationTable.mq5, отображающий данные в подокне графика, с такими свойствами, входными параметрами и глобальными переменными:

//+------------------------------------------------------------------+
//|                                            iCorrelationTable.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers   0
#property indicator_plots     0

#define   CHART_FLOAT_WIDTH   750                                    // Ширина открываемого графика символов
#define   CHART_FLOAT_HEIGHT  500                                    // Высота открываемого графика символов

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

//--- input parameters
input(name="Bars Total (at least 10)") uint              InpBarsTotal   =  1000;                                                 // Количество баров данных для расчёта корреляции (не менее 10)
input(name="Timeframe")                ENUM_TIMEFRAMES   InpTimeframe   =  PERIOD_CURRENT;                                       // Таймфрейм данных для расчёта корреляции
input(name="Symbols for Correlation")  string            InpSymbols     =  "EURUSD,GBPUSD,USDJPY,USDCHF,AUDUSD,NZDUSD,USDCAD";   // Символы для расчёта корреляции

//--- global variables
string   ExtSymbolsArray[];                                          // Массив символов для расчёта корреляции
matrix   ExtPricesData;                                              // Матрица данных символов (цены Close)
uint     ExtBarsTotal;                                               // Количество баров данных для расчёта корреляции
matrix   ExtCorrelationMatrix;                                       // Матрица рассчитанных парных корреляций между всеми символами
bool     ExtDataReady;                                               // Флаг готовности данных по всем символам
long     ExtSymbolsChart;                                            // Идентификатор нового графика для символов корреляции
CTableControl *ExtTableCtrl;                                         // Указатель на объект CTableControl
CTableView *ExtTableView;                                            // Указатель на объект визуального представления таблицы

В обработчике OnInit() индикатора создадим список символов из тех, что указаны во входных параметрах, и запросим их данные:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping

//--- Идентификатор открываемого графика
   ExtSymbolsChart=0;

//--- Ищем подокно графика
   int wnd=ChartWindowFind();

//--- Заполняем массив символов из указанных во входном параметре InpSymbols
   string sep=",";   // разделитель в виде символа
   ushort u_sep;     // код символа разделителя
//--- получим код разделителя 
   u_sep=StringGetCharacter(sep,0); 
//--- получим из строки InpSymbols подстроки по разделителю u_sep и запишем их в массив ExtSymbolsArray
   StringSplit(InpSymbols,u_sep,ExtSymbolsArray);
   
//--- Распечатаем в журнале набор символов для расчёта корреляции
   Print("\nSymbols Array:");
   ArrayPrint(ExtSymbolsArray);
   
//--- Включаем все символы в обзор рынка
   SymbolsSelect(ExtSymbolsArray);

//--- Получаем данные символов (не менее 10 баров) для расчёта корреляции
   ExtBarsTotal=(InpBarsTotal<10 ? 10 : InpBarsTotal);
   ExtDataReady=GetAndCalculateData(ExtBarsTotal);
   
//--- Создаём графический элемент управления таблицами
   int w=500;
   int h=138;
   ExtTableCtrl=new CTableControl("TableControl0",0,wnd,8,8,w-0,h-0);
   if(ExtTableCtrl==NULL)
     {
      Print("Error. Failed to create TableControl object");
      return INIT_FAILED;
     }

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

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

//--- Если данные символов и их корреляции успешно получены,
//--- создаём объект таблицы 0 (компонент Model + View) внутри элемента управления таблицами
//--- из вышесозданной матрицы ExtCorrelationMatrixSymmetric и
//--- string-массива символов ExtSymbolsArray как заголовков столбцов
   if(ExtDataReady && !CreateTable(ExtTableCtrl))
      return INIT_FAILED;
   
//--- Всё успешно
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t 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 int32_t &spread[])
  {
//--- Получаем данные пока не будут готовы
   ExtDataReady=GetAndCalculateData(ExtBarsTotal);
   if(!ExtDataReady)
     {
      Print("The symbol data and their correlations have not yet been obtained. Waiting for the next tick...");
      return 0;
     }
   
//--- Если таблица ещё не создана
//--- создаём объект таблицы 0 (компонент Model + View) внутри элемента управления таблицами
//--- из вышесозданной матрицы ExtCorrelationMatrixSymmetric и
//--- string-массива символов ExtSymbolsArray как заголовков столбцов
   if(ExtTableView==NULL && !CreateTable(ExtTableCtrl))
      return 0;

//--- Обновляем данные в таблице с установкой цветов корреляции
   UpdateTableValuesAndColors(ExtTableCtrl.GetTableView(0),ExtCorrelationMatrix);

//--- return value of prev_calculated for next call
   return(rates_total);
  }

В обработчике OnDeinit() удаляем созданные объекты:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int32_t reason)
  {
//--- Удаляем элемент управления таблицами и уничтожаем менеджер общих ресурсов библиотеки
   delete ExtTableCtrl;
   CCommonManager::DestroyInstance();
  }

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

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Раз в полторы минуты получаем данные по символам из массива
   static int count=0;
   count++;
   if(count>=3000)
     {
      double array[];
      for(int i=0;i<(int)ExtSymbolsArray.Size();i++)
         CopyClose(ExtSymbolsArray[i],InpTimeframe,0,ExtBarsTotal,array);
      count=0;
     }
//--- Вызываем обработчик OnTimer элемента управления таблицами
   ExtTableCtrl.OnTimer();
  }

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

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

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Вызываем обработчик OnChartEvent элемента управления таблицами
   ExtTableCtrl.OnChartEvent(id,lparam,dparam,sparam);
   if(id>=CHARTEVENT_CUSTOM)
     {
      //--- Преобразуем идентификатор полученного пользовательского события к значениям стандартных событий
      ENUM_CHART_EVENT chart_event=ENUM_CHART_EVENT(id-CHARTEVENT_CUSTOM);
      
      //--- Если событие щелчка по графическому объекту
      if(chart_event==CHARTEVENT_OBJECT_CLICK)
        {
         //--- Если в имени события (значение sparam) присутствует наименование строки таблицы (начинается с "TableCellView")
         if(StringFind(sparam,"TableCellView")==0)
           {
            //--- Получаем номер строки и столбца из параметров сорбытия
            int row=(int)lparam;
            int col=(int)dparam;
            
            string sep=";";                  // разделитель в виде символа 
            ushort u_sep;                    // код символа разделителя 
            string result[];                 // массив для получения строк 
            
            //--- Получим код разделителя и разделим sparam на подстроки
            u_sep=StringGetCharacter(sep,0); 
            int n=StringSplit(sparam,u_sep,result);
            
            //--- Должно быть три подстроки
            if(n==3)
              {
               //--- Получаем символ строки и символ столбца
               string row_symb=result[1];
               string col_symb=result[2];
               
               //--- Если график ещё не открыт - открываем его
               if(ExtSymbolsChart==0 || !IsExistChart(ExtSymbolsChart))
                  ExtSymbolsChart=OpenCharts(row_symb,col_symb);

               //--- Если график уже открыт
               if(ExtSymbolsChart!=0)
                 {
                  //--- Устанавливаем символы для двух объектов-графиков и перерисовываем график
                  ObjectSetString(ExtSymbolsChart,"ChartRowSymbol",OBJPROP_SYMBOL,row_symb);
                  ObjectSetString(ExtSymbolsChart,"ChartColSymbol",OBJPROP_SYMBOL,col_symb);
                  ChartRedraw(ExtSymbolsChart);
                 }
              }
           }
        }
     }
  }

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

Рассмотрим функции, используемые в индикаторе.

Функция, включающая символы из массива в обзор рынка:

//+------------------------------------------------------------------+
//| Включает символы из массива в обзор рынка                        |
//+------------------------------------------------------------------+
bool SymbolsSelect(string &array[])
  {
   bool res=true;
   for(int i=0;i<(int)array.Size();i++)
      res &=SymbolSelect(array[i],true);
   return res;
  }

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

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

//+------------------------------------------------------------------+
//| Возвращает символ по индексу массива                             |
//+------------------------------------------------------------------+
string GetSymbolByIndex(const int index,string &array[])
  {
   int total=(int)array.Size();
   if(index<0 || index>total-1)
      return StringFormat("%s: Error. Invalid index (%d)",__FUNCTION__,index);
   return array[index];
  }

Функция, заполняющая матрицу данных символов:

//+------------------------------------------------------------------+
//| Заполняет матрицу данных символов                                |
//+------------------------------------------------------------------+
bool SymbolsDataMatrixFill(const ENUM_TIMEFRAMES timeframe,string &array[],matrix &data,const int data_count)
  {
//--- В цикле по количеству символов в массиве array
   int total=(int)array.Size();
   for(int i=0; i<total; i++)
     {
      //--- получаем цены закрытия в количестве data_count
      double close[];
      int copied=CopyClose(array[i], timeframe, 0, data_count, close);
      if(copied!=data_count)
         return false;
      
      //--- Записываем цены в строку матрицы так, чтобы 0 ячейка строки соответствовала 0 бару
      for(int j=0; j<data_count; j++)
        {
         string symbol=GetSymbolByIndex(i,array);
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
         data[i][data_count-1-j]=NormalizeDouble(close[j],digits);
        }
     }
   return true;
  }

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

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

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

//+------------------------------------------------------------------+
//|Рассчитывает симметричную матрицу корреляций между всеми символами|
//+------------------------------------------------------------------+
bool SymbolsCorrelationMatrixSymmetric(const matrix &data, matrix &correlation)
  {
   int symb_total=(int)data.Rows();    // количество символов

//--- Устанавливаем размер матрицы корреляций
   if(!correlation.Resize(symb_total,symb_total))
      return false;

//--- Внешний цикл по всем символам (строкам)
   for(int i=0;i<symb_total;i++)
     {
      //--- Получаем временной ряд цен для символа i
      vector vi=data.Row(i);
      //--- Внутренний цикл по всем символам (столбцам)
      for(int j=0;j<symb_total;j++)
        {
         //--- Если символы в строке и столбце одинаковы, то это корреляция с собой
         if(i==j)
            correlation[i][j]=1.0;
         
         //--- Символы в строке и столбце различаются
         else
           {
            //--- Получаем временной ряд цен для символа j и считаем корреляцию между символами i и j
            vector vj=data.Row(j);
            correlation[i][j]=vi.CorrCoef(vj);
           }
        }
     }
//--- Всё успешно
   return true;
  }

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

Функция, распечатывающая в журнале симметричную матрицу корреляций:

//+------------------------------------------------------------------+
//| Распечатывает в журнале симметричную матрицу корреляций          |
//+------------------------------------------------------------------+
void SymbolsCorrelationMatrixSymmetricPrint(const string &symb_array[],matrix &correlation)
  {
//--- Создаём и распечатываем заголовок
   Print("Correlation matrix:");
   string header="        ";
   for(int j=0;j<(int)symb_array.Size();j++)
      header+=symb_array[j]+" ";
   Print(header);

//--- Распечатываем данные корреляции символов
   for(int i=0;i<(int)symb_array.Size();i++)
     {
      string row=symb_array[i]+" ";
      for(int j=0;j<(int)symb_array.Size();j++)
         row+=DoubleToString(correlation[i][j],2)+" ";
      Print(row);
     }
  }

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

Функция, получающая и рассчитывающая все необходимые данные по символам:

//+------------------------------------------------------------------+
//| Получает и рассчитывает все необходимые данные                   |
//+------------------------------------------------------------------+
bool GetAndCalculateData(uint bars_total)
  {
//--- Получаем значения для данных по символам
   const int symb_total=(int)ExtSymbolsArray.Size();     // количество символов

//--- Изменяем размер матрицы: строки - символы, столбцы - бары
   if(!ExtPricesData.Resize(symb_total,bars_total))
     {
      Print("Error. Failed to resize the symbol data matrix");
      return false;
     }

//--- Заполняем матрицу ценами закрытия символов и присваиваем значение флагу готовности данных
   ExtDataReady=SymbolsDataMatrixFill(InpTimeframe,ExtSymbolsArray,ExtPricesData,bars_total);
   if(!ExtDataReady)
      return false;
      
//--- Рассчитываем симметричную матрицу корреляций
   if(!SymbolsCorrelationMatrixSymmetric(ExtPricesData,ExtCorrelationMatrix))
     {
      Print("Error calculating correlation matrix");
      return false;
     }
   return true; 
  }

В функцию передаётся количество запрашиваемых данных. По каждому из символов запрашивается указанное количество данных. Если данные успешно получены, то рассчитывается матрица корреляций между символами. При успешности получения и расчёта всех данных функция возвращает true, в противном случае — false. Функцию необходимо вызывать до тех пор, пока не будут получены и рассчитаны все данные. Делается это сначала при инициализации, а затем на каждом тике. Как только все данные получены и рассчитаны, больше эта функция не вызывается.

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

//+------------------------------------------------------------------+
//| Создаёт на панели таблицу символов с данными их корреляций       |
//+------------------------------------------------------------------+
bool CreateTable(CTableControl *table_ctrl)
  {
//--- создаём объект таблицы 0 (компонент Model + View) внутри элемента управления таблицами
//--- из вышесозданной матрицы ExtCorrelationMatrixSymmetric и
//--- string-массива символов ExtSymbolsArray как заголовков столбцов
   ExtTableView=table_ctrl.TableCreate(ExtCorrelationMatrix,ExtSymbolsArray,ExtSymbolsArray);
   if(ExtTableView==NULL)
      return false;
   
//--- Установим для столбцов вывод текста по центру ячейки
   int total=(int)table_ctrl.RowsTotal(0);
   for(int i=0;i<total;i++)
     {
      table_ctrl.ColumnSetTextAnchor(0,i,ANCHOR_CENTER,true,false);
     }
   
//--- Установим режим подсветки строк таблицы для отдельных ячеек
   table_ctrl.SetRowsHighlightMode(0,ROWS_HIGHLIGHT_MODE_CELLS);
//--- и сделаем таблицу несортируемой
   table_ctrl.SetSortable(0,false);
   
//--- Нарисуем и раскрасим таблицу в цвета корреляции символов
   table_ctrl.Draw(false);
   UpdateTableValuesAndColors(table_ctrl.GetTableView(0),ExtCorrelationMatrix);
   
//--- Получим модель таблицы с индексом 0 и распечатаем в журнале
   CTable *table_model=table_ctrl.GetTableModel(0);
   table_model.Print(7);
   
//--- Всё успешно
   return true;
  }

Функция создаёт внутри элемента управления таблицами одну таблицу на данных символов и корреляции между ними. В заголовках таблицы прописываются наименования символов по горизонтали и вертикали. В ячейках таблицы прописываются значения корреляции символов из расположения строка/столбец. Каждая ячейка окрашивается в цвет корреляции от красного цвета (корреляция -1) через желтый (корреляция 0) до зелёного (корреляция +1), создавая тем самым тепловую карту корреляций.

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

//+------------------------------------------------------------------+
//| Обновляет значения и цвета ячеек таблицы по матрице корреляций   |
//+------------------------------------------------------------------+
void UpdateTableValuesAndColors(CTableView *table_view, matrix &corr_matrix)
  {
//--- Проверим валидность указателя на таблицу
   if(table_view==NULL)
      return;

//--- Количество строк и столбцов таблицы
   int total_row=table_view.RowsTotal();
   int total_col=table_view.CellsInRow(0);

//--- В цикле по строкам таблицы
   for(int r=0; r<total_row; r++)
     {
      //--- получаем очередной объект визуального представления строки
      CTableRowView *row_obj=table_view.GetRowView(r);
      if(row_obj==NULL)
         continue;
      //--- Получаем элемент управления цветом
      CColorElement *ce=row_obj.GetBackColorControl();
      if(ce==NULL)
         continue;
      
      //--- В цикле по количеству ячеек в строке
      for(int c=0; c<total_col; c++)
        {
         //--- получаем очередной объект визуального представления ячейки
         CTableCellView *cell=table_view.GetCellView(r,c);
         if(cell==NULL)
            continue;
         
         //--- Берём значение корреляции из матрицы
         double val=corr_matrix[r][c];
         //--- Обновляем текст ячейки,
         cell.SetText(DoubleToString(val,2));
         //--- обновляем цвет ячейки
         color new_color=ce.InterpolateColorByCoeff(clrRed,clrYellow,clrGreen,val);
         cell.SetBackColor(new_color);
         
         //--- Перерисовываем ячейку (график обновляем на последней ячейке таблицы)
         bool flag=(r==total_row-1 && c==total_col-1 ? true : false); 
         cell.Draw(flag);
        }
     }
  }

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

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

//+------------------------------------------------------------------+
//| Возвращает флаг существования графика с указанным идентификатором|
//+------------------------------------------------------------------+
bool IsExistChart(const long id)
  {
//--- Переменные для идентификаторов графиков 
   long curr_chart=0, prev_chart=0; 
   int i=0; 

//--- Проходим по всем графикам
   while(!IsStopped() && i<CHARTS_MAX)
     { 
      //--- На основании предыдущего получим новый график
      curr_chart=ChartNext(prev_chart);   // prev_chart==0 - получить первый график
      //--- Если достигли конца списка графиков - выходим из цикла
      if(curr_chart<0)
         break;
      //--- Если идентификатор графика совпадает с искомым - такой график есть
      if(curr_chart==id)
         return true;
      //--- Запомним идентификатор текущего графика для следующего ChartNext()
      prev_chart=curr_chart;
      //--- увеличиваем счетчик
      i++;
     }
//--- Нет искомого графика
   return false;
  }

Чтобы при каждом щелчке мышкой по ячейкам таблицы не открывать новое окно графика, нам нужно проверить существование уже ранее открытого графика. Именно это и делает функция.

Функция, открывающая графики символов:

//+------------------------------------------------------------------+
//| Открывает графики символов                                       |
//+------------------------------------------------------------------+
long OpenCharts(const string row_symb,const string col_symb)
  {
   //--- устанавливаем символ и таймфрейм для нового графика 
   string symbol=row_symb; 
   if(symbol==NULL || symbol=="") 
      symbol=Symbol(); 
    
//--- открываем новый график с заданными символом и периодом 
   long id=ChartOpen(symbol,PERIOD_CURRENT);
   if(id==0) 
     { 
      Print("ChartOpen() failed. Error ", GetLastError()); 
      return 0; 
     }
//--- Открепляем график и делаем его пустым
   ChartSetInteger(id,CHART_IS_DOCKED,false);
   ChartSetInteger(id,CHART_SHOW,false);

//--- Получаем координаты сторон откреплённого графика
   int top=(int)ChartGetInteger(id,CHART_FLOAT_TOP);
   int bottom=(int)ChartGetInteger(id,CHART_FLOAT_BOTTOM);
   int left=(int)ChartGetInteger(id,CHART_FLOAT_LEFT);
   int right=(int)ChartGetInteger(id,CHART_FLOAT_RIGHT);
   
//--- Устанавливаем новые ширину и высоту графика
   ChartSetInteger(id,CHART_FLOAT_RIGHT,left+CHART_FLOAT_WIDTH);
   ChartSetInteger(id,CHART_FLOAT_BOTTOM,top+CHART_FLOAT_HEIGHT);
   
//--- Получаем размеры графика в пикселях
   int cw=(int)ChartGetInteger(id,CHART_WIDTH_IN_PIXELS);
   int ch=(int)ChartGetInteger(id,CHART_HEIGHT_IN_PIXELS);
   
//--- Задаём высоту для верхнего и нижнего объектов-графиков
   int h0=(int)round(ch/2);
   int h1=ch-h0;
   
//--- Создаём на откреплённом графике два объекта-графика с символами строки и столбца
   if(!CreateChartObject(id,"ChartRowSymbol",row_symb,PERIOD_CURRENT,0,0,cw,h0))
      return 0;
   if(!CreateChartObject(id,"ChartColSymbol",col_symb,PERIOD_CURRENT,0,h0-1,cw,h1+1))
      return 0;
      
//--- Обновляем открытый график и возвращаем его идентификатор
   ChartRedraw(id);
   return id;
  }

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

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

//+------------------------------------------------------------------+
//| Создаёт объект-график указанного символа                         |
//+------------------------------------------------------------------+
bool CreateChartObject(const long chart_id,const string name,const string symbol,const ENUM_TIMEFRAMES timeframe,const int x,const int y,const int w,const int h)
  {
//--- Создаём объект-график с указанными координатами и размерами
//--- и устанавливаем его свойства - символ, период, координаты и размеры
   if(ObjectCreate(chart_id,name,OBJ_CHART,0,x,y,w,h))
     {
      ObjectSetString(chart_id,name,OBJPROP_SYMBOL,symbol);
      ObjectSetInteger(chart_id,name,OBJPROP_PERIOD,timeframe);
      ObjectSetInteger(chart_id,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(chart_id,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(chart_id,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(chart_id,name,OBJPROP_YSIZE,h);
      return true;
     }
//--- Ошибка создания объекта-графика
   return false;
  } 

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

Всё, индикатор готов.

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

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


Заключение

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

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

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

Созданный сегодня индикатор является не только полезным инструментом для анализа корреляций, но и примером того, как можно использовать программные возможности MQL5 для построения сложных и функциональных элементов интерфейса.


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

#
 Имя Тип
Описание
 1  Tables.mqh  Библиотека классов  Классы для создания модели таблицы
 2  Base.mqh  Библиотека классов  Классы для создания базового объекта элементов управления
 3  Controls.mqh  Библиотека классов  Классы элементов управления
 4  iCorrelationTable.mq5  Тестовый индикатор  Индикатор для отображения корреляции символов в таблице
 5  MQL5.zip  Архив  Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала
Все созданные файлы прилагаются к статье для самостоятельного изучения. Файл архива можно распаковать в папку терминала, и все файлы будут расположены в нужной папке: \MQL5\Indicators\Tables\.
Прикрепленные файлы |
Tables.mqh (272.47 KB)
Base.mqh (305.36 KB)
Controls.mqh (928.8 KB)
MQL5.zip (146.27 KB)
Нейросети в трейдинге: Сеточная аппроксимация событийного потока как инструмент анализа ценовых паттернов (CDC-модуль) Нейросети в трейдинге: Сеточная аппроксимация событийного потока как инструмент анализа ценовых паттернов (CDC-модуль)
В статье представлен промежуточный этап реализации фреймворка EEMFlow средствами MQL5. Основное внимание уделено построению и интеграции CDC-модуля, включающего Self-Corrector, механизм Self-Attention для скорректированного потока и взвешенное объединение сигналов через маску доверия. Рассмотрены принципы архитектуры, порядок прямого и обратного проходов, а также особенности работы с локальными и глобальными признаками движения.
Нейросети в трейдинге: Сеточная аппроксимация событийного потока как инструмент анализа ценовых паттернов (ADM-модуль) Нейросети в трейдинге: Сеточная аппроксимация событийного потока как инструмент анализа ценовых паттернов (ADM-модуль)
В статье представлена реализация Adaptive Density Module (ADM), ключевого компонента фреймворка EEMFlow, средствами MQL5. Рассмотрены этапы построения и объединения субмодулей MDC и MDS, а также интеграция ADM в существующую торговую модель BAT. Результаты тестирования на исторических данных EURUSD показывают устойчивый рост депозита, контролируемые просадки и высокую стабильность кривой эквити.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Выборочные методы марковских цепей Монте-Карло. Алгоритм HMC Выборочные методы марковских цепей Монте-Карло. Алгоритм HMC
В статье исследуется гамильтонов алгоритм Монте-Карло (HMC) — золотой стандарт сэмплирования из сложных многомерных распределений. Представлена полноценная реализация HMC на языке MQL5, которая включает адаптивную настройку матрицы масс, поиск моды апостериорного распределения (MAP) с помощью метода оптимизации L-BFGS и комплексной диагностикой.