Русский
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 |
2 268 2
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:

//+------------------------------------------------------------------+
//| Macros                                                           |
//+------------------------------------------------------------------+
#define  __TABLES__                 // File ID
#define  MARKER_START_DATA    -1    // Data start marker in a file
#define  MAX_STRING_LENGTH    128   // Maximum length of a string in a cell
#define  CELL_WIDTH_IN_CHARS  19    // Table cell width in characters

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 libraries                                                |
//+------------------------------------------------------------------+
#include <Canvas\Canvas.mqh>              // CCanvas class
#include <Arrays\List.mqh>                // CList class
#include "..\Tables.mqh"

//--- Forward declaration of control element classes
class    CBoundedObj;                     // Base class storing object dimensions
class    CCanvasBase;                     // Base class of graphical elements canvas
class    CCounter;                        // Delay counter class
class    CAutoRepeat;                     // Event auto-repeat class
class    CImagePainter;                   // Image drawing class
class    CVisualHint;                     // Hint class
class    CLabel;                          // Text label class
class    CButton;                         // Simple button class
class    CButtonTriggered;                // Two-position button class
class    CButtonArrowUp;                  // Up arrow button class
class    CButtonArrowDown;                // Down arrow button class
class    CButtonArrowLeft;                // Left arrow button class
class    CButtonArrowRight;               // Right arrow button class
class    CCheckBox;                       // CheckBox control class
class    CRadioButton;                    // RadioButton control class
class    CScrollBarThumbH;                // Horizontal scrollbar slider class
class    CScrollBarThumbV;                // Vertical scrollbar slider class
class    CScrollBarH;                     // Horizontal scrollbar class
class    CScrollBarV;                     // Vertical scrollbar class
class    CTableRowView;                   // Class for visual representation of a table row
class    CPanel;                          // Panel control class
class    CGroupBox;                       // GroupBox control class
class    CContainer;                      // Container control class

//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
#define  clrNULL              0x00FFFFFF  // Transparent color for CCanvas
#ifndef  __TABLES__
#define  MARKER_START_DATA    -1          // Data start marker in a file
#endif 
#define  DEF_FONTNAME         "Calibri"   // Default font
#define  DEF_FONTSIZE         10          // Default font size
#define  DEF_EDGE_THICKNESS   3           // Zone width to capture the border/corner

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:

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE                    // Enumeration of graphical element types
  {
   ELEMENT_TYPE_BASE = 0x10000,           // Basic object of graphical elements
   ELEMENT_TYPE_COLOR,                    // Color object
   ELEMENT_TYPE_COLORS_ELEMENT,           // Color object of the graphical object element
   ELEMENT_TYPE_RECTANGLE_AREA,           // Rectangular area of the element
   ELEMENT_TYPE_IMAGE_PAINTER,            // Object for drawing images
   ELEMENT_TYPE_COUNTER,                  // Counter object
   ELEMENT_TYPE_AUTOREPEAT_CONTROL,       // Event auto-repeat object
   ELEMENT_TYPE_BOUNDED_BASE,             // Basic object of graphical element sizes
   ELEMENT_TYPE_CANVAS_BASE,              // Basic canvas object for graphical elements
   ELEMENT_TYPE_ELEMENT_BASE,             // Basic object of graphical elements
   ELEMENT_TYPE_HINT,                     // Hint
   ELEMENT_TYPE_LABEL,                    // Text label
   ELEMENT_TYPE_BUTTON,                   // Simple button
   ELEMENT_TYPE_BUTTON_TRIGGERED,         // Two-position button
   ELEMENT_TYPE_BUTTON_ARROW_UP,          // Up arrow button
   ELEMENT_TYPE_BUTTON_ARROW_DOWN,        // Down arrow button
   ELEMENT_TYPE_BUTTON_ARROW_LEFT,        // Left arrow button
   ELEMENT_TYPE_BUTTON_ARROW_RIGHT,       // Right arrow button
   ELEMENT_TYPE_CHECKBOX,                 // CheckBox control
   ELEMENT_TYPE_RADIOBUTTON,              // RadioButton control
   ELEMENT_TYPE_SCROLLBAR_THUMB_H,        // Horizontal scroll bar slider
   ELEMENT_TYPE_SCROLLBAR_THUMB_V,        // Vertical scroll bar slider
   ELEMENT_TYPE_SCROLLBAR_H,              // ScrollBarHorisontal control
   ELEMENT_TYPE_SCROLLBAR_V,              // ScrollBarVertical control
   ELEMENT_TYPE_TABLE_CELL,               // Table cell (View)
   ELEMENT_TYPE_TABLE_ROW,                // Table row (View)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION,     // Table column header (View)
   ELEMENT_TYPE_TABLE_HEADER,             // Table header (View)
   ELEMENT_TYPE_TABLE,                    // Table (View)
   ELEMENT_TYPE_PANEL,                    // Panel control
   ELEMENT_TYPE_GROUPBOX,                 // GroupBox control
   ELEMENT_TYPE_CONTAINER,                // Container control
  };
#define  ACTIVE_ELEMENT_MIN   ELEMENT_TYPE_LABEL         // Minimum value of the list of active elements
#define  ACTIVE_ELEMENT_MAX   ELEMENT_TYPE_TABLE_HEADER  // Maximum value of the list of active elements

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

//+------------------------------------------------------------------+
//|  Return the short name of the element by type                    |
//+------------------------------------------------------------------+
string ElementShortName(const ENUM_ELEMENT_TYPE type)
  {
   switch(type)
     {
      case ELEMENT_TYPE_ELEMENT_BASE         :  return "BASE";    // Basic object of graphical elements
      case ELEMENT_TYPE_HINT                 :  return "HNT";     // Hint
      case ELEMENT_TYPE_LABEL                :  return "LBL";     // Text label
      case ELEMENT_TYPE_BUTTON               :  return "SBTN";    // Simple button
      case ELEMENT_TYPE_BUTTON_TRIGGERED     :  return "TBTN";    // Toggle button
      case ELEMENT_TYPE_BUTTON_ARROW_UP      :  return "BTARU";   // Up arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN    :  return "BTARD";   // Down arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT    :  return "BTARL";   // Left arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT   :  return "BTARR";   // Right arrow button
      case ELEMENT_TYPE_CHECKBOX             :  return "CHKB";    // CheckBox control
      case ELEMENT_TYPE_RADIOBUTTON          :  return "RBTN";    // RadioButton control
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H    :  return "THMBH";   // Horizontal scroll bar slider
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V    :  return "THMBV";   // Vertical scroll bar slider
      case ELEMENT_TYPE_SCROLLBAR_H          :  return "SCBH";    // ScrollBarHorisontal control
      case ELEMENT_TYPE_SCROLLBAR_V          :  return "SCBV";    // ScrollBarVertical control
      case ELEMENT_TYPE_TABLE_CELL           :  return "TCELL";   // Table cell (View)
      case ELEMENT_TYPE_TABLE_ROW            :  return "TROW";    // Table row (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  return "TCAPT";   // Table column header (View)
      case ELEMENT_TYPE_TABLE_HEADER         :  return "THDR";    // Table header (View)
      case ELEMENT_TYPE_TABLE                :  return "TABLE";   // Table (View)
      case ELEMENT_TYPE_PANEL                :  return "PNL";     // Panel control
      case ELEMENT_TYPE_GROUPBOX             :  return "GRBX";    // GroupBox control
      case ELEMENT_TYPE_CONTAINER            :  return "CNTR";    // Container control
      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:

//+------------------------------------------------------------------+
//| Base class of graphical elements                                 |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   int               m_id;                                     // ID
   ushort            m_name[];                                 // Name
   
public:
//--- Set (1) name and (2) ID
   void              SetName(const string name)                { ::StringToShortArray(name,this.m_name);    }
   virtual void      SetID(const int id)                       { this.m_id=id;                              }
//--- Return (1) name and (2) ID
   string            Name(void)                          const { return ::ShortArrayToString(this.m_name);  }
   int               ID(void)                            const { return this.m_id;                          }

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   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) Return and (2) display the object description in the journal
   virtual string    Description(void);
   virtual void      Print(void);
   
//--- Constructor/destructor
                     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:

//+------------------------------------------------------------------+
//| Rectangular region class                                         |
//+------------------------------------------------------------------+
class CBound : public CBaseObj
  {
protected:
   CBaseObj         *m_assigned_obj;                           // Object assigned to the region
   CRect             m_bound;                                  // Rectangular area structure

public:
//--- Change the bounding rectangular (1) width, (2) height and (3) size
   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);               }
   
//--- Set (1) X, (2) Y and (3) both coordinates of the bounding rectangle
   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) Set and (2) shift the bounding rectangle by the specified coordinates/offset size
   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);                                   }
   
//--- Returns the object coordinates, dimensions, and boundaries
   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);}

//--- Returns the flag indicating whether the cursor is inside the area
   bool              Contains(const int x,const int y)   const { return this.m_bound.Contains(x,y);                           }
   
//--- (1) Assign and (2) return the pointer to the assigned element
   void              AssignObject(CBaseObj *obj)               { this.m_assigned_obj=obj;                                     }
   CBaseObj         *GetAssignedObj(void)                      { return this.m_assigned_obj;                                  }
   
//--- Return the object description
   virtual string    Description(void);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   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);                         }
   
//--- Constructors/destructor
                     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:

//+------------------------------------------------------------------+
//| Base class storing object dimensions                             |
//+------------------------------------------------------------------+
class CBoundedObj : public CBaseObj
  {
protected:
   CBound            m_bound;                                  // Object boundaries
   bool              m_canvas_owner;                           // Canvas ownership flag
public:
//--- Returns the object coordinates, dimensions, and boundaries
   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();                                                     }

//--- Change the bounding rectangular (1) width, (2) height and (3) size
   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);                                                         }
   
//--- Set (1) X, (2) Y and (3) both coordinates of the bounding rectangle
   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) Set and (2) shift the bounding rectangle by the specified coordinates/offset size
   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);                                                        }
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   //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::Constructor                                         |
//+------------------------------------------------------------------+
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)
  {
//--- Get the adjusted chart ID and the distance in pixels along the vertical Y axis
   this.m_bound.SetName(user_name);
   this.m_bound.SetID(id);
   this.m_bound.SetXY(x,y);
   this.m_bound.Resize(w,h);
  }
//+------------------------------------------------------------------+
//| CBoundedObj::Save to file                                        |
//+------------------------------------------------------------------+
bool CBoundedObj::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CBaseObj::Save(file_handle))
      return false;
 
//--- Save the canvas ownership flag
   if(::FileWriteInteger(file_handle,this.m_canvas_owner,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the dimensions
   return this.m_bound.Save(file_handle);
  }
//+------------------------------------------------------------------+
//| CBoundedObj::Load from file                                      |
//+------------------------------------------------------------------+
bool CBoundedObj::Load(const int file_handle)
  {
//--- Load parent object data
   if(!CBaseObj::Load(file_handle))
      return false;
   
//--- Load the canvas ownership flag
   this.m_canvas_owner=::FileReadInteger(file_handle,INT_VALUE);
//--- Load dimensions
   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:

//+------------------------------------------------------------------+
//| Base class of graphical elements canvas                          |
//+------------------------------------------------------------------+
class CCanvasBase : public CBoundedObj
  {
private: 
   bool              m_chart_mouse_wheel_flag;                 // Flag for sending mouse wheel scroll messages
   bool              m_chart_mouse_move_flag;                  // Flag for sending mouse cursor movement messages
   bool              m_chart_object_create_flag;               // Flag for sending messages about the graphical object creation event
   bool              m_chart_mouse_scroll_flag;                // Flag for scrolling the chart with the left button and mouse wheel
   bool              m_chart_context_menu_flag;                // Flag of access to the context menu using the right click
   bool              m_chart_crosshair_tool_flag;              // Flag of access to the Crosshair tool using the middle click
   bool              m_flags_state;                            // State of the flags for scrolling the chart with the wheel, the context menu, and the crosshair 
   
//--- Set chart restrictions (wheel scrolling, context menu, and crosshair)
   void              SetFlags(const bool flag);
   
protected:
   CCanvas          *m_background;                             // Background canvas
   CCanvas          *m_foreground;                             // Foreground canvas

   CCanvasBase      *m_container;                              // Parent container object
   CColorElement     m_color_background;                       // Background color control object
   CColorElement     m_color_foreground;                       // Foreground color control object
   CColorElement     m_color_border;                           // Border color control object

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

//--- Return the graphical object coordinates, boundaries and dimensions
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;                                      }
   
//--- Change the graphical object (1) width, (2) height and (3) size
protected:
   virtual bool      ObjectResizeW(const int size);
   virtual bool      ObjectResizeH(const int size);
   bool              ObjectResize(const int w,const int h);
   
//--- Set the graphical object (1) X, (2) Y and (3) both coordinates
   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.:

//--- Set the pointer to the parent container object
   void              SetContainerObj(CCanvasBase *obj);
   
protected:
//--- Create background and foreground canvases
   bool              CreateCanvasObjects(void);
//--- Create 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) Set and (2) return the state
   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:

//--- Set (1) movability, (2) main object flag for the object and (3) resizability
//--- cropping along the container borders
   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::Constructor                                         |
//+------------------------------------------------------------------+
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)
  {
//--- Get the adjusted chart ID and the distance in pixels along the vertical Y axis
//--- between the upper frame of the indicator subwindow and the upper frame of the chart main window
   this.m_chart_id=this.CorrectChartID(chart_id);
   
//--- If failed to create canvases, leave
   if(!this.CreateCanvasObjects())
      return;
      
//--- If the graphical resource and graphical object are created
   if(this.Create(this.m_chart_id,this.m_wnd,object_name,x,y,w,h))
     {
      //--- Clear the background and foreground canvases and set the initial coordinate values,
      //--- names of graphic objects and properties of text drawn in the foreground
      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");
      
      //--- Remember permissions for the mouse and chart tools
      this.Init();
     }
  }

A method that creates canvases for the background and foreground:

//+------------------------------------------------------------------+
//| CCanvasBase::Create background and foreground canvases           |
//+------------------------------------------------------------------+
bool CCanvasBase::CreateCanvasObjects(void)
  {
//--- If both canvases have already been created, or the class does not manage canvases, return 'true'
   if((this.m_background!=NULL && this.m_foreground!=NULL) || !this.m_canvas_owner)
      return true;
//--- Create the background canvas
   this.m_background=new CCanvas();
   if(this.m_background==NULL)
     {
      ::PrintFormat("%s: Error! Failed to create background canvas",__FUNCTION__);
      return false;
     }
//--- Create the foreground canvas
   this.m_foreground=new CCanvas();
   if(this.m_foreground==NULL)
     {
      ::PrintFormat("%s: Error! Failed to create foreground canvas",__FUNCTION__);
      return false;
     }
//--- All is successful
   return true;
  }

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

//+------------------------------------------------------------------+
//| CCanvasBase::Destroy the object                                  |
//+------------------------------------------------------------------+
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::Class initialization                                |
//+------------------------------------------------------------------+
void CCanvasBase::Init(void)
  {
//--- Remember permissions for the mouse and chart tools
   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);
//--- Set permissions for the mouse and chart
   ::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);

//--- Initialize the object default colors
   this.InitColors();
//--- Initialize the millisecond timer
   ::EventSetMillisecondTimer(16);
   
//--- Canvas ownership flag
   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:

//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W                50          // Text label default width
#define  DEF_LABEL_H                16          // Text label default height
#define  DEF_BUTTON_W               60          // Default button width
#define  DEF_BUTTON_H               16          // Default button height
#define  DEF_TABLE_ROW_H            16          // Default table row height
#define  DEF_TABLE_HEADER_H         20          // Default table header height
#define  DEF_PANEL_W                80          // Default panel width
#define  DEF_PANEL_H                80          // Default panel height
#define  DEF_PANEL_MIN_W            60          // Minimum panel width
#define  DEF_PANEL_MIN_H            60          // Minimum panel height
#define  DEF_SCROLLBAR_TH           13          // Default scrollbar width
#define  DEF_THUMB_MIN_SIZE         8           // Minimum width of the scrollbar slider
#define  DEF_AUTOREPEAT_DELAY       500         // Delay before launching auto-repeat
#define  DEF_AUTOREPEAT_INTERVAL    100         // Auto-repeat frequency

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 of the linked list of graphical elements                   |
//+------------------------------------------------------------------+
class CListElm : public CList
  {
protected:
   ENUM_ELEMENT_TYPE m_element_type;   // Created object type in CreateElement()
public:
//--- Set the element type
   void              SetElementType(const ENUM_ELEMENT_TYPE type) { this.m_element_type=type;   }
   
//--- Virtual method (1) for loading a list from a file, (2) for creating a list element
   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:

//+------------------------------------------------------------------+
//| List element creation method                                     |
//+------------------------------------------------------------------+
CObject *CListElm::CreateElement(void)
  {
//--- Create a new object depending on the object type in m_element_type 
   switch(this.m_element_type)
     {
      case ELEMENT_TYPE_BASE                 :  return new CBaseObj();           // Basic object of graphical elements
      case ELEMENT_TYPE_COLOR                :  return new CColor();             // Color object
      case ELEMENT_TYPE_COLORS_ELEMENT       :  return new CColorElement();      // Color object of the graphical object element
      case ELEMENT_TYPE_RECTANGLE_AREA       :  return new CBound();             // Rectangular area of the element
      case ELEMENT_TYPE_IMAGE_PAINTER        :  return new CImagePainter();      // Object for drawing images
      case ELEMENT_TYPE_CANVAS_BASE          :  return new CCanvasBase();        // Basic object of graphical elements
      case ELEMENT_TYPE_ELEMENT_BASE         :  return new CElementBase();       // Basic object of graphical elements
      case ELEMENT_TYPE_HINT                 :  return new CVisualHint();        // Hint
      case ELEMENT_TYPE_LABEL                :  return new CLabel();             // Text label
      case ELEMENT_TYPE_BUTTON               :  return new CButton();            // Simple button
      case ELEMENT_TYPE_BUTTON_TRIGGERED     :  return new CButtonTriggered();   // Toggle button
      case ELEMENT_TYPE_BUTTON_ARROW_UP      :  return new CButtonArrowUp();     // Up arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN    :  return new CButtonArrowDown();   // Down arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT    :  return new CButtonArrowLeft();   // Left arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT   :  return new CButtonArrowRight();  // Right arrow button
      case ELEMENT_TYPE_CHECKBOX             :  return new CCheckBox();          // CheckBox control
      case ELEMENT_TYPE_RADIOBUTTON          :  return new CRadioButton();       // RadioButton control
      case ELEMENT_TYPE_TABLE_CELL           :  return new CTableCellView();     // Table cell (View)
      case ELEMENT_TYPE_TABLE_ROW            :  return new CTableRowView();      // Table row (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  return new CColumnCaptionView(); // Table column header (View)
      case ELEMENT_TYPE_TABLE_HEADER         :  return new CTableHeaderView();   // Table header (View)
      case ELEMENT_TYPE_TABLE                :  return new CTableView();         // Table (View)
      case ELEMENT_TYPE_PANEL                :  return new CPanel();             // Panel control
      case ELEMENT_TYPE_GROUPBOX             :  return new CGroupBox();          // GroupBox control
      case ELEMENT_TYPE_CONTAINER            :  return new CContainer();         // GroupBox control
      default                                :  return NULL;
     }
  }

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

public:
//--- Return itself
   CElementBase     *GetObject(void)                           { return &this;                     }
//--- Return the pointer to (1) the drawing class and (2) the list of hints
   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:

//--- Set the text (1) X and (2) Y coordinate
   void              SetTextShiftH(const int x)                { this.ClearText(); this.m_text_x=x;            }
   void              SetTextShiftV(const int y)                { this.ClearText(); this.m_text_y=y;            }
   
//--- Display the text
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

In the CPanel panel class, refine the resizing methods:

//+------------------------------------------------------------------+
//| CPanel::Change the object width                                  |
//+------------------------------------------------------------------+
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);
     }
//--- Get the pointer to the base element and, if it exists, its type is container,
//--- check the ratio of the current element dimensions to the container dimensions
//--- to display scrollbars in the container if necessary
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);
      
//--- In the loop through the attached elements, trim each element along the container boundaries
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CPanel::Change the object height                                 |
//+------------------------------------------------------------------+
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);
     }
//--- Get the pointer to the base element and, if it exists, its type is container,
//--- check the ratio of the current element dimensions to the container dimensions
//--- to display scrollbars in the container if necessary
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);
      
//--- In the loop through the attached elements, trim each element along the container boundaries
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CPanel::Change the object size                                   |
//+------------------------------------------------------------------+
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);
     }
//--- Get the pointer to the base element and, if it exists, its type is container,
//--- check the ratio of the current element dimensions to the container dimensions
//--- to display scrollbars in the container if necessary
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);
      
//--- In the loop through the attached elements, trim each element along the container boundaries
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- All is successful
   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::Draw the appearance                                      |
//+------------------------------------------------------------------+
void CPanel::Draw(const bool chart_redraw)
  {
//--- Fill the object with background color
   this.Fill(this.BackColor(),false);
   
//--- Clear the drawing area
   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);
//--- Set the color for the dark and light lines and draw the panel frame
   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);
   
//--- Update the background canvas without redrawing the chart
   this.m_background.Update(false);
   
//--- Draw the list elements
   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 specified, update the chart
   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::Create and add a new element to the list                 |
//+------------------------------------------------------------------+
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)
  {
//--- Create a graphical object name
   int elm_total=this.m_list_elm.Total();
   string obj_name=this.NameFG()+"_"+ElementShortName(type)+(string)elm_total;
//--- Calculate the coordinates
   int x=this.X()+dx;
   int y=this.Y()+dy;
//--- Create a new object depending on the object type
   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;   // Text label
      case ELEMENT_TYPE_BUTTON               :  element = new CButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);            break;   // Simple button
      case ELEMENT_TYPE_BUTTON_TRIGGERED     :  element = new CButtonTriggered(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Toggle button
      case ELEMENT_TYPE_BUTTON_ARROW_UP      :  element = new CButtonArrowUp(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);     break;   // Up arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN    :  element = new CButtonArrowDown(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Down arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT    :  element = new CButtonArrowLeft(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Left arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT   :  element = new CButtonArrowRight(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);  break;   // Right arrow button
      case ELEMENT_TYPE_CHECKBOX             :  element = new CCheckBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // CheckBox control
      case ELEMENT_TYPE_RADIOBUTTON          :  element = new CRadioButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);       break;   // RadioButton control
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H    :  element = new CScrollBarThumbH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Horizontal 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;   // Vertical ScrollBar
      case ELEMENT_TYPE_SCROLLBAR_H          :  element = new CScrollBarH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Horizontal ScrollBar control
      case ELEMENT_TYPE_SCROLLBAR_V          :  element = new CScrollBarV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Vertical ScrollBar control
      case ELEMENT_TYPE_TABLE_ROW            :  element = new CTableRowView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);      break;   // Table row visual representation object
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  element = new CColumnCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break;   // Table column header visual representation object
      case ELEMENT_TYPE_TABLE_HEADER         :  element = new CTableHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Table header visual representation object
      case ELEMENT_TYPE_TABLE                :  element = new CTableView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Table visual representation object
      case ELEMENT_TYPE_PANEL                :  element = new CPanel(obj_name,"",this.m_chart_id,this.m_wnd,x,y,w,h);               break;   // Panel control
      case ELEMENT_TYPE_GROUPBOX             :  element = new CGroupBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // GroupBox control
      case ELEMENT_TYPE_CONTAINER            :  element = new CContainer(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Container control
      default                                :  element = NULL;
     }

//--- If the new element is not created, report this and return NULL
   if(element==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create graphic element %s",__FUNCTION__,ElementDescription(type));
      return NULL;
     }
//--- Set the element ID, name, container, and z-order
   element.SetID(elm_total);
   element.SetName(user_name);
   element.SetContainerObj(&this);
   element.ObjectSetZOrder(this.ObjectZOrder()+1);
   
//--- If the created element is not added to the list, report this, remove the created element and return 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;
     }
//--- Get the parent element the children ones are attached to
   CElementBase *elm=this.GetContainer();
//--- If the parent element is of Container type, then it has scrollbars
   if(elm!=NULL && elm.Type()==ELEMENT_TYPE_CONTAINER)
     {
      //--- Convert CElementBase to CContainer
      CContainer *container_obj=elm;
      //--- If the horizontal scrollbar is visible,
      if(container_obj.ScrollBarHorzIsVisible())
        {
         //--- get the pointer to the horizontal scrollbar and move it to the front
         CScrollBarH *sbh=container_obj.GetScrollBarH();
         if(sbh!=NULL)
            sbh.BringToTop(false);
        }
      //--- If the vertical scrollbar is visible,
      if(container_obj.ScrollBarVertIsVisible())
        {
         //--- get the pointer to the vertical scrollbar and move it to the front
         CScrollBarV *sbv=container_obj.GetScrollBarV();
         if(sbv!=NULL)
            sbv.BringToTop(false);
        }
     }
//--- Return the pointer to the created and attached element
   return element;
  }

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

//+------------------------------------------------------------------+
//| CPanel::Bring an object to the foreground                        |
//+------------------------------------------------------------------+
void CPanel::BringToTop(const bool chart_redraw)
  {
//--- Bring the panel to the foreground
   CCanvasBase::BringToTop(false);
//--- Bring attached objects to the foreground
   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 specified, redraw the chart
   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:

//+------------------------------------------------------------------+
//| Horizontal scrollbar class                                       |
//+------------------------------------------------------------------+
class CScrollBarH : public CPanel
  {
protected:
   CButtonArrowLeft *m_butt_left;                              // Left arrow button 
   CButtonArrowRight*m_butt_right;                             // Right arrow button
   CScrollBarThumbH *m_thumb;                                  // Scrollbar slider

public:
//--- Return the pointer to the (1) left, (2) right button and (3) slider
   CButtonArrowLeft *GetButtonLeft(void)                       { return this.m_butt_left;                                              }
   CButtonArrowRight*GetButtonRight(void)                      { return this.m_butt_right;                                             }
   CScrollBarThumbH *GetThumb(void)                            { return this.m_thumb;                                                  }

//--- (1) Sets and (2) return the chart update flag
   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);  }

//--- Return (1) the track length (2) start and (3) the slider position
   int               TrackLength(void)    const;
   int               TrackBegin(void)     const;
   int               ThumbPosition(void)  const;
   
//--- Set the slider position
   bool              SetThumbPosition(const int pos)     const { return(this.m_thumb!=NULL ? this.m_thumb.MoveX(pos) : false);         }
//--- Change the slider size
   bool              SetThumbSize(const uint size)       const { return(this.m_thumb!=NULL ? this.m_thumb.ResizeW(size) : false);      }

//--- Change the object width
   virtual bool      ResizeW(const int size);
   
//--- Set the flag of visibility in the container
   virtual void      SetVisibleInContainer(const bool flag);
   
//--- Set the clipping flag to the container borders
   virtual void      SetTrimmered(const bool flag);

//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Object type
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_SCROLLBAR_H);                                     }
   
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);
   
//--- Wheel scroll handler (Wheel)
   virtual void      OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam);

//--- Constructors/destructor
                     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::Set the clipping flag to the container borders      |
//+------------------------------------------------------------------+
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::Initialization                                      |
//+------------------------------------------------------------------+
void CScrollBarH::Init(void)
  {
//--- Initialize a parent class
   CPanel::Init();
//--- background - opaque
   this.SetAlphaBG(255);
//--- Frame width and text
   this.SetBorderWidth(0);
   this.SetText("");

//--- Create scroll buttons
   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;
     }
//--- Customize the colors and appearance of the left arrow button
   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();

//--- Customize the colors and appearance of the right arrow button
   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();
 
//--- Create a slider
   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;
     }
//--- Set the slider colors and set its movability flag
   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);

//--- Prohibit independent chart redrawing
   this.m_thumb.SetChartRedrawFlag(false);
   
//--- Initially not displayed in the container and not clipped to its borders
   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:

//+------------------------------------------------------------------+
//| Container class                                                  |
//+------------------------------------------------------------------+
class CContainer : public CPanel
  {
private:
   bool              m_visible_scrollbar_h;                    // Visibility flag for the horizontal scrollbar
   bool              m_visible_scrollbar_v;                    // Vertical scrollbar visibility flag
   int               m_init_border_size_top;                   // Initial border size from the top
   int               m_init_border_size_bottom;                // Initial border size from the bottom
   int               m_init_border_size_left;                  // Initial border size from the left
   int               m_init_border_size_right;                 // Initial border size from the right
   
//--- Return the type of the element that sent the event
   ENUM_ELEMENT_TYPE GetEventElementType(const string name);
   
protected:
   CScrollBarH      *m_scrollbar_h;                            // Pointer to the horizontal scrollbar
   CScrollBarV      *m_scrollbar_v;                            // Pointer to the vertical scrollbar
 
//--- Handler for dragging element edges and corners
   virtual void      ResizeActionDragHandler(const int x, const int y);
   
public:
//--- Check the dimensions of the element to display scrollbars
   void              CheckElementSizes(CElementBase *element);
protected:
//--- Calculate and return the size (1) of the slider, (2) the full size, (3) the working size of the horizontal scrollbar track
   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:

//+------------------------------------------------------------------+
//| Table cell visual representation class                           |
//+------------------------------------------------------------------+
class CTableCellView : public CBoundedObj
  {
protected:
   CTableCell       *m_table_cell_model;                       // Pointer to the cell model
   CImagePainter    *m_painter;                                // Pointer to the drawing object
   CTableRowView    *m_element_base;                           // Pointer to the base element (table row)
   CCanvas          *m_background;                             // Pointer to the background canvas
   CCanvas          *m_foreground;                             // Pointer to the foreground canvas
   int               m_index;                                  // Index in the cell list
   ENUM_ANCHOR_POINT m_text_anchor;                            // Text anchor point (alignment in cell)
   int               m_text_x;                                 // Text X coordinate (offset relative to the left border of the object area)
   int               m_text_y;                                 // Y text coordinate (offset relative to the top border of the object area)
   ushort            m_text[];                                 // Text
   
//--- Return the offsets of the initial drawing coordinates on the canvas relative to the canvas and the coordinates of the base element
   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());  }
   
//--- Return the adjusted coordinate of a point on the canvas, taking into account the offset of the canvas relative to the base element
   int               AdjX(const int x)                            const { return(x-this.CanvasOffsetX());            }
   int               AdjY(const int y)                            const { return(y-this.CanvasOffsetY());            }

//--- Return the X and Y coordinates of the text based on the anchor point
   bool              GetTextCoordsByAnchor(int &x, int&y);
   
public:
//--- (1) Set and (2) return the cell text
   void              SetText(const string text)                         { ::StringToShortArray(text,this.m_text);    }
   string            Text(void)                                   const { return ::ShortArrayToString(this.m_text);  }
   
//--- Set the ID
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Set and (2) return the cell index
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }

//--- (1) Set and (2) return the text offset along the X axis
   void              SetTextShiftX(const int shift)                     { this.m_text_x=shift;                       }
   int               TextShiftX(void)                             const { return this.m_text_x;                      }
   
//--- (1) Set and (2) return the text offset along the Y axis
   void              SetTextShiftY(const int shift)                     { this.m_text_y=shift;                       }
   int               TextShiftY(void)                             const { return this.m_text_y;                      }
   
//--- (1) Set and (2) return the text anchor point
   void              SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw);
   int               TextAnchor(void)                             const { return this.m_text_anchor;                 }
   
//--- Sets the anchor point and text offsets
   void              SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw);

//--- Assign the base element (table row)
   void              RowAssign(CTableRowView *base_element);
   
//--- (1) Assign and (2) return the cell model
   bool              TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h);
   CTableCell       *GetTableCellModel(void)                            { return this.m_table_cell_model;            }

//--- Print the assigned cell model in the journal
   void              TableCellModelPrint(void);
   
//--- (1) Fill the object with the background color, (2) update the object to reflect the changes and (3) draw the appearance
   virtual void      Clear(const bool chart_redraw);
   virtual void      Update(const bool chart_redraw);
   virtual void      Draw(const bool chart_redraw);
   
//--- Display the text
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   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);           }
  
//--- Initialize a class object
   void              Init(const string text);
   
//--- Return the object description
   virtual string    Description(void);
   
//--- Constructors/destructor
                     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::Default constructor. Builds an object in the main      |
//| window of the current chart at coordinates 0,0 with default dimensions |
//+------------------------------------------------------------------------+
CTableCellView::CTableCellView(void) : CBoundedObj("TableCell",-1,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1),m_text_anchor(ANCHOR_LEFT)
  {
//--- Initialization
   this.Init("");
   this.SetID(-1);
   this.SetName("TableCell");
  }
//+-------------------------------------------------------------------------------+
//| CTableCellView::Parametric constructor. Builds an object                      |
//| in the specified window of the specified chart with the specified text,       |
//| coordinates and dimensions                                                    |
//+-------------------------------------------------------------------------------+
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)
  {
//--- Initialization
   this.Init(text);
   this.SetID(id);
   this.SetName(user_name);
  }

Class Object Initialization Method:

//+------------------------------------------------------------------+
//| CTableCellView::Initialization                                   |
//+------------------------------------------------------------------+
void CTableCellView::Init(const string text)
  {
//--- The class does not manage canvases
   this.m_canvas_owner=false;
//--- Cell text
   this.SetText(text);
//--- Default text offsets
   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::Return the object description                    |
//+------------------------------------------------------------------+
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::Assigns the row, background and foreground canvases |
//+---------------------------------------------------------------------+
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::Assigns a cell model                             |
//+------------------------------------------------------------------+
bool CTableCellView::TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h)
  {
//--- If an invalid cell model object is passed, report this and return 'false'
   if(cell_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- If the base element (table row) is not assigned, report this and return 'false'
   if(this.m_element_base==NULL)
     {
      ::PrintFormat("%s: Error. Base element not assigned. Please use RowAssign() method first",__FUNCTION__);
      return false;
     }
//--- Save the cell model
   this.m_table_cell_model=cell_model;
//--- Set the coordinates and dimensions of the cell visual representation
   this.BoundSetXY(dx,dy);
   this.BoundResize(w,h);
//--- Set the dimensions of the drawing area of the cell visual representation
   this.m_painter.SetBound(dx,dy,w,h);
//--- All is successful
   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::Return the X and Y coordinates of the text       |
//| depending on the anchor point                                    |
//+------------------------------------------------------------------+
bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y)
  {
//--- Get the text size in the cell
   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;
//--- Depending on the text anchor point in the cell,
//--- calculate its initial coordinates (upper left corner)
   switch(this.m_text_anchor)
     {
      //--- Anchor point left centered
      case ANCHOR_LEFT :
        x=0;
        y=(this.Height()-text_h)/2;
        break;
      //--- Anchor point in the lower left corner
      case ANCHOR_LEFT_LOWER :
        x=0;
        y=this.Height()-text_h;
        break;
      //--- Anchor point at the bottom center
      case ANCHOR_LOWER :
        x=(this.Width()-text_w)/2;
        y=this.Height()-text_h;
        break;
      //--- Anchor point in the lower right corner
      case ANCHOR_RIGHT_LOWER :
        x=this.Width()-text_w;
        y=this.Height()-text_h;
        break;
      //--- Anchor point is right centered
      case ANCHOR_RIGHT :
        x=this.Width()-text_w;
        y=(this.Height()-text_h)/2;
        break;
      //--- Anchor point in the upper right corner
      case ANCHOR_RIGHT_UPPER :
        x=this.Width()-text_w;
        y=0;
        break;
      //--- Anchor point at top center
      case ANCHOR_UPPER :
        x=(this.Width()-text_w)/2;
        y=0;
        break;
      //--- Anchor point is strictly in the center of the object
      case ANCHOR_CENTER :
        x=(this.Width()-text_w)/2;
        y=(this.Height()-text_h)/2;
        break;
      //--- Anchor point in the upper left corner
      //---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::Set the text anchor point                        |
//+------------------------------------------------------------------+
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::Set the anchor point and text offsets            |
//+------------------------------------------------------------------+
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::Fill an object with color                        |
//+------------------------------------------------------------------+
void CTableCellView::Clear(const bool chart_redraw)
  {
//--- Set the correct coordinates of the cell corners
   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());
//--- Erase the background and foreground inside the rectangular area of the cell location
   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::Update the object to reflect changes             |
//+------------------------------------------------------------------+
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::Draw the appearance                              |
//+------------------------------------------------------------------+
void CTableCellView::Draw(const bool chart_redraw)
  {
//--- Get the text coordinates depending on the anchor point
   int text_x=0, text_y=0;
   if(!this.GetTextCoordsByAnchor(text_x,text_y))
      return;
//--- Correct the text coordinates
   int x=this.AdjX(this.X()+text_x);
   int y=this.AdjY(this.Y()+text_y);
   
//--- Set the coordinates of the dividing line
   int x1=this.AdjX(this.X());
   int x2=this.AdjX(this.X());
   int y1=this.AdjY(this.Y());
   int y2=this.AdjY(this.Bottom());
   
//--- Display text on the foreground canvas without updating the chart
   this.DrawText(x+this.m_text_x,y+this.m_text_y,this.Text(),false);
   
//--- If this is not the rightmost cell, draw a vertical dividing line near the cell on the right
   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()));
     }
//--- Update the background canvas with the specified chart redraw flag
   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::Display the text                                 |
//+------------------------------------------------------------------+
void CTableCellView::DrawText(const int dx,const int dy,const string text,const bool chart_redraw)
  {
//--- Check the base element
   if(this.m_element_base==NULL)
      return;
      
//--- Clear the cell and set the text
   this.Clear(false);
   this.SetText(text);
   
//--- Display the specified text on the foreground canvas
   this.m_foreground.TextOut(dx,dy,this.Text(),::ColorToARGB(this.m_element_base.ForeColor(),this.m_element_base.AlphaFG()));
   
//--- If the text extends beyond the right border of the cell area
   if(this.Right()-dx<this.m_foreground.TextWidth(text))
     {
      //--- Get the dimensions of the "..." text 
      int w=0,h=0;
      this.m_foreground.TextSize("... ",w,h);
      if(w>0 && h>0)
        {
         //--- Erase the text at the right edge of the object to the size of the "..." text and replace the end of the label text with "..."
         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()));
        }
     }
//--- Update the foreground canvas with the specified chart redraw flag
   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::Print the assigned row model in the journal      |
//+------------------------------------------------------------------+
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::Save to file                                     |
//+------------------------------------------------------------------+
bool CTableCellView::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CBaseObj::Save(file_handle))
      return false;
  
//--- Save the cell number
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the text anchor point
   if(::FileWriteInteger(file_handle,this.m_text_anchor,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the text X coordinate
   if(::FileWriteInteger(file_handle,this.m_text_x,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the text Y coordinate
   if(::FileWriteInteger(file_handle,this.m_text_y,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the text
   if(::FileWriteArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
      
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CTableCellView::Load from file                                   |
//+------------------------------------------------------------------+
bool CTableCellView::Load(const int file_handle)
  {
//--- Load parent object data
   if(!CBaseObj::Load(file_handle))
      return false;
      
//--- Load the cell index
   this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the text anchor point
   this.m_text_anchor=(ENUM_ANCHOR_POINT)::FileReadInteger(file_handle,INT_VALUE);
//--- Load the text X coordinate
   this.m_text_x=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the text Y coordinate
   this.m_text_y=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the text
   if(::FileReadArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
   
//--- All is successful
   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:

//+------------------------------------------------------------------+
//| Table row visual representation class                            |
//+------------------------------------------------------------------+
class CTableRowView : public CPanel
  {
protected:
   CTableCellView    m_temp_cell;                                    // Temporary cell object for searching
   CTableRow        *m_table_row_model;                              // Pointer to the row model
   CListElm          m_list_cells;                                   // Cell list
   int               m_index;                                        // Index in the list of rows
//--- Create and add a new cell representation object to the list
   CTableCellView   *InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h);
   
public:
//--- Reutrn the (1) list and (2) the number of cells
   CListElm         *GetListCells(void)                                 { return &this.m_list_cells;        }
   int               CellsTotal(void)                             const { return this.m_list_cells.Total(); }
   
//--- Set the ID
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;        }
//--- (1) Set and (2) return the row index
   void              SetIndex(const int index)                          { this.SetID(index);                }
   int               Index(void)                                  const { return this.m_index;              }

//--- (1) Set and (2) return the row model
   bool              TableRowModelAssign(CTableRow *row_model);
   CTableRow        *GetTableRowModel(void)                             { return this.m_table_row_model;    }

//--- Print the assigned row model in the journal
   void              TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   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);   }
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);

//--- Constructors/destructor
                     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::Default constructor. Builds an object in the main     |
//| window of the current chart at coordinates 0,0 with default dimension|
//+----------------------------------------------------------------------+
CTableRowView::CTableRowView(void) : CPanel("TableRow","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1)
  {
//--- Initialization
   this.Init();
  }
//+----------------------------------------------------------------------------------+
//| CTableRowView::Parametric constructor. Builds an object in                       |
//| in the specified window of the specified chart with the specified text,          |
//| coordinates and dimensions                                                       |
//+----------------------------------------------------------------------------------+
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)
  {
//--- Initialization
   this.Init();
  }

Object Initialization:

//+------------------------------------------------------------------+
//| CTableRowView::Initialization                                    |
//+------------------------------------------------------------------+
void CTableRowView::Init(void)
  {
//--- Initialize the parent object
   CPanel::Init();
//--- background - opaque
   this.SetAlphaBG(255);
//--- Border width
   this.SetBorderWidth(1);
  }

Default Object Color Initialization Method:

//+------------------------------------------------------------------+
//| CTableRowView::Initialize the object default colors              |
//+------------------------------------------------------------------+
void CTableRowView::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and make it the current background color
   this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and make it the current text color
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and make it the current border color
   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();
   
//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrSilver);
   this.InitForeColorBlocked(clrSilver);
  }

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

//+------------------------------------------------------------------+
//| CTableRowView::Create and add a new                              |
//| cell representation object to the list                           |
//+------------------------------------------------------------------+
CTableCellView *CTableRowView::InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h)
  {
//--- Check whether the list contains an object with the specified ID and, if it does, report this and return NULL
   this.m_temp_cell.SetIndex(index);
//--- Set the sorting flag for the list by ID
   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;
     }
//--- Create a name and ID for the cell object
   string name="TableCellView"+(string)this.Index()+"x"+(string)index;
   int id=this.m_list_cells.Total();
//--- Create a new TableCellView object; in case of a failure, report it and return 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;
     }
//--- If failed to add the new object to the list, report this, remove the object and return 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;
     }
//--- Assign the base element and return the pointer to the object
   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::Set the row model                                 |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelAssign(CTableRow *row_model)
  {
//--- If an empty object is passed, report this and return 'false'
   if(row_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- If the passed row model does not contain a single cell, report this and return 'false'
   int total=(int)row_model.CellsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Save the pointer to the passed row model and calculate the cell width
   this.m_table_row_model=row_model;
   int cell_w=(int)::round((double)this.Width()/(double)total);
   
//--- In the loop by the number of cells in the row model
   for(int i=0;i<total;i++)
     {
      //--- get the model of the next cell,
      CTableCell *cell_model=this.m_table_row_model.GetCell(i);
      if(cell_model==NULL)
         return false;
      //--- calculate the coordinate and create a name for the cell area
      int x=cell_w*i;
      string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i;
      //--- Create a new cell area
      CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height());
      if(cell_bound==NULL)
         return false;
      //--- Create a new cell visual representation object
      CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height());
      if(cell_view==NULL)
         return false;
      //--- Assign the corresponding visual representation object of the cell to the current cell area
      cell_bound.AssignObject(cell_view);
     }
//--- All is successful
   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::Draw the appearance                               |
//+------------------------------------------------------------------+
void CTableRowView::Draw(const bool chart_redraw)
  {
//--- Fill the object with the background color, draw the row line and update the background canvas
   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()));
  
//--- Draw the row cells
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next cell
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Get the attached cell object from the cell area
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      //--- Draw a visual representation of the cell
      if(cell_view!=NULL)
         cell_view.Draw(false);
     }
//--- Update the background and foreground canvases with the specified chart redraw flag
   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::Print the assigned row model in the journal       |
//+------------------------------------------------------------------+
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::Save to file                                      |
//+------------------------------------------------------------------+
bool CTableRowView::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CPanel::Save(file_handle))
      return false;

//--- Save the list of cells
   if(!this.m_list_cells.Save(file_handle))
      return false;
//--- Save the row index
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
   
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CTableRowView::Load from file                                    |
//+------------------------------------------------------------------+
bool CTableRowView::Load(const int file_handle)
  {
//--- Load parent object data
   if(!CPanel::Load(file_handle))
      return false;
     
//--- Load the list of cells
   if(!this.m_list_cells.Load(file_handle))
      return false;
//--- Load the row index
   this.m_id=this.m_index=(uchar)::FileReadInteger(file_handle,INT_VALUE);
   
//--- All is successful
   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 for visual representation of table column header           |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CButton
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Pointer to the column header model
   int               m_index;                                        // Index in the column list
   
public:
//--- Set the ID
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Set and (2) return the cell index
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }
   
//--- (1) Assign and (2) return the column header model
   bool              ColumnCaptionModelAssign(CColumnCaption *caption_model);
   CColumnCaption   *ColumnCaptionModel(void)                           { return this.m_column_caption_model;        }

//--- Print the assigned column header model in the journal
   void              ColumnCaptionModelPrint(void);
 
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   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); }
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Return the object description
   virtual string    Description(void);
   
//--- Constructors/destructor
                     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::Default constructor. Builds an object        |
//| in the main window of the current chart at coordinates 0,0       |
//| with default dimensions                                          |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(void) : CButton("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0)
  {
//--- Initialization
   this.Init("Caption");
   this.SetID(0);
   this.SetName("ColumnCaption");
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Parametric constructor.                      |
//| Plot an object in the specified window of the specified plot with|
//| the specified text, coordinates and dimensions                   |
//+------------------------------------------------------------------+
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)
  {
//--- Initialization
   this.Init(text);
   this.SetID(0);
  }

Object Initialization Method:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Initialization                               |
//+------------------------------------------------------------------+
void CColumnCaptionView::Init(const string text)
  {
//--- Default text offsets
   this.m_text_x=4;
   this.m_text_y=2;
//--- Set the colors of different states
   this.InitColors();
  }

Default Object Color Initialization Method:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Initialize the object default colors         |
//+------------------------------------------------------------------+
void CColumnCaptionView::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and make it the current background color
   this.InitBackColors(clrWhiteSmoke,this.GetBackColorControl().NewColor(clrWhiteSmoke,-6,-6,-6),clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and make it the current text color
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and make it the current border color
   this.InitBorderColors(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.InitBorderColorsAct(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.BorderColorToDefault();
   
//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrNULL);
   this.InitForeColorBlocked(clrSilver);
  }

A Method That Draws the Column Header View:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Draw the appearance                          |
//+------------------------------------------------------------------+
void CColumnCaptionView::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the light vertical line on the left and the dark one on the right
   this.Fill(this.BackColor(),false);
   color clr_dark =this.BorderColor();                                                       // "Dark color"
   color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100);   // "Light color"
   this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG()));                          // Line on the left
   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())); // Line on the right
//--- Update the background canvas
   this.m_background.Update(false);
   
//--- Display the header text
   CLabel::Draw(false);
      
//--- If specified, update the chart
   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::Return the object description                |
//+------------------------------------------------------------------+
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::Assign the column caption model              |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ColumnCaptionModelAssign(CColumnCaption *caption_model)
  {
//--- If an invalid column header model object is passed, report this and return 'false'
   if(caption_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Save the column header model
   this.m_column_caption_model=caption_model;
//--- Set the dimensions of the drawing area of the column header visual representation
   this.m_painter.SetBound(0,0,this.Width(),this.Height());
//--- All is successful
   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::Print in the journal                         |
//| the assigned column header model                                 |
//+------------------------------------------------------------------+
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::Save to file                                 |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CButton::Save(file_handle))
      return false;
  
//--- Save the header index
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Load from file                               |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Load(const int file_handle)
  {
//--- Load parent object data
   if(!CButton::Load(file_handle))
      return false;
      
//--- Load the header index
   this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE);
   
//--- All is successful
   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 for visual representation of table header                  |
//+------------------------------------------------------------------+
class CTableHeaderView : public CPanel
  {
protected:
   CColumnCaptionView m_temp_caption;                                // Temporary column header object for searching
   CTableHeader     *m_table_header_model;                           // Pointer to the table header model

//--- Create and add a new column header representation object to the list
   CColumnCaptionView *InsertNewColumnCaptionView(const string text, const int x, const int y, const int w, const int h);
   
public:
//--- (1) Set and (2) return the table header model
   bool              TableHeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetTableHeaderModel(void)                          { return this.m_table_header_model;    }

//--- Print the assigned table header model in the journal
   void              TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   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);   }
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);

//--- Constructors/destructor
                     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::Default constructor. Builds an object in       |
//| in the main window of the current chart at coordinates 0,0       |
//| with default dimensions                                          |
//+------------------------------------------------------------------+
CTableHeaderView::CTableHeaderView(void) : CPanel("TableHeader","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H)
  {
//--- Initialization
   this.Init();
  }
//+----------------------------------------------------------------------------------+
//| CTableHeaderView::Parametric constructor. Builds an object in                    |
//| in the specified window of the specified chart with the specified text,          |
//| coordinates and dimensions                                                       |
//+----------------------------------------------------------------------------------+
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)
  {
//--- Initialization
   this.Init();
  }

Object Initialization Method:

//+------------------------------------------------------------------+
//| CTableHeaderView::Initialization                                 |
//+------------------------------------------------------------------+
void CTableHeaderView::Init(void)
  {
//--- Initialize the parent object
   CPanel::Init();
//--- Background color - opaque
   this.SetAlphaBG(255);
//--- Border width
   this.SetBorderWidth(1);
  }

Default Object Color Initialization Method:

//+------------------------------------------------------------------+
//| CTableHeaderView::Initialize the object default colors           |
//+------------------------------------------------------------------+
void CTableHeaderView::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and make it the current background color
   this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and make it the current text color
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and make it the current border color
   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();
   
//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrSilver);
   this.InitForeColorBlocked(clrSilver);
  }

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

//+------------------------------------------------------------------+
//| CTableHeaderView::Create and add to the list                     |
//| new column header representation object                          |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::InsertNewColumnCaptionView(const string text,const int x,const int y,const int w,const int h)
  {
//--- Create an object name and return the result of creating a new column header
   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::Set the header model                           |
//+------------------------------------------------------------------+
bool CTableHeaderView::TableHeaderModelAssign(CTableHeader *header_model)
  {
//--- If an empty object is passed, report this and return 'false'
   if(header_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- If the passed header model does not contain any column headers, report this and return 'false'
   int total=(int)header_model.ColumnsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Header model does not contain any columns",__FUNCTION__);
      return false;
     }
//--- Store the pointer to the passed table header model and calculate the width of each column header
   this.m_table_header_model=header_model;
   int caption_w=(int)::round((double)this.Width()/(double)total);
   
//--- In the loop by the number of column headers in the table header model
   for(int i=0;i<total;i++)
     {
      //--- get the model of the next column header,
      CColumnCaption *caption_model=this.m_table_header_model.GetColumnCaption(i);
      if(caption_model==NULL)
         return false;
      //--- calculate the coordinate and create a name for the column header area
      int x=caption_w*i;
      string name="CaptionBound"+(string)i;
      //--- Create a new column header area
      CBound *caption_bound=this.InsertNewBound(name,x,0,caption_w,this.Height());
      if(caption_bound==NULL)
         return false;
      //--- Create a new column header visual representation object
      CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height());
      if(caption_view==NULL)
         return false;
      //--- Assign the corresponding visual representation object of the column header to the current column header area
      caption_bound.AssignObject(caption_view);
     }
//--- All is successful
   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::Draw the appearance                            |
//+------------------------------------------------------------------+
void CTableHeaderView::Draw(const bool chart_redraw)
  {
//--- Fill the object with the background color, draw the row line and update the background canvas
   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);
   
//--- Draw column headers
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next column header
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Get the attached column header object from the column header area
      CColumnCaptionView *caption_view=cell_bound.GetAssignedObj();
      //--- Draw a visual representation of the column header
      if(caption_view!=NULL)
         caption_view.Draw(false);
     }
//--- If specified, update the chart
   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::Print in the journal                           |
//| the assigned table header model                                  |
//+------------------------------------------------------------------+
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:

//+------------------------------------------------------------------+
//| Table visual representation class                                |
//+------------------------------------------------------------------+
class CTableView : public CPanel
  {
protected:
//--- Obtained table data
   CTable           *m_table_obj;                  // Pointer to table object (includes table and header models)
   CTableModel      *m_table_model;                // Pointer to the table model (obtained from CTable)
   CTableHeader     *m_header_model;               // Pointer to the table header model (obtained from CTable)
   
//--- View component data
   CTableHeaderView *m_header_view;                // Pointer to the table header (View)
   CPanel           *m_table_area;                 // Panel for placing table rows
   CContainer       *m_table_area_container;       // Container for placing a panel with table rows
   
//--- (1) Set and (2) return the table model
   bool              TableModelAssign(CTableModel *table_model);
   CTableModel      *GetTableModel(void)                                { return this.m_table_model;           }
   
//--- (1) Set and (2) return the table header model
   bool              HeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetHeaderModel(void)                               { return this.m_header_model;          }

//--- Create a (1) header and (2) table object from the model
   bool              CreateHeader(void);
   bool              CreateTable(void);
   
public:
//--- (1) Set and (2) return the table object
   bool              TableObjectAssign(CTable *table_obj);
   CTable           *GetTableObj(void)                                  { return this.m_table_obj;             }

//--- Print the assigned (1) table, (2) table header and (3) table object model in the journal
   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);

//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   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);          }
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);

//--- Constructors/destructor
                     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::Default constructor.                                 |
//| Builds an element in the main window of the current chart        |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
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)
  {
//--- Initialization
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableView::Parametric constructor.                              |
//| Builds an element in the specified window of the specified chart |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
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)
  {
//--- Initialization
   this.Init();
  }

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

//+------------------------------------------------------------------+
//| CTableView::Initialization                                       |
//+------------------------------------------------------------------+
void CTableView::Init(void)
  {
//--- Initialize the parent object
   CPanel::Init();
//--- Border width
   this.SetBorderWidth(1);
//--- Create the table header
   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);
   
//--- Create a container that will contain the table row panel
   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);
   
//--- Attach a panel to the container to store table rows
   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::Set the table model                                  |
//+------------------------------------------------------------------+
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::Set the table header model                           |
//+------------------------------------------------------------------+
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::Set the table object                                 |
//+------------------------------------------------------------------+
bool CTableView::TableObjectAssign(CTable *table_obj)
  {
//--- If an empty table object is passed, report this and return 'false'
   if(table_obj==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Save the pointer to a variable
   this.m_table_obj=table_obj;
//--- Set the result of assigning the table model and the header model
   bool res=this.TableModelAssign(this.m_table_obj.GetTableModel());
   res &=this.HeaderModelAssign(this.m_table_obj.GetTableHeader());
   
//--- If failed to assign any model, return 'false'
   if(!res)
      return false;
   
//--- Set the result of creating the table header from the model and the table from the model
   res=this.CreateHeader();
   res&=this.CreateTable();
   
//--- Return the result
   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::Create a table object from the model                 |
//+------------------------------------------------------------------+
bool CTableView::CreateTable(void)
  {
   if(this.m_table_area==NULL)
      return false;
   
//--- In a loop, create and attach RowsTotal rows from the TableRowView elements to the Panel (m_table_area) element
   int total=(int)this.m_table_model.RowsTotal();
   int y=1;                   // Vertical offset
   int table_height=0;        // Calculated panel height
   CTableRowView *row=NULL;
   for(int i=0;i<total;i++)
     {
      //--- Create and attach a table row object to the panel
      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;
      
      //--- Set the row ID
      row.SetID(i);
      //--- Set the row background color depending on its index (even/odd)
      if(row.ID()%2==0)
         row.InitBackColorDefault(clrWhite);
      else
         row.InitBackColorDefault(clrWhiteSmoke);
      row.BackColorToDefault();
      row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4));
      
      //--- Get the row model from the table object
      CTableRow *row_model=this.m_table_model.GetRow(i);
      if(row_model==NULL)
         return false;
      //--- Assign the obtained row model to the created table row object
      row.TableRowModelAssign(row_model);
      //--- Calculate the new panel height value
      table_height+=row.Height();
     }
//--- Return the result of changing the panel size to the value calculated in the loop
   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::Draw the appearance                                  |
//+------------------------------------------------------------------+
void CTableView::Draw(const bool chart_redraw)
  {
//--- Draw the table header and rows
   this.m_header_view.Draw(false);
   this.m_table_area_container.Draw(false);
//--- If specified, update the chart
   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::Print the assigned table model in the journal        |
//+------------------------------------------------------------------+
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::Print the assigned header model in the journal       |
//+------------------------------------------------------------------+
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::Print the assigned table object in the journal       |
//+------------------------------------------------------------------+
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 libraries                                                |
//+------------------------------------------------------------------+
#include "Controls\Controls.mqh"    // Controls library

CPanel     *panel=NULL; // Pointer to the Panel graphical element
CTable     *table;      // Pointer to table object (Model)

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

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Search for the chart subwindow
   int wnd=ChartWindowFind();

//--- Create the Panel graphical element
   panel=new CPanel("Panel","",0,wnd,100,40,400,192);
   if(panel==NULL)
      return INIT_FAILED;
//--- Set the panel parameters
   panel.SetID(1);                     // ID 
   panel.SetAsMain();                  // The chart should have one main element
   panel.SetBorderWidth(1);            // Border width (one pixel margin on each side of the container)
   panel.SetResizable(false);          // Ability to resize by dragging edges and corners
   panel.SetName("Main container");    // Name
   
//--- Create table data
//--- Declare and fill the array of column headers with the dimension of 4
   string captions[4]={"Column 0","Column 1","Column 2","Column 3"};
  
//--- Declare and fill the 10x4 data array
//--- Acceptable array types: 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}};
//--- Create a table object from the above created 10x4 long array and a string array of column headers (Model component)
   table=new CTable(array,captions);
   if(table==NULL)
      return INIT_FAILED;
   PrintFormat("The [%s] has been successfully created:",table.Description());
   
//--- Create a new element on the panel - a table (View component)
   CTableView *table_view=panel.InsertNewElement(ELEMENT_TYPE_TABLE,"","TableView",4,4,panel.Width()-8,panel.Height()-8);
//--- Assign the table object (Model) to the Table graphical element (View)
   table_view.TableObjectAssign(table);
//--- Print the table model in the journal
   table_view.TablePrint();
   
//--- Draw a table with a panel
   panel.Draw(true);

//--- Successful
   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)
  {
//--- Remove the Panel element and destroy the table and library's shared resource manager
   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 the next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Call the OnChartEvent handler of the Panel element
   panel.OnChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
//--- Call the OnTimer handler of the Panel element
   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)
Last comments | Go to discussion (2)
Alireza
Alireza | 29 Dec 2025 at 04:45

Excellent breakdown of MVC, but how exactly does this complexity improve trading which is what we’re really here for?

Have all other trading challenges been solved, leaving only the user interface?

Sometimes we forget what a tool is really for—or get frustrated when it doesn’t work the way we want—and end up trying to make it do things it was never meant to do which it never will

Artyom Trishkin
Artyom Trishkin | 29 Dec 2025 at 09:02
Alireza #:

Great description of MVC, but how exactly does this complexity improve the trading we're actually here for?

Are all the other trading problems solved, with only the UI left?

Sometimes we forget what a tool is really for, or get frustrated when it doesn't work the way we want it to, and end up trying to make it do things it was never intended to do and never will.

Yes, of course, it's not the interface that does the profiting. But an interface is like an aeroplane dashboard: it won't teach you how to fly, but it will help you not crash in the fog. When key trading problems have already been solved, code architecture and terminal usability become the very factor that allows scaling success rather than drowning in routine.
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.
Larry Williams Market Secrets (Part 1): Building a Swing Structure Indicator in MQL5 Larry Williams Market Secrets (Part 1): Building a Swing Structure Indicator in MQL5
A practical guide to building a Larry Williams–style market structure indicator in MQL5, covering buffer setup, swing-point detection, plot configuration, and how traders can apply the indicator in technical market analysis.
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.