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

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

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

Содержание



Введение

В статье "Классы таблицы и заголовка на базе модели таблицы в MQL5: Применение концепции MVC" мы завершили создание класса модели таблиц (в концепции MVC — компонент Model). Далее занимались разработкой библиотеки простых элементов управления, которые позволяют создавать на их основе совершенно разные по назначению и сложности элементы управления. В частности — компонент View для создания элемента управления TableView.

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

Элемент управления будет создан на базе класса объекта Панель (Panel), и состоять из нескольких элементов:

  1. Panel — основа, подложка, к которой прикреплены заголовок таблицы и область данных таблицы;
  2. Panel — заголовок таблицы, состоящий из ряда элементов — заголовков столбцов, созданных на базе класса объекта Button;
  3. Container — контейнер табличных данных с возможностью прокрутки содержимого;
  4. Panel — панель для расположения на ней строк таблицы (прикреплена к контейнеру (п.3) и, при выходе за пределы контейнера, прокручивается при помощи полос прокрутки контейнера);
  5. Panel — строка таблицы — панель для рисования на ней ячеек таблицы, прикреплённая к панели (п.4); таких объектов создаётся по количеству строк таблицы;
  6. Класс ячейки таблицы — новый класс, позволяющий рисовать в указанных координатах на указанном холсте (CCanvas). Объект такого класса прикрепляется к объекту строки таблицы (п.5), канвасы объекта строки таблицы указываются как элементы для рисования, и ячейка таблицы рисуется на этой панели в указанных координатах. Область каждой ячейки таблицы задаётся в объекте строки таблицы (п.5) экземпляром объекта класса CBound, и к этому объекту прикрепляется объект класса ячейки таблицы.

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

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

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

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


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

Все файлы разрабатываемой библиотеки расположены по адресу \MQL5\Indicators\Tables\Controls\. Если их ещё нет, то получить предыдущую версию всех файлов можно из прошлой статьи. Также для проекта потребуется файл классов таблиц (компонент Model), получить который можно из статьи, где мы завершили их разработку. Файл Tables.mqh необходимо сохранить в папку \MQL5\Indicators\Tables\. Дорабатываться будут файлы Base.mqh и Control.mqh.

В самом начале файла Tables.mqh в разделе макросов напишем идентификатор файла:

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

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

Откроем файл Base.mqh. Подключим к нему файл классов модели таблицы и пропишем форвард-декларацию новых классов:

//+------------------------------------------------------------------+
//|                                                         Base.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd." 
#property link      "https://www.mql5.com"

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#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    CTableRowView;                   // Класс визуального представления строки таблицы
class    CPanel;                          // Класс элемента управления Panel
class    CGroupBox;                       // Класс элемента управления GroupBox
class    CContainer;                      // Класс элемента управления Container

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#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)
   ELEMENT_TYPE_TABLE_ROW,                // Строка таблицы (View)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION,     // Заголовок столбца таблицы (View)
   ELEMENT_TYPE_TABLE_HEADER,             // Заголовок таблицы (View)
   ELEMENT_TYPE_TABLE,                    // Таблица (View)
   ELEMENT_TYPE_PANEL,                    // Элемент управления Panel
   ELEMENT_TYPE_GROUPBOX,                 // Элемент управления GroupBox
   ELEMENT_TYPE_CONTAINER,                // Элемент управления Container
  };
#define  ACTIVE_ELEMENT_MIN   ELEMENT_TYPE_LABEL         // Минимальное значение списка активных элементов
#define  ACTIVE_ELEMENT_MAX   ELEMENT_TYPE_TABLE_HEADER  // Максимальное значение списка активных элементов

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

//+------------------------------------------------------------------+
//|  Возвращает короткое имя элемента по типу                        |
//+------------------------------------------------------------------+
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           :  return "TCELL";   // Ячейка таблицы (View)
      case ELEMENT_TYPE_TABLE_ROW            :  return "TROW";    // Строка таблицы (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  return "TCAPT";   // Заголовок столбца таблицы (View)
      case ELEMENT_TYPE_TABLE_HEADER         :  return "THDR";    // Заголовок таблицы (View)
      case ELEMENT_TYPE_TABLE                :  return "TABLE";   // Таблица (View)
      case ELEMENT_TYPE_PANEL                :  return "PNL";     // Элемент управления Panel
      case ELEMENT_TYPE_GROUPBOX             :  return "GRBX";    // Элемент управления GroupBox
      case ELEMENT_TYPE_CONTAINER            :  return "CNTR";    // Элемент управления Container
      default                                :  return "Unknown"; // Unknown
     }
  }

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

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

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BASE); }
   
//--- (1) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   virtual void      Print(void);
   
//--- Конструктор/деструктор
                     CBaseObj (void) : m_id(-1) { this.SetName(""); }
                    ~CBaseObj (void) {}
  };

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

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

Пропишем в классе CBound такую возможность:

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

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

//--- Возвращает флаг нахождения курсора внутри области
   bool              Contains(const int x,const int y)   const { return this.m_bound.Contains(x,y);                           }
   
//--- (1) Назначает, (2) возвращает указатель на назначенный элемент
   void              AssignObject(CBaseObj *obj)               { this.m_assigned_obj=obj;                                     }
   CBaseObj         *GetAssignedObj(void)                      { return this.m_assigned_obj;                                  }
   
//--- Возвращает описание объекта
   virtual string    Description(void);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_RECTANGLE_AREA);                         }
   
//--- Конструкторы/деструктор
                     CBound(void) { ::ZeroMemory(this.m_bound); }
                     CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h);             }
                    ~CBound(void) { ::ZeroMemory(this.m_bound); }
  };

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

Значит, нам необходимо перенести из класса CCanvasBase объект класса CBound в другой — родительский класс, который не будет создавать объекты канваса, и от которого будет наследоваться класс CCanvasBase. А в классе CCanvasBase не объявлять экземпляры двух CCanvas, а объявить указатели на создаваемые канвасы и сделать метод, который будет создавать два объекта канваса. Необходимо также объявить флаг, указывающий, что графический элемент управляет или не управляет объектами канвасов фона и переднего плана. Это требуется для управления удалением созданных объектов канвасов.

В файле Base.mqh сразу после класса CAutoRepeat, перед классом CCanvasBase напишем новый класс CBoundedObj, наследуемый от CBaseObj, в который просто перенесём все методы работы с объектом CBound из класса CCanvasBase:

//+------------------------------------------------------------------+
//| Базовый класс, хранящий размеры объекта                          |
//+------------------------------------------------------------------+
class CBoundedObj : public CBaseObj
  {
protected:
   CBound            m_bound;                                  // Границы объекта
   bool              m_canvas_owner;                           // Флаг владения канвасами
public:
//--- Возвращает координаты, размеры и границы объекта
   int               X(void)                             const { return this.m_bound.X();                                                          }
   int               Y(void)                             const { return this.m_bound.Y();                                                          }
   int               Width(void)                         const { return this.m_bound.Width();                                                      }
   int               Height(void)                        const { return this.m_bound.Height();                                                     }
   int               Right(void)                         const { return this.m_bound.Right();                                                      }
   int               Bottom(void)                        const { return this.m_bound.Bottom();                                                     }

//--- Изменяет (1) ширину, (2) высоту, (3) размер ограничивающего прямоугольника
   void              BoundResizeW(const int size)              { this.m_bound.ResizeW(size);                                                       }
   void              BoundResizeH(const int size)              { this.m_bound.ResizeH(size);                                                       }
   void              BoundResize(const int w,const int h)      { this.m_bound.Resize(w,h);                                                         }
   
//--- Устанавливает координату (1) X, (2) Y, (3) обе координаты ограничивающего прямоугольника
   void              BoundSetX(const int x)                    { this.m_bound.SetX(x);                                                             }
   void              BoundSetY(const int y)                    { this.m_bound.SetY(y);                                                             }
   void              BoundSetXY(const int x,const int y)       { this.m_bound.SetXY(x,y);                                                          }
   
//--- (1) Устанавливает, (2) смещает ограничивающий прямоугольник на указанные координаты/размер смещения
   void              BoundMove(const int x,const int y)        { this.m_bound.Move(x,y);                                                           }
   void              BoundShift(const int dx,const int dy)     { this.m_bound.Shift(dx,dy);                                                        }
   
//--- Виртуальные методы (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_BOUNDED_BASE); }
                     
                     CBoundedObj (void) : m_canvas_owner(true) {}
                     CBoundedObj (const string user_name,const int id,const int x,const int y,const int w,const int h);
                    ~CBoundedObj (void){}
  };
//+------------------------------------------------------------------+
//| CBoundedObj::Конструктор                                         |
//+------------------------------------------------------------------+
CBoundedObj::CBoundedObj(const string user_name,const int id,const int x,const int y,const int w,const int h) : m_canvas_owner(true)
  {
//--- Получаем скорректированный идентификатор графика и дистанцию в пикселях по вертикальной оси Y
   this.m_bound.SetName(user_name);
   this.m_bound.SetID(id);
   this.m_bound.SetXY(x,y);
   this.m_bound.Resize(w,h);
  }
//+------------------------------------------------------------------+
//| CBoundedObj::Сохранение в файл                                   |
//+------------------------------------------------------------------+
bool CBoundedObj::Save(const int file_handle)
  {
//--- Сохраняем данные родительского объекта
   if(!CBaseObj::Save(file_handle))
      return false;
 
//--- Сохраняем флаг владения канвасами
   if(::FileWriteInteger(file_handle,this.m_canvas_owner,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем размеры
   return this.m_bound.Save(file_handle);
  }
//+------------------------------------------------------------------+
//| CBoundedObj::Загрузка из файла                                   |
//+------------------------------------------------------------------+
bool CBoundedObj::Load(const int file_handle)
  {
//--- Загружаем данные родительского объекта
   if(!CBaseObj::Load(file_handle))
      return false;
   
//--- Загружаем флаг владения канвасами
   this.m_canvas_owner=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем размеры
   return this.m_bound.Load(file_handle);
  }

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

Базовый класс холста графических элементов теперь должен быть унаследован от нового CBoundedObj, а не от класса CBaseObj, как это было ранее. А всё, что было связано с работой с объектом класса CBound для установки и возврата размеров элемента, всё уже перенесено в новый класс CBoundedObj и удалено из класса CCanvasBase. Теперь объекты канвасов не объявлены как экземпляры класса, а объявлены как указатели на создаваемые объекты CCanvas:

//+------------------------------------------------------------------+
//| Базовый класс холста графических элементов                       |
//+------------------------------------------------------------------+
class CCanvasBase : public CBoundedObj
  {
private: 
   bool              m_chart_mouse_wheel_flag;                 // Флаг отправки сообщений о прокрутке колёсика мышки
   bool              m_chart_mouse_move_flag;                  // Флаг отправки сообщений о перемещениях курсора мышки
   bool              m_chart_object_create_flag;               // Флаг отправки сообщений о событии создания графического объекта
   bool              m_chart_mouse_scroll_flag;                // Флаг прокрутки графика левой кнопкой и колёсиком мышки
   bool              m_chart_context_menu_flag;                // Флаг доступа к контекстному меню по нажатию правой клавиши мышки
   bool              m_chart_crosshair_tool_flag;              // Флаг доступа к инструменту "Перекрестие" по нажатию средней клавиши мышки
   bool              m_flags_state;                            // Состояние флагов прокрутки графика колёсиком, контекстного меню и перекрестия
   
//--- Установка запретов для графика (прокрутка колёсиком, контекстное меню и перекрестие)
   void              SetFlags(const bool flag);
   
protected:
   CCanvas          *m_background;                             // Канвас для рисования фона
   CCanvas          *m_foreground;                             // Канвас для рисования переднего плана

   CCanvasBase      *m_container;                              // Родительский объект-контейнер
   CColorElement     m_color_background;                       // Объект управления цветом фона
   CColorElement     m_color_foreground;                       // Объект управления цветом переднего плана
   CColorElement     m_color_border;                           // Объект управления цветом рамки

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

//--- Возврат координат, границ и размеров графического объекта
public:
   int               ObjectX(void)                       const { return this.m_obj_x;                                                              }
   int               ObjectY(void)                       const { return this.m_obj_y;                                                              }
   int               ObjectWidth(void)                   const { return this.m_background.Width();                                                 }
   int               ObjectHeight(void)                  const { return this.m_background.Height();                                                }
   int               ObjectRight(void)                   const { return this.ObjectX()+this.ObjectWidth()-1;                                       }
   int               ObjectBottom(void)                  const { return this.ObjectY()+this.ObjectHeight()-1;                                      }
   
//--- Изменяет (1) ширину, (2) высоту, (3) размер графического объекта
protected:
   virtual bool      ObjectResizeW(const int size);
   virtual bool      ObjectResizeH(const int size);
   bool              ObjectResize(const int w,const int h);
   
//--- Устанавливает координату (1) X, (2) Y, (3) обе координаты графического объекта
   virtual bool      ObjectSetX(const int x);
   virtual bool      ObjectSetY(const int y);
   bool              ObjectSetXY(const int x,const int y)      { return(this.ObjectSetX(x) && this.ObjectSetY(y));                                 }

В защищённой секции класса объявлен метод для создания канвасов:

//--- Устанавливает указатель на родительский объект-контейнер
   void              SetContainerObj(CCanvasBase *obj);
   
protected:
//--- Создаёт канвасы фона и переднего плана
   bool              CreateCanvasObjects(void);
//--- Создаёт OBJ_BITMAP_LABEL
   bool              Create(const long chart_id,const int wnd,const string object_name,const int x,const int y,const int w,const int h); 
public:
//--- (1) Устанавливает, (2) возвращает состояние
   void              SetState(ENUM_ELEMENT_STATE state)        { this.m_state=state; this.ColorsToDefault();                                       }
   ENUM_ELEMENT_STATE State(void)                        const { return this.m_state;                                                              }

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

//--- Устанавливает объекту флаг (1) перемещаемости, (2) главного объекта, (3) возможности изменения размеров
//--- обрезки по границам контейнера
   void              SetMovable(const bool flag)               { this.m_movable=flag;                                                              }
   void              SetAsMain(void)                           { this.m_main=true;                                                                 }
   virtual void      SetResizable(const bool flag)             { this.m_resizable=flag;                                                            }
   void              SetAutorepeat(const bool flag)            { this.m_autorepeat_flag=flag;                                                      }
   void              SetScrollable(const bool flag)            { this.m_scroll_flag=flag;                                                          }
   virtual void      SetTrimmered(const bool flag)             { this.m_trim_flag=flag;                                                            }

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

//+------------------------------------------------------------------+
//| CCanvasBase::Конструктор                                         |
//+------------------------------------------------------------------+
CCanvasBase::CCanvasBase(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)), m_wnd(wnd<0 ? 0 : wnd), m_alpha_bg(0), m_alpha_fg(255),
   m_hidden(false), m_blocked(false), m_focused(false), m_movable(false), m_resizable(false), m_main(false), m_autorepeat_flag(false), m_trim_flag(true), m_scroll_flag(false),
   m_border_width_lt(0), m_border_width_rt(0), m_border_width_up(0), m_border_width_dn(0), m_z_order(0),
   m_state(0), m_cursor_delta_x(0), m_cursor_delta_y(0)
  {
//--- Получаем скорректированный идентификатор графика и дистанцию в пикселях по вертикальной оси Y
//--- между верхней рамкой подокна индикатора и верхней рамкой главного окна графика
   this.m_chart_id=this.CorrectChartID(chart_id);
   
//--- Если не удалось создать канвасы - уходим
   if(!this.CreateCanvasObjects())
      return;
      
//--- Если графический ресурс и графический объект созданы
   if(this.Create(this.m_chart_id,this.m_wnd,object_name,x,y,w,h))
     {
      //--- Очищаем канвасы фона и переднего плана и устанавливаем начальные значения координат,
      //--- наименования графических объектов и свойства текста, рисуемого на переднем плане
      this.Clear(false);
      this.m_obj_x=x;
      this.m_obj_y=y;
      this.m_color_background.SetName("Background");
      this.m_color_foreground.SetName("Foreground");
      this.m_color_border.SetName("Border");
      this.m_foreground.FontSet(DEF_FONTNAME,-DEF_FONTSIZE*10,FW_MEDIUM);
      this.m_bound.SetName("Perimeter");
      
      //--- Запоминаем разрешения для мышки и инструментов графика
      this.Init();
     }
  }

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

//+------------------------------------------------------------------+
//| CCanvasBase::Создаёт канвасы фона и переднего плана              |
//+------------------------------------------------------------------+
bool CCanvasBase::CreateCanvasObjects(void)
  {
//--- Если оба канваса уже созданы, или класс не управляет канвасами - возвращаем true
   if((this.m_background!=NULL && this.m_foreground!=NULL) || !this.m_canvas_owner)
      return true;
//--- Создаём канвас фона
   this.m_background=new CCanvas();
   if(this.m_background==NULL)
     {
      ::PrintFormat("%s: Error! Failed to create background canvas",__FUNCTION__);
      return false;
     }
//--- Создаём канвас переднего плана
   this.m_foreground=new CCanvas();
   if(this.m_foreground==NULL)
     {
      ::PrintFormat("%s: Error! Failed to create foreground canvas",__FUNCTION__);
      return false;
     }
//--- Всё успешно
   return true;
  }

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

//+------------------------------------------------------------------+
//| CCanvasBase::Уничтожает объект                                   |
//+------------------------------------------------------------------+
void CCanvasBase::Destroy(void)
  {
   if(this.m_canvas_owner)
     {
      this.m_background.Destroy();
      this.m_foreground.Destroy();
      delete this.m_background;
      delete this.m_foreground;
      this.m_background=NULL;
      this.m_foreground=NULL;
     }
  }

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

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

//+------------------------------------------------------------------+
//| CCanvasBase::Инициализация класса                                |
//+------------------------------------------------------------------+
void CCanvasBase::Init(void)
  {
//--- Запоминаем разрешения для мышки и инструментов графика
   this.m_chart_mouse_wheel_flag   = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL);
   this.m_chart_mouse_move_flag    = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE);
   this.m_chart_object_create_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE);
   this.m_chart_mouse_scroll_flag  = ::ChartGetInteger(this.m_chart_id, CHART_MOUSE_SCROLL);
   this.m_chart_context_menu_flag  = ::ChartGetInteger(this.m_chart_id, CHART_CONTEXT_MENU);
   this.m_chart_crosshair_tool_flag= ::ChartGetInteger(this.m_chart_id, CHART_CROSSHAIR_TOOL);
//--- Устанавливаем разрешения для мышки и графика
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL, true);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE, true);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE, true);

//--- Инициализируем цвета объекта по умолчанию
   this.InitColors();
//--- Инициализируем миллисекундный таймер
   ::EventSetMillisecondTimer(16);
   
//--- Флаг владения канвасами
   this.m_canvas_owner=true;
  }

Только для тех объектов, у которых нет собственных канвасов, и которые будут наследоваться от класса CBoundedObj, для них нужно будет этот флаг сбрасывать в false.

Теперь откроем файл Controls.mqh и внесём в него доработки.

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

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W                50          // Ширина текстовой метки по умолчанию
#define  DEF_LABEL_H                16          // Высота текстовой метки по умолчанию
#define  DEF_BUTTON_W               60          // Ширина кнопки по умолчанию
#define  DEF_BUTTON_H               16          // Высота кнопки по умолчанию
#define  DEF_TABLE_ROW_H            16          // Высота строки таблицы по умолчанию
#define  DEF_TABLE_HEADER_H         20          // Высота заголовка таблицы по умолчанию
#define  DEF_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         // Частота автоповторов

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

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

По всему файлу произведём замену строки "CListObj" на строку "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           :  return new CTableCellView();     // Ячейка таблицы (View)
      case ELEMENT_TYPE_TABLE_ROW            :  return new CTableRowView();      // Строка таблицы (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  return new CColumnCaptionView(); // Заголовок столбца таблицы (View)
      case ELEMENT_TYPE_TABLE_HEADER         :  return new CTableHeaderView();   // Заголовок таблицы (View)
      case ELEMENT_TYPE_TABLE                :  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;
     }
  }

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

public:
//--- Возвращает себя
   CElementBase     *GetObject(void)                           { return &this;                     }
//--- Возвращает указатель на (1) класс рисования, (2) список подсказок
   CImagePainter    *Painter(void)                             { return &this.m_painter;           }
   CListElm         *GetListHints(void)                        { return &this.m_list_hints;        }

В классе текстовой метки метод, выводящий текст на канвас, сделаем виртуальным:

//--- Устанавливает координату (1) X, (2) Y текста
   void              SetTextShiftH(const int x)                { this.ClearText(); this.m_text_x=x;            }
   void              SetTextShiftV(const int y)                { this.ClearText(); this.m_text_y=y;            }
   
//--- Выводит текст
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

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

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

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

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

//+------------------------------------------------------------------+
//| CPanel::Рисует внешний вид                                       |
//+------------------------------------------------------------------+
void CPanel::Draw(const bool chart_redraw)
  {
//--- Заливаем объект цветом фона
   this.Fill(this.BackColor(),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);
//--- Задаём цвет для тёмной и светлой линий и рисуем рамку панели
   color clr_dark =(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(),-20,-20,-20));
   color clr_light=(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(),  6,  6,  6));
   if(this.BorderWidthBottom()+this.BorderWidthLeft()+this.BorderWidthRight()+this.BorderWidthTop()!=0)
      this.m_painter.FrameGroupElements(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),
                                        this.m_painter.Width(),this.m_painter.Height(),this.Text(),
                                        this.ForeColor(),clr_dark,clr_light,this.AlphaFG(),true);
   
//--- Обновляем канвас фона без перерисовки графика
   this.m_background.Update(false);
   
//--- Рисуем элементы списка
   for(int i=0;i<this.m_list_elm.Total();i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL && elm.Type()!=ELEMENT_TYPE_SCROLLBAR_H && elm.Type()!=ELEMENT_TYPE_SCROLLBAR_V)
         elm.Draw(false);
     }
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

//+------------------------------------------------------------------+
//| 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            :  element = new CTableRowView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);      break;   // Объект визуального представления строки таблицы
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  element = new CColumnCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break;   // Объект визуального представления заголовка столбца таблицы
      case ELEMENT_TYPE_TABLE_HEADER         :  element = new CTableHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Объект визуального представления заголовка таблицы
      case ELEMENT_TYPE_TABLE                :  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;
  }

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

//+------------------------------------------------------------------+
//| CPanel::Помещает объект на передний план                         |
//+------------------------------------------------------------------+
void CPanel::BringToTop(const bool chart_redraw)
  {
//--- Помещаем панель на передний план
   CCanvasBase::BringToTop(false);
//--- Помещаем на передний план прикреплённые объекты
   for(int i=0;i<this.m_list_elm.Total();i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
        {
         if(elm.Type()==ELEMENT_TYPE_SCROLLBAR_H || elm.Type()==ELEMENT_TYPE_SCROLLBAR_V)
            continue;
         elm.BringToTop(false);
         elm.ObjectTrim();
        }
     }
//--- Если указано - перерисовываем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

//+------------------------------------------------------------------+
//| Класс горизонтальной полосы прокрутки                            |
//+------------------------------------------------------------------+
class CScrollBarH : public CPanel
  {
protected:
   CButtonArrowLeft *m_butt_left;                              // Кнопка со стрелкой влево
   CButtonArrowRight*m_butt_right;                             // Кнопка со стрелкой вправо
   CScrollBarThumbH *m_thumb;                                  // Ползунок скроллбара

public:
//--- Возвращает указатель на (1) левую, (2) правую кнопку, (3) ползунок
   CButtonArrowLeft *GetButtonLeft(void)                       { return this.m_butt_left;                                              }
   CButtonArrowRight*GetButtonRight(void)                      { return this.m_butt_right;                                             }
   CScrollBarThumbH *GetThumb(void)                            { return this.m_thumb;                                                  }

//--- (1) Устанавливает, (2) возвращает флаг обновления графика
   void              SetChartRedrawFlag(const bool flag)       { if(this.m_thumb!=NULL) this.m_thumb.SetChartRedrawFlag(flag);         }
   bool              ChartRedrawFlag(void)               const { return(this.m_thumb!=NULL ? this.m_thumb.ChartRedrawFlag() : false);  }

//--- Возвращает (1) длину (2) начало трека, (3) позицию ползунка
   int               TrackLength(void)    const;
   int               TrackBegin(void)     const;
   int               ThumbPosition(void)  const;
   
//--- Устанавливает позицию ползунка
   bool              SetThumbPosition(const int pos)     const { return(this.m_thumb!=NULL ? this.m_thumb.MoveX(pos) : false);         }
//--- Изменяет размер ползунка
   bool              SetThumbSize(const uint size)       const { return(this.m_thumb!=NULL ? this.m_thumb.ResizeW(size) : false);      }

//--- Изменяет ширину объекта
   virtual bool      ResizeW(const int size);
   
//--- Устанавливает флаг видимости в контейнере
   virtual void      SetVisibleInContainer(const bool flag);
   
//--- Устанавливает флаг обрезки по границам контейнера
   virtual void      SetTrimmered(const bool flag);

//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);
   
//--- Тип объекта
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_SCROLLBAR_H);                                     }
   
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);
   virtual void      InitColors(void);
   
//--- Обработчик прокрутки колёсика (Wheel)
   virtual void      OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam);

//--- Конструкторы/деструктор
                     CScrollBarH(void);
                     CScrollBarH(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);
                    ~CScrollBarH(void) {}
  };

Реализация метода:

//+------------------------------------------------------------------+
//| CScrollBarH::Устанавливает флаг обрезки по границам контейнера   |
//+------------------------------------------------------------------+
void CScrollBarH::SetTrimmered(const bool flag)
  {
   this.m_trim_flag=flag;
   if(this.m_butt_left!=NULL)
      this.m_butt_left.SetTrimmered(flag);
   if(this.m_butt_right!=NULL)
      this.m_butt_right.SetTrimmered(flag);
   if(this.m_thumb!=NULL)
      this.m_thumb.SetTrimmered(flag);
  }

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

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

//+------------------------------------------------------------------+
//| CScrollBarH::Инициализация                                       |
//+------------------------------------------------------------------+
void CScrollBarH::Init(void)
  {
//--- Инициализация родительского класса
   CPanel::Init();
//--- Фон - непрозрачный
   this.SetAlphaBG(255);
//--- Ширина рамки и текст
   this.SetBorderWidth(0);
   this.SetText("");

//--- Создаём кнопки прокрутки
   int w=this.Height();
   int h=this.Height();
   this.m_butt_left = this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_LEFT, "","ButtL",0,0,w,h);
   this.m_butt_right= this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_RIGHT,"","ButtR",this.Width()-w,0,w,h);
   if(this.m_butt_left==NULL || this.m_butt_right==NULL)
     {
      ::PrintFormat("%s: Init failed",__FUNCTION__);
      return;
     }
//--- Настраиваем цвета и вид кнопки со стрелкой влево
   this.m_butt_left.SetImageBound(1,1,w-2,h-4);
   this.m_butt_left.InitBackColors(this.m_butt_left.BackColorFocused());
   this.m_butt_left.ColorsToDefault();
   this.m_butt_left.InitBorderColors(this.BorderColor(),this.m_butt_left.BackColorFocused(),this.m_butt_left.BackColorPressed(),this.m_butt_left.BackColorBlocked());
   this.m_butt_left.ColorsToDefault();

//--- Настраиваем цвета и вид кнопки со стрелкой вправо
   this.m_butt_right.SetImageBound(1,1,w-2,h-4);
   this.m_butt_right.InitBackColors(this.m_butt_right.BackColorFocused());
   this.m_butt_right.ColorsToDefault();
   this.m_butt_right.InitBorderColors(this.BorderColor(),this.m_butt_right.BackColorFocused(),this.m_butt_right.BackColorPressed(),this.m_butt_right.BackColorBlocked());
   this.m_butt_right.ColorsToDefault();
 
//--- Создаём ползунок
   int tsz=this.Width()-w*2;
   this.m_thumb=this.InsertNewElement(ELEMENT_TYPE_SCROLLBAR_THUMB_H,"","ThumbH",w,1,tsz-w*4,h-2);
   if(this.m_thumb==NULL)
     {
      ::PrintFormat("%s: Init failed",__FUNCTION__);
      return;
     }
//--- Настраиваем цвета ползунка и устанавливаем ему флаг перемещаемости
   this.m_thumb.InitBackColors(this.m_thumb.BackColorFocused());
   this.m_thumb.ColorsToDefault();
   this.m_thumb.InitBorderColors(this.m_thumb.BackColor(),this.m_thumb.BackColorFocused(),this.m_thumb.BackColorPressed(),this.m_thumb.BackColorBlocked());
   this.m_thumb.ColorsToDefault();
   this.m_thumb.SetMovable(true);

//--- Запрещаем самостоятельную перерисовку графика
   this.m_thumb.SetChartRedrawFlag(false);
   
//--- Изначально в контейнере не отображается и не обрезается по его границам
   this.SetVisibleInContainer(false);
   this.SetTrimmered(false);
  }

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

В классе объекта контейнера метод CheckElementSizes сделаем публичным:

//+------------------------------------------------------------------+
//| Класс Контейнер                                                  |
//+------------------------------------------------------------------+
class CContainer : public CPanel
  {
private:
   bool              m_visible_scrollbar_h;                    // Флаг видимости горизонтальной полосы прокрутки
   bool              m_visible_scrollbar_v;                    // Флаг видимости вертикальной полосы прокрутки
   int               m_init_border_size_top;                   // Изначальный размер рамки сверху
   int               m_init_border_size_bottom;                // Изначальный размер рамки снизу
   int               m_init_border_size_left;                  // Изначальный размер рамки слева
   int               m_init_border_size_right;                 // Изначальный размер рамки справа
   
//--- Возвращает тип элемента, отправившего событие
   ENUM_ELEMENT_TYPE GetEventElementType(const string name);
   
protected:
   CScrollBarH      *m_scrollbar_h;                            // Указатель на горизонтальную полосу прокрутки
   CScrollBarV      *m_scrollbar_v;                            // Указатель на вертикальную полосу прокрутки
 
//--- Обработчик перетаскивания граней и углов элемента
   virtual void      ResizeActionDragHandler(const int x, const int y);
   
public:
//--- Проверяет размеры элемента для отображения полос прокрутки
   void              CheckElementSizes(CElementBase *element);
protected:
//--- Рассчитывает и возвращает размер (1) ползунка, (2) полный, (3) рабочий размер трека горизонтального скроллбара
   int               ThumbSizeHorz(void);
   int               TrackLengthHorz(void)               const { return(this.m_scrollbar_h!=NULL ? this.m_scrollbar_h.TrackLength() : 0);       }
   int               TrackEffectiveLengthHorz(void)            { return(this.TrackLengthHorz()-this.ThumbSizeHorz());                           }

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

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

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

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

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

На данном этапе будет создана простая статичная таблица без возможности управления её отображением курсором мышки.


Класс ячейки таблицы (View)

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

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

В файле Controls.mqh напишем новый класс:

//+------------------------------------------------------------------+
//| Класс визуального представления ячейки таблицы                   |
//+------------------------------------------------------------------+
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[];                                 // Текст
   
//--- Возвращает смещения начальных координат рисования на холсте относительно канваса и координат базового элемента
   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);
   
public:
//--- (1) Устанавливает, (2) возвращает текст ячейки
   void              SetText(const string text)                         { ::StringToShortArray(text,this.m_text);    }
   string            Text(void)                                   const { return ::ShortArrayToString(this.m_text);  }
   
//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Устанавливает, (2) возвращает индекс ячейки
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }

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

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

//--- Распечатывает в журнале назначенную модель ячейки
   void              TableCellModelPrint(void);
   
//--- (1) Заливает объект цветом фона, (2) обновляет объект для отображения изменений, (3) рисует внешний вид
   virtual void      Clear(const bool chart_redraw);
   virtual void      Update(const bool chart_redraw);
   virtual void      Draw(const bool chart_redraw);
   
//--- Выводит текст
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   virtual int       Compare(const CObject *node,const int mode=0)const { return CBaseObj::Compare(node,mode);       }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_CELL);           }
  
//--- Инициализация объекта класса
   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){}
  };

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

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

//+------------------------------------------------------------------+
//| CTableCellView::Конструктор по умолчанию. Строит объект в главном|
//| окне текущего графика в координатах 0,0 с размерами по умолчанию |
//+------------------------------------------------------------------+
CTableCellView::CTableCellView(void) : CBoundedObj("TableCell",-1,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1),m_text_anchor(ANCHOR_LEFT)
  {
//--- Инициализация
   this.Init("");
   this.SetID(-1);
   this.SetName("TableCell");
  }
//+------------------------------------------------------------------+
//| CTableCellView::Конструктор параметрический. Строит объект       |
//| в указанном окне указанного графика с указанными текстом,        |
//| координатами и размерами                                         |
//+------------------------------------------------------------------+
CTableCellView::CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h) :
   CBoundedObj(user_name,id,x,y,w,h), m_index(-1),m_text_anchor(ANCHOR_LEFT)
  {
//--- Инициализация
   this.Init(text);
   this.SetID(id);
   this.SetName(user_name);
  }

Метод инициализации объекта класса:

//+------------------------------------------------------------------+
//| CTableCellView::Инициализация                                    |
//+------------------------------------------------------------------+
void CTableCellView::Init(const string text)
  {
//--- Класс не управляет канвасами
   this.m_canvas_owner=false;
//--- Текст ячейки
   this.SetText(text);
//--- Смещения текста по умолчанию
   this.m_text_x=2;
   this.m_text_y=0;
  }

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

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

//+------------------------------------------------------------------+
//| CTableCellView::Возвращает описание объекта                      |
//+------------------------------------------------------------------+
string CTableCellView::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());
  }

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

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

//+------------------------------------------------------------------+
//| 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();
  }

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

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

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

//+------------------------------------------------------------------+
//| CTableCellView::Назначает модель ячейки                          |
//+------------------------------------------------------------------+
bool CTableCellView::TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h)
  {
//--- Если передан невалидный объект модели ячейки - сообщаем об этом и возвращаем false
   if(cell_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Если базовый элемент (строка таблицы) не назначен - сообщаем об этом и возвращаем false
   if(this.m_element_base==NULL)
     {
      ::PrintFormat("%s: Error. Base element not assigned. Please use RowAssign() method first",__FUNCTION__);
      return false;
     }
//--- Сохраняем модель ячейки
   this.m_table_cell_model=cell_model;
//--- Устанавливаем координаты и размеры визуального представления ячейки
   this.BoundSetXY(dx,dy);
   this.BoundResize(w,h);
//--- Устанавливаем размеры области рисования визуального представления ячейки
   this.m_painter.SetBound(dx,dy,w,h);
//--- Всё успешно
   return true;
  }

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

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

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

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

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

После расчёта координат верхнего левого угла, в переменные записываются рассчитанные координаты текста и возвращается true.

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

//+------------------------------------------------------------------+
//| CTableCellView::Устанавливает точку привязки текста              |
//+------------------------------------------------------------------+
void CTableCellView::SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw)
  {
   if(this.m_text_anchor==anchor)
      return;
   this.m_text_anchor=anchor;
   if(cell_redraw)
      this.Draw(chart_redraw);
  }

Сначала в переменную записывается новая точка привязки, затем, если установлен флаг перерисовки этой ячейки — вызывается метод её рисования с указанным флагом перерисовки графика.

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

//+------------------------------------------------------------------+
//| CTableCellView::Устанавливает точку привязки и смещения текста   |
//+------------------------------------------------------------------+
void CTableCellView::SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw)
  {
   this.SetTextShiftX(shift_x);
   this.SetTextShiftY(shift_y);
   this.SetTextAnchor(anchor,cell_redraw,chart_redraw);
  }

В этом методе дополнительно к установке точки привязки устанавливаются начальные смещения текста по осям X и Y.

Метод, заливающий объект цветом:

//+------------------------------------------------------------------+
//| CTableCellView::Заливает объект цветом                           |
//+------------------------------------------------------------------+
void CTableCellView::Clear(const bool chart_redraw)
  {
//--- Устанавливаем корректные координаты углов ячейки
   int x1=this.AdjX(this.m_bound.X());
   int y1=this.AdjY(this.m_bound.Y());
   int x2=this.AdjX(this.m_bound.Right());
   int y2=this.AdjY(this.m_bound.Bottom());
//--- Стираем фон и передний план внутри прямоугольной области расположения ячейки
   if(this.m_background!=NULL)
      this.m_background.FillRectangle(x1,y1,x2,y2-1,::ColorToARGB(this.m_element_base.BackColor(),this.m_element_base.AlphaBG()));
   if(this.m_foreground!=NULL)
      this.m_foreground.FillRectangle(x1,y1,x2,y2-1,clrNULL);
  }

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

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

//+------------------------------------------------------------------+
//| CTableCellView::Обновляет объект для отображения изменений       |
//+------------------------------------------------------------------+
void CTableCellView::Update(const bool chart_redraw)
  {
   if(this.m_background!=NULL)
      this.m_background.Update(false);
   if(this.m_foreground!=NULL)
      this.m_foreground.Update(chart_redraw);
  }

Если канвасы фона и переднего плана валидны — вызываются их методы обновления с указанным флагом перерисовки графика.

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

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

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

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

Метод, выводящий текст ячейки:

//+------------------------------------------------------------------+
//| CTableCellView::Выводит текст                                    |
//+------------------------------------------------------------------+
void CTableCellView::DrawText(const int dx,const int dy,const string text,const bool chart_redraw)
  {
//--- Проверяем базовый элемент
   if(this.m_element_base==NULL)
      return;
      
//--- Очищаем ячейку и устанавливаем текст
   this.Clear(false);
   this.SetText(text);
   
//--- Выводим установленный текст на канвасе переднего плана
   this.m_foreground.TextOut(dx,dy,this.Text(),::ColorToARGB(this.m_element_base.ForeColor(),this.m_element_base.AlphaFG()));
   
//--- Если текст выходит за правую границу области ячейки
   if(this.Right()-dx<this.m_foreground.TextWidth(text))
     {
      //--- Получаем размеры текста "троеточие"
      int w=0,h=0;
      this.m_foreground.TextSize("... ",w,h);
      if(w>0 && h>0)
        {
         //--- Стираем текст у правой границы объекта по размеру текста "троеточие" и заменяем троеточием окончание текста метки
         this.m_foreground.FillRectangle(this.AdjX(this.Right())-w,this.AdjY(this.Y()),this.AdjX(this.Right()),this.AdjY(this.Y())+h,clrNULL);
         this.m_foreground.TextOut(this.AdjX(this.Right())-w,this.AdjY(dy),"...",::ColorToARGB(this.m_element_base.ForeColor(),this.m_element_base.AlphaFG()));
        }
     }
//--- Обновляем канвас переднего плана с указанным флагом перерисовки графика
   this.m_foreground.Update(chart_redraw);
  }

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

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

//+------------------------------------------------------------------+
//| CTableCellView::Распечатывает в журнале назначенную модель строки|
//+------------------------------------------------------------------+
void CTableCellView::TableCellModelPrint(void)
  {
   if(this.m_table_cell_model!=NULL)
      this.m_table_cell_model.Print();
  }

Метод позволяет проконтролировать, какая ячейки компонента Model назначена на ячейку компонента View.

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

//+------------------------------------------------------------------+
//| CTableCellView::Сохранение в файл                                |
//+------------------------------------------------------------------+
bool CTableCellView::Save(const int file_handle)
  {
//--- Сохраняем данные родительского объекта
   if(!CBaseObj::Save(file_handle))
      return false;
  
//--- Сохраняем номер ячейки
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем точку привязки текста
   if(::FileWriteInteger(file_handle,this.m_text_anchor,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем координату X текста
   if(::FileWriteInteger(file_handle,this.m_text_x,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем координату Y текста
   if(::FileWriteInteger(file_handle,this.m_text_y,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем текст
   if(::FileWriteArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
      
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CTableCellView::Загрузка из файла                                |
//+------------------------------------------------------------------+
bool CTableCellView::Load(const int file_handle)
  {
//--- Загружаем данные родительского объекта
   if(!CBaseObj::Load(file_handle))
      return false;
      
//--- Загружаем номер ячейки
   this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем точку привязки текста
   this.m_text_anchor=(ENUM_ANCHOR_POINT)::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем координату X текста
   this.m_text_x=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем координату Y текста
   this.m_text_y=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем текст
   if(::FileReadArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
   
//--- Всё успешно
   return true;
  }

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

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


Класс строки таблицы (View)

Строка таблицы — это объект, унаследованный от класса панели. Имеет список объектов ячеек класса CTableCellView.

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

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

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

//--- Распечатывает в журнале назначенную модель строки
   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);   }
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);
   virtual void      InitColors(void);

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

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Конструктор по умолчанию. Строит объект в главном |
//| окне текущего графика в координатах 0,0 с размерами по умолчанию |
//+------------------------------------------------------------------+
CTableRowView::CTableRowView(void) : CPanel("TableRow","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1)
  {
//--- Инициализация
   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)
  {
//--- Инициализация
   this.Init();
  }

Инициализация объекта:

//+------------------------------------------------------------------+
//| CTableRowView::Инициализация                                     |
//+------------------------------------------------------------------+
void CTableRowView::Init(void)
  {
//--- Инициализация родительского объекта
   CPanel::Init();
//--- Фон - непрозрачный
   this.SetAlphaBG(255);
//--- Ширина рамки
   this.SetBorderWidth(1);
  }

Метод инициализации цветов объекта по умолчанию:

//+------------------------------------------------------------------+
//| CTableRowView::Инициализация цветов объекта по умолчанию         |
//+------------------------------------------------------------------+
void CTableRowView::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,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);
  }

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

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

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

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Устанавливает модель строки                       |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelAssign(CTableRow *row_model)
  {
//--- Если передан пустой объект - сообщаем об этом и возвращаем false
   if(row_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Если в переданной модели строки нет ни одной ячейки - сообщаем об этом и возвращаем false
   int total=(int)row_model.CellsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Сохраняем указатель на переданную модель строки и рассчитываем ширину ячейки
   this.m_table_row_model=row_model;
   int cell_w=(int)::round((double)this.Width()/(double)total);
   
//--- В цикле по количеству ячеек в модели строки
   for(int i=0;i<total;i++)
     {
      //--- получаем модель очередной ячейки,
      CTableCell *cell_model=this.m_table_row_model.GetCell(i);
      if(cell_model==NULL)
         return false;
      //--- рассчитываем координату и создаём имя для области ячейки
      int x=cell_w*i;
      string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i;
      //--- Создаём новую область ячейки
      CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height());
      if(cell_bound==NULL)
         return false;
      //--- Создаём новый объект визуального представления ячейки
      CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height());
      if(cell_view==NULL)
         return false;
      //--- На текущую область ячейки назначаем соответствующий объект визуального представления ячейки
      cell_bound.AssignObject(cell_view);
     }
//--- Всё успешно
   return true;
  }

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

Метод, рисующий внешний вид строки:

//+------------------------------------------------------------------+
//| CTableRowView::Рисует внешний вид                                |
//+------------------------------------------------------------------+
void CTableRowView::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()));
  
//--- Рисуем ячейки строки
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Получаем область очередной ячейки
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Из области ячейки получаем присоединённый объект ячейки
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      //--- Рисуем визуальное представление ячейки
      if(cell_view!=NULL)
         cell_view.Draw(false);
     }
//--- Обновляем канвасы фона и переднего плана с указанным флагом перерисовки графика
   this.Update(chart_redraw);
  }

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

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

//+------------------------------------------------------------------+
//| CTableRowView::Распечатывает в журнале назначенную модель строки |
//+------------------------------------------------------------------+
void CTableRowView::TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_table_row_model!=NULL)
      this.m_table_row_model.Print(detail,as_table,cell_width);
  }

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

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

//+------------------------------------------------------------------+
//| 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;
   
//--- Всё успешно
   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_id=this.m_index=(uchar)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Всё успешно
   return true;
  }

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

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


Класс заголовка столбца таблицы (View)

Заголовок столбца таблицы — это самостоятельный объект, унаследованный от объекта "Кнопка" (CButton), наследующий поведение этого объекта и дополняющий его. В текущей реализации свойства кнопки не будут дополняться — просто поменяем цвета различных состояний. В следующей статье добавим к заголовку индикацию направления сортировки — стрелочки вверх/вниз в правой стороне пространства кнопки и событийную модель для запуска сортировки по нажатию на кнопку.

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

//+------------------------------------------------------------------+
//| Класс визуального представления заголовка столбца таблицы        |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CButton
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Указатель на модель заголовка столбца
   int               m_index;                                        // Индекс в списке столбцов
   
public:
//--- Устанавливает идентификатор
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Устанавливает, (2) возвращает индекс ячейки
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }
   
//--- (1) Назначает, (2) возвращает модель заголовка столбца
   bool              ColumnCaptionModelAssign(CColumnCaption *caption_model);
   CColumnCaption   *ColumnCaptionModel(void)                           { return this.m_column_caption_model;        }

//--- Распечатывает в журнале назначенную модель заголовка столбца
   void              ColumnCaptionModelPrint(void);
 
//--- Рисует внешний вид
   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_COLUMN_CAPTION); }
  
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Возвращает описание объекта
   virtual string    Description(void);
   
//--- Конструкторы/деструктор
                     CColumnCaptionView(void);
                     CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); 
                    ~CColumnCaptionView (void){}
  };

Метод имеет индекс, равный идентификатору, как и у двух предыдущих рассмотренных классов. И метод установки идентификатора аналогичен предыдущим классам.

В конструкторах класса вызывается метод инициализации объекта и устанавливается нулевой идентификатор (он меняется после создания объекта):

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

Метод инициализации объекта:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Инициализация                                |
//+------------------------------------------------------------------+
void CColumnCaptionView::Init(const string text)
  {
//--- Смещения текста по умолчанию
   this.m_text_x=4;
   this.m_text_y=2;
//--- Устанавливаем цвета различных состояний
   this.InitColors();
  }

Метод инициализации цветов объекта по умолчанию:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Инициализация цветов объекта по умолчанию    |
//+------------------------------------------------------------------+
void CColumnCaptionView::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(clrWhiteSmoke,this.GetBackColorControl().NewColor(clrWhiteSmoke,-6,-6,-6),clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   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);
  }

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

//+------------------------------------------------------------------+
//| CColumnCaptionView::Рисует внешний вид                           |
//+------------------------------------------------------------------+
void CColumnCaptionView::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем слева светлую вертикальную линию, справа - тёмную
   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);
  }

Сначала фон заливается установленным цветом, затем рисуются две вертикальные линии — слева светлая, а справа — тёмная. Далее на канвас переднего плана выводится текст заголовка.

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

//+------------------------------------------------------------------+
//| CColumnCaptionView::Возвращает описание объекта                  |
//+------------------------------------------------------------------+
string CColumnCaptionView::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());
  }

Метод возвращает описание типа элемента с идентификатором, координатами и размерами объекта.

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

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

Предоставляют возможность сохранения и загрузки параметров заголовка из файла.


Класс заголовка таблицы (View)

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

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

Продолжим писать код в том же файле:

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

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

//--- Распечатывает в журнале назначенную модель заголовка таблицы
   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) сохранения в файл, (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);   }
  
//--- Инициализация (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)
  {
//--- Инициализация
   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)
  {
//--- Инициализация
   this.Init();
  }

Метод инициализации объекта:

//+------------------------------------------------------------------+
//| CTableHeaderView::Инициализация                                  |
//+------------------------------------------------------------------+
void CTableHeaderView::Init(void)
  {
//--- Инициализация родительского объекта
   CPanel::Init();
//--- Цвет фона - непрозрачный
   this.SetAlphaBG(255);
//--- Ширина рамки
   this.SetBorderWidth(1);
  }

Метод инициализации цветов объекта по умолчанию:

//+------------------------------------------------------------------+
//| CTableHeaderView::Инициализация цветов объекта по умолчанию      |
//+------------------------------------------------------------------+
void CTableHeaderView::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,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);
  }

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Создаёт и добавляет в список                   |
//| новый объект представления заголовка столбца                     |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::InsertNewColumnCaptionView(const string text,const int x,const int y,const int w,const int h)
  {
//--- Создаём наименование объекта и возвращаем результат создания нового заголовка столбца
   string user_name="ColumnCaptionView"+(string)this.m_list_elm.Total();
   CColumnCaptionView *caption_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_COLUMN_CAPTION,text,user_name,x,y,w,h);
   return(caption_view!=NULL ? caption_view : NULL);
  }

Объект заголовка столбца создаётся стандартным для объектов библиотеки методом InsertNewElement(), размещающего созданные объекты в списке графических элементов объекта.

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

//+------------------------------------------------------------------+
//| 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)::round((double)this.Width()/(double)total);
   
//--- В цикле по количеству заголовков столбцов в модели заголовка таблицы
   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;
      //--- Создаём новый объект визуального представления заголовка столбца
      CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height());
      if(caption_view==NULL)
         return false;
      //--- На текущую область заголовка столбца назначаем соответствующий объект визуального представления заголовка столбца
      caption_bound.AssignObject(caption_view);
     }
//--- Всё успешно
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Рисует внешний вид                             |
//+------------------------------------------------------------------+
void CTableHeaderView::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++)
     {
      //--- Получаем область очередного заголовка столбца
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Из области заголовка столбца получаем присоединённый объект заголовка столбца
      CColumnCaptionView *caption_view=cell_bound.GetAssignedObj();
      //--- Рисуем визуальное представление заголовка столбца
      if(caption_view!=NULL)
         caption_view.Draw(false);
     }
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Распечатывает в журнале                        |
//| назначенную модель заголовка таблицы                             |
//+------------------------------------------------------------------+
void CTableHeaderView::TableHeaderModelPrint(const bool detail,const bool as_table=false,const int cell_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_table_header_model!=NULL)
      this.m_table_header_model.Print(detail,as_table,cell_width);
  }

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

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



Класс таблицы (View)

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

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

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

//--- Распечатывает в журнале назначенную модель (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);

//--- Рисует внешний вид
   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);          }
  
//--- Инициализация (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){}
  };

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

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

В конструкторах класса вызываем метод инициализации объекта:

//+------------------------------------------------------------------+
//| 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_table_area(NULL),m_table_area_container(NULL)
  {
//--- Инициализация
   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_table_obj(NULL),m_header_view(NULL),m_table_area(NULL),m_table_area_container(NULL)
  {
//--- Инициализация
   this.Init();
  }

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

//+------------------------------------------------------------------+
//| CTableView::Инициализация                                        |
//+------------------------------------------------------------------+
void CTableView::Init(void)
  {
//--- Инициализация родительского объекта
   CPanel::Init();
//--- Ширина рамки
   this.SetBorderWidth(1);
//--- Создаём заголовок таблицы
   this.m_header_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_HEADER,"","TableHeader",0,0,this.Width(),DEF_TABLE_HEADER_H);
   if(this.m_header_view==NULL)
      return;
   this.m_header_view.SetBorderWidth(1);
   
//--- Создаём контейнер, в котором будет находиться панель строк таблицы
   this.m_table_area_container=this.InsertNewElement(ELEMENT_TYPE_CONTAINER,"","TableAreaContainer",0,DEF_TABLE_HEADER_H,this.Width(),this.Height()-DEF_TABLE_HEADER_H);
   if(this.m_table_area_container==NULL)
      return;
   this.m_table_area_container.SetBorderWidth(0);
   this.m_table_area_container.SetScrollable(true);
   
//--- Присоединяем к контейнеру панель для хранения строк таблицы
   int shift_y=0;
   this.m_table_area=this.m_table_area_container.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableAreaPanel",0,shift_y,this.m_table_area_container.Width(),this.m_table_area_container.Height()-shift_y);
   if(m_table_area==NULL)
      return;
   this.m_table_area.SetBorderWidth(0);
  }

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

//+------------------------------------------------------------------+
//| CTableView::Устанавливает модель таблицы                         |
//+------------------------------------------------------------------+
bool CTableView::TableModelAssign(CTableModel *table_model)
  {
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
   this.m_table_model=table_model;
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Устанавливает модель заголовка таблицы               |
//+------------------------------------------------------------------+
bool CTableView::HeaderModelAssign(CTableHeader *header_model)
  {
   if(header_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
   this.m_header_model=header_model;
   return true;
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Устанавливает объект таблицы                         |
//+------------------------------------------------------------------+
bool CTableView::TableObjectAssign(CTable *table_obj)
  {
//--- Если передан пустой объект таблицы - сообщаем об этом и возвращаем false
   if(table_obj==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Сохраняем указатель в переменную
   this.m_table_obj=table_obj;
//--- Записываем результат назначения модели таблицы и модели заголовка
   bool res=this.TableModelAssign(this.m_table_obj.GetTableModel());
   res &=this.HeaderModelAssign(this.m_table_obj.GetTableHeader());
   
//--- Если не удалось назначить какую-либо модель - возвращаем false
   if(!res)
      return false;
   
//--- Записываем результат создания заголовка таблицы из модели и таблицы из модели
   res=this.CreateHeader();
   res&=this.CreateTable();
   
//--- Возвращаем результат
   return res;
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Создаёт из модели объект таблицы                     |
//+------------------------------------------------------------------+
bool CTableView::CreateTable(void)
  {
   if(this.m_table_area==NULL)
      return false;
   
//--- В цикле создаёи и присоединяем к элементу Panel (m_table_area) RowsTotal строк из элементов TableRowView
   int total=(int)this.m_table_model.RowsTotal();
   int y=1;                   // Смещение по вертикали
   int table_height=0;        // Рассчитываемая высота панели
   CTableRowView *row=NULL;
   for(int i=0;i<total;i++)
     {
      //--- Создаём и присоединяем к панели объект строки таблицы
      row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H);
      if(row==NULL)
         return false;
      
      //--- Устанавливаем идентификатор строки
      row.SetID(i);
      //--- В зависимости от номера строки (чет/нечет) устанавливаем цвет её фона
      if(row.ID()%2==0)
         row.InitBackColorDefault(clrWhite);
      else
         row.InitBackColorDefault(clrWhiteSmoke);
      row.BackColorToDefault();
      row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4));
      
      //--- Получаем модель строки из объекта таблицы
      CTableRow *row_model=this.m_table_model.GetRow(i);
      if(row_model==NULL)
         return false;
      //--- Созданному объекту строки таблицы назначаем полученную модель строки
      row.TableRowModelAssign(row_model);
      //--- Рассчитываем новое значение высоты панели
      table_height+=row.Height();
     }
//--- Возвращаем результат изменения размера панели на рассчитанное в цикле значение
   return this.m_table_area.ResizeH(table_height+y);
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Рисует внешний вид                                   |
//+------------------------------------------------------------------+
void CTableView::Draw(const bool chart_redraw)
  {
//--- Рисуем заголовок и строки таблицы
   this.m_header_view.Draw(false);
   this.m_table_area_container.Draw(false);
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Распечатывает в журнале назначенную модель таблицы   |
//+------------------------------------------------------------------+
void CTableView::TableModelPrint(const bool detail)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.Print(detail);
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Распечатывает в журнале назначенную модель заголовка |
//+------------------------------------------------------------------+
void CTableView::HeaderModelPrint(const bool detail,const bool as_table=false,const int column_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_header_model!=NULL)
      this.m_header_model.Print(detail,as_table,column_width);
  }

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

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

//+------------------------------------------------------------------+
//| CTableView::Распечатывает в журнале назначенный объект таблицы   |
//+------------------------------------------------------------------+
void CTableView::TablePrint(const int column_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_table_obj!=NULL)
      this.m_table_obj.Print(column_width);
  }

Распечатывает в журнале всю назначенную объекту таблицу — заголовок и данные.

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


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

В каталоге терминала \MQL5\Indicators\Tables\ создадим новый индикатор, не имеющий буферов и строящийся в подокне графика:

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

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

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

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

CPanel     *panel=NULL; // Указатель на графический элемент Panel
CTable     *table;      // Указатель на объект таблицы (Model)

Далее напишем в обработчике OnInit() индикатора такой код:

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

//--- Создаём графический элемент "Панель"
   panel=new CPanel("Panel","",0,wnd,100,40,400,192);
   if(panel==NULL)
      return INIT_FAILED;
//--- Устанавливаем параметры панели
   panel.SetID(1);                     // Идентификатор 
   panel.SetAsMain();                  // На графике обязательно должен быть один главный элемент
   panel.SetBorderWidth(1);            // Ширина рамки (отступ видимой области на один пиксель с каждой стороны контейнера)
   panel.SetResizable(false);          // Возможность менять размеры перетаскиванием за грани и углы отключена
   panel.SetName("Main container");    // Наименование
   
//--- Создаём данные для таблицы
//--- Объявляем и заполняем массив заголовков столбцов с размерностью 4
   string captions[4]={"Column 0","Column 1","Column 2","Column 3"};
  
//--- Объявляем и заполняем массив данных с размерностью 10x4
//--- Тип массива может быть double, long, datetime, color, string
   long array[10][4]={{ 1,  2,  3,  4},
                      { 5,  6,  7,  8},
                      { 9, 10, 11, 12},
                      {13, 14, 15, 16},
                      {17, 18, 19, 20},
                      {21, 22, 23, 24},
                      {25, 26, 27, 28},
                      {29, 30, 31, 32},
                      {33, 34, 35, 36},
                      {37, 38, 39, 40}};
//--- Создаём объект таблицы из вышесозданного long-массива array 10x4 и string-массива заголовков столбцов (компонент Model)
   table=new CTable(array,captions);
   if(table==NULL)
      return INIT_FAILED;
   PrintFormat("The [%s] has been successfully created:",table.Description());
   
//--- На панели создаём новый элемент - таблицу (компонент View)
   CTableView *table_view=panel.InsertNewElement(ELEMENT_TYPE_TABLE,"","TableView",4,4,panel.Width()-8,panel.Height()-8);
//--- Графическому элементу "Таблица" (View) назначаем объект таблицы (Model)
   table_view.TableObjectAssign(table);
//--- Распечатаем в журнале модель таблицы
   table_view.TablePrint();
   
//--- Нарисуем таблицу вместе с панелью
   panel.Draw(true);

//--- Успешно
   return(INIT_SUCCEEDED);
  }

Здесь есть три блока кода:

  1. Создание панели, на которой будет построена таблица;
  2. Создание табличных данных в двух массивах — таблица и заголовок (компоненты Model);
  3. Создание таблицы на панели (компонент View).

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

Допишем остальной код индикатора:

//+------------------------------------------------------------------+
//| Custom deindicator initialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Удаляем элемент Панель и уничтожаем таблицу и менеджер общих ресурсов библиотеки
   delete panel;
   delete table;
   CCommonManager::DestroyInstance();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Вызываем обработчик OnChartEvent элемента Панель
   panel.OnChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
//--- Вызываем обработчик OnTimer элемента Панель
   panel.OnTimer();
  }

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

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

The [Table: Rows total: 10, Columns total: 4] has been successfully created:
Table: Rows total: 10, Columns total: 4:
|                n/n |           Column 0 |           Column 1 |           Column 2 |           Column 3 |
| 0                  |                  1 |                  2 |                  3 |                  4 |
| 1                  |                  5 |                  6 |                  7 |                  8 |
| 2                  |                  9 |                 10 |                 11 |                 12 |
| 3                  |                 13 |                 14 |                 15 |                 16 |
| 4                  |                 17 |                 18 |                 19 |                 20 |
| 5                  |                 21 |                 22 |                 23 |                 24 |
| 6                  |                 25 |                 26 |                 27 |                 28 |
| 7                  |                 29 |                 30 |                 31 |                 32 |
| 8                  |                 33 |                 34 |                 35 |                 36 |
| 9                  |                 37 |                 38 |                 39 |                 40 |

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


Заключение

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

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

#
 Имя Тип
Описание
 1  Tables.mqh  Библиотека классов  Классы для создания модели таблицы
 2  Base.mqh  Библиотека классов  Классы для создания базового объекта элементов управления
 3  Controls.mqh  Библиотека классов  Классы элементов управления
 4  iTestTable.mq5  Тестовый индикатор  Индикатор для тестирования работы с элементом управления TableView
 5  MQL5.zip  Архив  Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала

Все созданные файлы прилагаются к статье для самостоятельного изучения. Файл архива можно распаковать в папку терминала, и все файлы будут расположены в нужной папке: \MQL5\Indicators\Tables\.

Прикрепленные файлы |
Tables.mqh (261.78 KB)
Base.mqh (294.37 KB)
Controls.mqh (650.82 KB)
iTestTable.mq5 (10.71 KB)
MQL5.zip (111.29 KB)
Нейросети в трейдинге: Модель адаптивной графовой диффузии (SAGDFN) Нейросети в трейдинге: Модель адаптивной графовой диффузии (SAGDFN)
В статье мы раскрываем архитектуру SAGDFN — современного фреймворка, способного преобразовать подход к обработке пространственно-временных данных. Он сохраняет ключевую информацию даже в сложных графах и при этом снижает вычислительные издержки.
Введение в MQL5 (Часть 10): Руководство по работе со встроенными индикаторами в MQL5 для начинающих Введение в MQL5 (Часть 10): Руководство по работе со встроенными индикаторами в MQL5 для начинающих
В этой статье описывается работа со встроенными индикаторами в MQL5, отдельное внимание уделяется созданию советника на основе индикатора RSI с использованием проектного подхода. Вы научитесь получать и использовать значения RSI, обрабатывать колебания ликвидности и улучшать визуализацию торговли с помощью графических объектов. Кроме того, в статье рассматривается еще один важный аспект. Сюда относится риск в процентах от депозита, соотношение риска и доходности, а также модификация риска на ходу для защиты прибыли.
От новичка до эксперта: Программирование японских свечей От новичка до эксперта: Программирование японских свечей
В настоящей статье сделаем первый шаг в программировании на MQL5, даже для совсем новичков. Мы покажем вам, как преобразовать знакомые свечные паттерны в полнофункциональный пользовательский индикатор. Свечные паттерны ценны тем, что они отражают реальное движение цены и сигнализируют о сдвигах на рынке. Вместо ручного сканирования графиков — подхода, чреватого ошибками и неэффективностью, — мы обсудим, как автоматизировать этот процесс с помощью индикатора, идентифицирующего и помечающего паттерны для вас. Попутно рассмотрим такие ключевые понятия, как индексация, временные ряды, средний истинный диапазон (для обеспечения точности при различной волатильности рынка), а также разработку пользовательской библиотеки свечных паттернов для многократного использования в будущих проектах.
Анализ почасового движения торговых символов и их спредов в MetaTrader 5 Анализ почасового движения торговых символов и их спредов в MetaTrader 5
Индикатор индекса сезонности ProSpread со скользящим средним, как инструмент технического анализа, который выявляет сезонные закономерности ценового движения, анализирует поведение цены в определенные часы торговли, может работать как с одним инструментом, так и со спредом между двумя активами, а также визуализирует статистическую вероятность направленных движений.