Русский
preview
Tables in the MVC Paradigm in MQL5: Integrating the Model Component into the View Component

Tables in the MVC Paradigm in MQL5: Integrating the Model Component into the View Component

MetaTrader 5Examples |
239 0
Artyom Trishkin
Artyom Trishkin

Contents



Introduction

In article “Table and Header Classes based on a table model in MQL5: Applying the MVC concept" we completed creating the table model class (in the MVC concept it means the Model component). Next, we were developing a library of simple controls that allow for creating controls based on them that are completely different in purpose and complexity. In particular, the View component for creating the TableView control.

This article will cover implementation of the interaction between the Model component and the View component. In other words, today we will combine tabular data with their graphical representation in a single control.

The control will be created based on the Panel object class, and will consist of several elements:

  1. Panel is the base, the substrate to which the table header and the data area of the table are attached;
  2. Panel is a table header consisting of a number of elements: column headers created based on the Button object class;
  3. Container is the tabular data container with scrollable content;
  4. Panel is a panel for arranging table rows on it (attached to the container (item 3) and, when going beyond the container, scrolls using container scrollbars);
  5. Panel — table row is a panel for drawing table cells on it, attached to the panel (item 4). A number of such objects corresponds to the number of rows in the table;
  6. The table cell class is a new class that allows drawing at specified coordinates on the indicated canvas (CCanvas). An object of this class is attached to a table row object (item 5), the canvases of the table row object are specified as drawing elements; and the table cell is drawn on this panel at the specified coordinates. The area of each table cell is set in the table row object (item 5) by an instance of the CBound class object, and an object of the table cell class is attached to this object.

This algorithm for constructing rows and cells in a table, where each row is divided into horizontal areas to which cell class objects are attached, makes it easy to sort and reposition cells by simply reassigning cells to the desired areas of the row. 

Based on the above written, we understand that we need a new object class to which a pointer to the control will be passed, and drawing will take place on its canvas. Currently, all library objects, when created by the new operator, create their own canvas objects (background and foreground). But we want an object that will enable setting and getting its size, as well setting a pointer to the control in it, on which the drawing will be performed.

The CBound class enables to set and retrieve the dimensions of the created object, and it is located as part of the base object of all controls. Background and foreground canvases are created in that very object. This means that another intermediate object should be created that will inherit from the base one and will include the CBound class for setting and getting dimensions. The class in which CCanvas objects are created will inherit from this object. Thus, from this intermediate class we can inherit the class to which a pointer to the control on which we will draw, will be passed.

Let's finalize the files of the already created classes, and only then write new ones.


Refining Library Classes

All files of the library being developed are located at \MQL5\Indicators\Tables\Controls\. If they are not available yet, a previous version of all files is available in the previous article. The project will also require a table class file (the Model component), which can be obtained from the article where we completed their development. Tables.mqh must be saved to \MQL5\Indicators\Tables\. Files Base.mqh and Control.mqh will be refined.

At the very beginning of Tables.mqh file, in the macros section, write the file ID:

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

This is necessary so that the MARKER_START_DATA macro substitution can be declared in two different include files for their separate compilation.

Open Base.mqh file. Connect the class file of the table model to it and specify the forward declaration of new classes:

//+------------------------------------------------------------------+
//|                                                         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

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

In the macro substitutions section, frame the declaration of MARKER_START_DATA macro by verifying its existence:

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#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           // Толщина зоны для захвата границы/угла

Now declaring the same macro substitution in two different files will not result in compilation errors, either individually or jointly between the two files.

Add new types to the enumeration of types of UI elements, and set a new maximum type of the active element:

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
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  // Максимальное значение списка активных элементов

Add new return values to the function that returns the element’s short name by type:

//+------------------------------------------------------------------+
//|  Возвращает короткое имя элемента по типу                        |
//+------------------------------------------------------------------+
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
     }
  }

In the base class of UI elements, make the identifier setting method virtual, so far as it should be redefined in the new classes:

//+------------------------------------------------------------------+
//| Базовый класс графических элементов                              |
//+------------------------------------------------------------------+
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) {}
  };

Each object within its composition has a rectangular area class CBound, which returns the size of the control. Also, each control has a list of CBound objects. This list allows you to store many CBound objects, which in turn allow you to specify many different areas on the UI element surface.

Each such area can be used for any scenarios. For example, you can define a certain zone inside an UI element, track the cursor location inside this area and react to the interaction of this area with the mouse cursor. A certain control can be placed within a certain area to display it inside that area. To do this, it is necessary to make it possible to write a pointer to the control to the CBound object.

Implement such a feature in the CBound class:

//+------------------------------------------------------------------+
//| Класс прямоугольной области                                      |
//+------------------------------------------------------------------+
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); }
  };

At the moment, the CBound rectangular area object is declared in the CCanvasBase class, in which two CCanvas objects for drawing the background and foreground of an UI element are created. That is, all the UI elements have two canvases within them. But we want to create a class that will allow us to specify in it the control on which canvases we want to draw. At the same time, not to create own canvases, and at the same time not to stray from the general concept of creating graphical elements.

This means that we want to transfer the CBound class object from the CCanvasBase class to another parent class, which will not create canvas objects, and from which the CCanvasBase class will inherit. And in the CCanvasBase class, not to declare instances of two CCanvas, but to declare pointers to the canvases being created and implement a method that will create two canvas objects. Also, we want to declare a flag indicating that a graphical element controls or does not control canvas objects of the background and foreground. This is required to manage the deletion of created canvas objects.

In the Base.mqh file, immediately after the CAutoRepeat class, before the CCanvasBase class, implement a new CBoundedObj class, inherited from CBaseObj, into which simply transfer all methods of working with the CBound object from the CCanvasBase class:

//+------------------------------------------------------------------+
//| Базовый класс, хранящий размеры объекта                          |
//+------------------------------------------------------------------+
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);
  }

The presented class simply inherits properties of the CBaseObj base class, allows to specify object boundaries (the CBound class), and has a canvas management flag. 

The base class of the graphical elements canvas should now be inherited from the new CBoundedObj, rather than from the CBaseObj class, as it was before. And everything related to working with the CBound class object to set and return sizes of an element has already been moved to a new CBoundedObj class and removed from the CCanvasBase class. Now canvas objects are not declared as instances of a class, but as pointers to CCanvas objects being created:

//+------------------------------------------------------------------+
//| Базовый класс холста графических элементов                       |
//+------------------------------------------------------------------+
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;                           // Объект управления цветом рамки

Methods for obtaining coordinates and sizes of canvas graphical objects are performed public, and methods for resizing canvases are implemented virtual:

//--- Возврат координат, границ и размеров графического объекта
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));                                 }

A method for creating canvases is declared in the protected section of the class.:

//--- Устанавливает указатель на родительский объект-контейнер
   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;                                                              }

The method that sets a flag for allowing cropping of an element along container boundaries is implemented virtual:

//--- Устанавливает объекту флаг (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;                                                            }

In the class constructor a method for creating canvas objects for background and foreground is now called:

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

A method that creates canvases for the background and foreground:

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

In the method that destroys the object, now check the canvas management flag:

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

Canvases are deleted only if an object manages canvases. If canvases of another control are passed to an object for drawing, it will not delete them — they should be deleted only by the object to which they belong.

In the default initialization method of the class, a canvas management flag is set for the object:

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

Only for those objects that do not have their own canvases, and which will inherit from the CBoundedObj class, they will have to reset this flag to false.

Now open the Controls.mqh file and make improvements.

In the macro substitutions section, implement two new ones that define the default height of the table row and header:

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#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         // Частота автоповторов

The file contains a class of linked list of CListObj objects for storing a list of graphical elements. We copied it to this file from the table model class file. If you leave the name of the class the same, you will have a name conflict, and the objects stored in these two lists in different files are completely different. So here leave the class unchanged, but rename it. This will be a class of the linked list of UI elements:

//+------------------------------------------------------------------+
//| Класс связанного списка графических элементов                    |
//+------------------------------------------------------------------+
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);
  };

Replace the string "CListObj" with the string "CListElm" throughout the file.

In the method of creating a list item, add new types of UI elements:

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

In the UI element base class, add a method that returns a pointer to the object:

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

In the text label class, make the method that displays the text on canvas, virtual:

//--- Устанавливает координату (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);

In the CPanel panel class, refine the resizing methods:

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

The essence of methods refinement is that if an element is within a container, or if an element has been resized, then it is necessary to check the new element dimensions with respect to the container. If an element has become larger than container bounds, then the element is cropped according to the container size (this has already been implemented), but scrollbars should still appear on the container. We added this check using the CheckElementSizes() container object method to methods for UI element resizing.

In the method for drawing the appearance of a panel object in the CPanel class, draw a border only if at least one of the four sides has a non-zero value for the BorderWidth parameter:

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

In the method of creating and adding a new element to the list, in the panel object class, add creation of new controls:

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

In the method that places an object in the foreground, add cropping of attached objects according to the container size:

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

There is a flag in the horizontal scrollbar class indicating that the element can be cropped along the container bounds. The scrollbar object consists of several objects, and for each of them the same flag value should be set. Let's do everything in one method. In the class redefine the method that sets the trim flag along the container boundaries:

//+------------------------------------------------------------------+
//| Класс горизонтальной полосы прокрутки                            |
//+------------------------------------------------------------------+
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) {}
  };

Implementing the method:

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

A flag passed to the method is set for each of the objects that make up the scrollbar.

In the object initialization method, you no longer need to set the trim flag for each of the elements along container boundaries. It is sufficient to call the SetTrimmered() method at the end of the method:

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

Exactly the same improvements have been made for the vertical scrollbar class. They are completely identical to those discussed, and we will not repeat them.

Make the CheckElementSizes method public in the container object class:

//+------------------------------------------------------------------+
//| Класс Контейнер                                                  |
//+------------------------------------------------------------------+
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());                           }

This method is called from the elements bound to the container, and must be public to access it.

We have reviewed the main improvements to library classes. Some minor improvements and fixes have been left out of the scope. View full codes in the files attached to the article.

The table model consists of a table cell, a table row, and a list of rows that end up being a table. In addition to the above, the table also has a header consisting of table column headers.

  • A table cell is a separate element that stores the row number, column number, and contents;
  • A table row is a list of table cells that has its own number;
  • A table is a list of rows, and it can have a header that represents a list of column headers.

The View component will have approximately the same structure for displaying data from the table model. The table object will be based on, for example, an array of data and an array of headers of this data. Then we will pass a pointer to the table model (the Model component) to the created table visual display object (the View component) and all data will be written to table cells.

At this milestone, a simple static table will be created without the feature to control its display with the mouse cursor.


Table Cell Class (View)

The table consists of a list of rows. The rows, in turn, consist of objects provided with two canvases for drawing the background and foreground. Table cells are arranged horizontally on their rows. It makes no sense to give cells their own canvases. You can simply "slice" each row into areas, where each area will correspond to the location of its cell. And draw the cell data on the canvas of the table row object.

It is for these purposes that we created an intermediate class at the very beginning. It has only the object size with its coordinates and a pointer to the control on which canvas drawing will be performed. And such an object will be a table cell. We will pass a pointer to the table row object for drawing on its canvases.

Implement a new class in 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){}
  };

All variables and methods here are commented out in the code. The cell's index and its identifier are the same thing here. Therefore, the SetID() virtual method has been redefined, where the index and identifier are set to the same values. Let's look at the implementation of declared methods.

In the class constructors the object initialization method is called, the cell identifier and its name are set:

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

Class Object Initialization Method:

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

Be sure to set a flag for the object that it does not manage canvases, so that when the object is destroyed, it does not delete other objets' canvases.

A Method That Returns Description of the Object:

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

The method prints out to the log a line with the name of the object type, identifier, coordinates and cell sizes.

A Method That Assigns a Table Row and Background and Foreground Canvases To a Cell:

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

A pointer to a table row object is passed to the method (the class description will be below) and if the pointer is not valid, report it and return NULL. Next, a pointer to a table row is written to class variables, and from the table row object pointers are written to canvases of the background and foreground, and to the drawing object.

The table model object also has its own rows and cells. A table cell model is assigned to this class, and it draws data from the cell model on the row object canvas.

A Method That Assigns the Cell Model:

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

First, the validity of the passed pointer to the cell model is checked, then it is checked that the table row has already been assigned to this cell object. If any of this is incorrect, it is reported in the log and false is returned. Next, the pointer to the cell model is stored in a variable and the coordinates and dimensions of the cell area on the corresponding row are set.

When sorting out rows and columns of the table, the pointer to the cell object will simply be reassigned, and this object, which has its exact coordinates in the table, will draw the data of the new cell assigned to it.

A Method That Returns the X and Y Coordinates Of the Text Depending On the Anchor Point:

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

Variables are passed to the method by reference, into which the calculated coordinates of location of the upper-left corner of the output text will be written. If the cell text size fails, the method returns false

After calculating the coordinates of the upper-left corner, the calculated coordinates of the text are written to the variables and return true.

A Method That Sets the Anchor Point of the Text:

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

First, a new anchor point is written to the variable, then, if the redraw flag for this cell is set, the method for drawing it is called with the specified chart redraw flag.

A Method That Sets the Anchor Point and Text Shift:

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

In this method, in addition to setting the anchor point, initial text shifts along the X and Y axes are set.

A Method That Fills In an Object With a Color:

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

Get correct coordinates of the four corners of the rectangular area. Then fill the background with the background color of the table row, and the foreground with a transparent color.

A Method That Updates an Object To Display Changes:

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

If the background and foreground canvases are valid, their update methods are called with the specified chart redrawing flag.

A Method That Draws the Cell View:

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

The method's logic is explained in the comments. Correction of coordinates is necessary if the object is cropped along the container boundaries. In this case, the initial coordinates are calculated not from the actual coordinates of the canvas graphical object, but from the graphical element coordinates, which are virtually located outside the container boundaries.

Dividing lines are drawn only on the right side of the cell. And if it is not the cell on the far right, it is limited by its container, and it is not expedient to draw an additional line there.

A Method For Displaying the Cell Text:

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

The method's logic is fully explained in comments to the code. If the printed text extends beyond the cell area, it is cropped along the area boundaries, and its end is replaced with a colon ("..."), indicating that not all the text fits into the cell size in its width.

A Method That Prints Out the Assigned String Model to the Log:

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

This method allows you to control which cell of the Model component is assigned to the cell of the View component.

A Method for Operating Files:

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

The methods allow you to save the table cell data to a file and load it from the file.

Now let's study the visual representation class of a table row.


Table Row Class (View)

A table row is an object inherited from the panel class. It has a list of cell objects of the CTableCellView class.

Continue writing the code in Controls.mqh file:

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

A table row has the same property as a cell, namely, the row index property is equal to its identifier. Here, the virtual method of setting the identifier is also redefined, and sets both properties simultaneously — the index and the identifier.

In the class constructors the object initialization method is called:

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

Object Initialization:

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

Default Object Color Initialization Method:

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

A Method That Implements and Adds a New Cell Representation Object to the List:

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

In addition to the fact that each cell is assigned to its own area of the table row object, objects created using the new operator must still be stored in the list, or they should be tracked by yourself and deleted, if necessary.

It is easier to store them in a list — then the terminal subsystem itself will keep track of when they need to be deleted. The method creates a new cell view object and places it in the list of row cells. After creating an object, it is assigned a row in which it is located, and on which canvases the created object will draw model data of the table cell.

The Method That Sets the Row Model:

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

A pointer to the row model is passed to the method, and in a loop by the number of cells in the model, new areas of the cell representation objects and the cells themselves are created (CTableCellView class). And each new such cell is assigned to each new area. In the end, we get a list of cell areas to which pointers to the corresponding table cells are assigned.

A Method That Draws the Row View:

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

First, the row is filled with the background color and a line is drawn from below. Then, in a loop through the cell areas of the row, we get the next cell area, from it we get a pointer to the cell object and call its drawing method.

A Method That Prints Out the Assigned String Model to the Log:

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

The method allows you to control which row model from the table model is assigned to this row visual representation object.

A Method for Operating Files:

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

The methods allow you to save the table row data to a file and load it from the file.

Each table must have a header. It allows you to understand what data is displayed in table columns. A table header is a list of column headers. Each column of the table has its own header, which displays the type and name of the data located in the column.


Column header class of the table (View)

The column header of the table is an independent object inherited from the “Button” object (CButton), inheriting this object’s behavior and complementing it. In the current implementation, the button properties will not be supplemented — we simply change colors of various states. In the next article, to the header we will add an indication of the sorting direction — up/down arrows on the right side of the button space and an event model to start sorting by clicking on the button.

Continue writing the code in Controls.mqh file:

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

The method has an index equal to the identifier, as in the case of the two previous classes considered. And the method of setting the identifier is similar to the previous classes.

In the class constructors the object initialization method is called and the null identifier is set (it changes after the object is created):

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

Object Initialization Method:

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

Default Object Color Initialization Method:

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

A Method That Draws the Column Header View:

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

First, the background is filled in with the set color, then two vertical lines are drawn — a light one on the left and a dark one on the right. Next, a title text is displayed on the foreground canvas.

A Method That Returns Description of the Object:

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

The method returns a description of the element type with the ID, coordinates, and dimensions of the object.

A Method That Assigns the Column Header Model:

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

A pointer to the column header model is passed to the method, which is saved to a variable. Next, coordinates and dimensions of the drawing area are set for the drawing object.

A Method That Prints Out  Assigned Column Header Model to the Log:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Распечатывает в журнале                      |
//| назначенную модель заголовка столбца                             |
//+------------------------------------------------------------------+
void CColumnCaptionView::ColumnCaptionModelPrint(void)
  {
   if(this.m_column_caption_model!=NULL)
      this.m_column_caption_model.Print();
  }

Allows you to check, by printing in the log a description of the column header model assigned to the object of visual representation of the header.

A Method for Operating Files:

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

They provide a feature to save and load header parameters from a file.


Table Header Class (View)

The table header is a regular list of column header objects based on Panel control (CPanel). As a result, such an object provides tools for managing table columns. 

In this implementation, it will be a regular static object. All the functionality of user interaction will be added later.

Continue writing the code in the same file:

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

A table header model is assigned to the object, and column header objects are created based on its contents and added to the list of attached elements.

In the class constructors the object initialization method is called:

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

Object Initialization Method:

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

Default Object Color Initialization Method:

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

A Method That Implements and Adds a New Column Header Representation Object to the List:

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

The column header object is created using the standard InsertNewElement() method for library objects, which places created objects in the list of object’s UI elements.

A Method That Sets the Header Model:

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

The table header model is passed to the method. In the loop, according to the number of column headers in the model, create another area to place the column header. Create a column header object and assign it to the current area. At the cycle end, we will have a list of areas with column headers assigned to them.

A Method That Draws the Table Header View:

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

First, the entire table header area is filled in with the background color, then a dividing line is drawn from below. Next, in the loop, through the list of header areas, get another area. And from this area get the column header object assigned to this area. The drawing method of this object we call.

A Method That Prints Out Assigned Table Header Model to the Log:

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

Allows you to print the table header model assigned to this object in the log.

A table, its visual representation, is a composite object that receives data about the table and a header from its model, and draws this data on various controls. This is a panel that contains a header and a container with tabular data (table rows).



Table class (View)

Continue writing the code in Controls.mqh file:

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

The class is inherited from the panel object class. Three Model components and three View components are visible. The class has pointers to the table model, header model, and the table. From the table we get pointers to the tabular data and the header. And there are three pointers to the header, the table row panel and a container for locating this panel.

The point is that the rows of the table are attached to the panel object (many elements can be attached), and this panel is attached to the container (you can attach a single element), where there are scrollbars that can be used to move the panel with table rows. Thus, visually, the element looks like a table header, and below a scrollable table is located (rows and columns formed by cells).

In the class constructors call the object initialization method:

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

In the initialization method, create all the necessary controls on the object panel:

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

A Method That Sets the Table Model:

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

We simply pass a pointer to the table model object to the method and write it to a variable from which we will access it.

A Method That Sets the Table Header Model:

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

Pass a pointer to the table header model object to the method and write it to a variable from which we will access it.

A Method That Sets a Table Object:

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

A pointer to a table object is passed to the method. From this object, get and assign the header model and the table model. Next, using the data from the header and table models, create objects for the visual representation of the header and table.

A Method That Creates a Table Object From a Model:

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

The method's logic is explained in the comments to the code. In a loop, by the number of rows in the table model, create a new table row object and attach it to the panel; while simultaneously calculating the future height of the panel. After completing the loop and creating all the necessary rows, the panel height is adjusted according to the size calculated in the loop.

A Method That Draws the Table View:

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

First, draw the table header, and under it — table rows in the container.

A Method That Prints Out the Assigned Table Model to the Log:

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

It allows printing the assigned table model to the log.

A Method That Prints Out Assigned Table Header Model to the Log:

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

It allows printing the assigned table header model to the log.

A Method That Prints Out the Assigned Table Object to the Log:

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

It prints out the entire table assigned to the object in the log, including the header and data.

That's it, we have implemented all the classes to create a visual representation of the table model. Let's see how you can now create a simple table from an array of data.


Testing the Result

In the terminal directory \MQL5\Indicators\Tables\ create a new indicator which has no buffers and is being built in the chart subwindow:

//+------------------------------------------------------------------+
//|                                                   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

Connect the library to the indicator file and declare pointers to objects globally:

//+------------------------------------------------------------------+
//|                                                   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)

Further, write the following code in the OnInit() handler of the indicator:

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

Here we have three blocks of the code:

  1. Creating a panel on which the table will be built;
  2. Creating tabular data in two arrays — a table and a header (Model components);
  3. Creating a table in the panel (View component).

In fact, two steps are required to create a table — to build data (item 2) and to build a table (item 3). The panel is only needed for decoration and as the basis for a graphical element.

Add the rest of the indicator code:

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

Compile the indicator and run it on the chart:

 The terminal log will print out a message about a successfully created table, table description and the table with a header:

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 |

On the chart, we can see that column headers and table rows are active and respond to mouse hovering. Handling of table's interaction with the user will be discussed in the next article.


Conclusion

We have learned how to create simple tables and display them on a chart. But so far, this is just a static table displaying the data received once. The next article will focus on table animation — changing and displaying dynamic data and interacting with the user to customize the display of rows and columns in a table. As well, in the next article, we will implement even simpler creation of tables.

Programs used in the article:

#
 Name Type
Description
 1  Tables.mqh  Class Library  Classes for creating a table model
 2  Base.mqh  Class Library  Classes for creating a base object of controls
 3  Controls.mqh  Class Library  Control classes
 4  iTestTable.mq5  Test indicator  Indicator for testing manipulations with the TableView control
 5  MQL5.zip  Archive  An archive of the files above for unpacking into the MQL5 directory of the client terminal

All created files are attached to the article for self-study. The archive file can be unzipped to the terminal folder, and all files will be located in the desired folder: \MQL5\Indicators\Tables\.

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/19288

Attached files |
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)
Creating Custom Indicators in MQL5 (Part 1): Building a Pivot-Based Trend Indicator with Canvas Gradient Creating Custom Indicators in MQL5 (Part 1): Building a Pivot-Based Trend Indicator with Canvas Gradient
In this article, we create a Pivot-Based Trend Indicator in MQL5 that calculates fast and slow pivot lines over user-defined periods, detects trend directions based on price relative to these lines, and signals trend starts with arrows while optionally extending lines beyond the current bar. The indicator supports dynamic visualization with separate up/down lines in customizable colors, dotted fast lines that change color on trend shifts, and optional gradient filling between lines, using a canvas object for enhanced trend-area highlighting.
Introduction to MQL5 (Part 32): Mastering API and WebRequest Function in MQL5 (VI) Introduction to MQL5 (Part 32): Mastering API and WebRequest Function in MQL5 (VI)
This article will show you how to visualize candle data obtained via the WebRequest function and API in candle format. We'll use MQL5 to read the candle data from a CSV file and display it as custom candles on the chart, since indicators cannot directly use the WebRequest function.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
From Novice to Expert: Automating Trade Discipline with an MQL5 Risk Enforcement EA From Novice to Expert: Automating Trade Discipline with an MQL5 Risk Enforcement EA
For many traders, the gap between knowing a risk rule and following it consistently is where accounts go to die. Emotional overrides, revenge trading, and simple oversight can dismantle even the best strategy. Today, we will transform the MetaTrader 5 platform into an unwavering enforcer of your trading rules by developing a Risk Enforcement Expert Advisor. Join this discussion to find out more.