Русский
preview
Tables in the MVC Paradigm in MQL5: Customizable and sortable table columns

Tables in the MVC Paradigm in MQL5: Customizable and sortable table columns

MetaTrader 5Examples |
6 763 2
Artyom Trishkin
Artyom Trishkin

Contents



Introduction

In the previous article focused on creating tables on MQL5 in the MVC paradigm, we linked tabular data (Model) with their graphical representation (View) in a single control (TableView), and created a simple static table based on the resulting object. Tables are a convenient tool for classifying and displaying various data in a user—friendly view. Accordingly, the table should provide more features for the user to control the display of data.

Today, we will add to the tables a feature to adjust the column widths, indicate the types of data displayed, and sort the table data by its columns. To do this, we just need to refine the previously created classes of controls. But in the end, we will add a new class that simplifies table creation. The class will allow for creating tables in several rows from previously prepared data.

In the MVC (Model — View — Controller) concept, the interaction between the three components is organized in such a way that when the external component (View) is changed using the Controller, the Model is modified. And then the modified model is re-displayed by the visual component (View). Here, we will organize interaction between the three components in the same way — clicking on table’s column header (the Controller component operation) will result in a change in data location in the table model (reorganizing the Model component), which will result in a change in the table view — the display of the result by the View component.



Refining Library Classes

All files of the library being developed are located at \MQL5\Indicators\Tables\. The Table Model class file (Tables.mqh), along with the test indicator file (iTestTable.mq5), is located at \MQL5\Indicators\Tables\. 

Graphic library files (Base.mqh and Controls.mqh) are located at subfolder \MQL5\Indicators\Tables\Controls\. All the necessary files can be downloaded in one archive from the previous article.

Refine table model classes in the file \MQL5\Indicators\Tables\Tables.mqh.

Rows in the table model are sorted by row ID (index) by default. The very first row has an index of 0. And the cells in the row also start with a zero index. To sort by cell indexes, we need to designate a certain number that we will add to the cell index, and by which we will determine that sorting by cell index is set. And we also should indicate the sorting direction. So we need two numbers: the first one will determine that the cell index needs to be sorted in ascending order, the second one will determine whether the cell index needs to be sorted in descending order.

Define macro substitutions for these numbers:

//+------------------------------------------------------------------+
//| 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
#define  ASC_IDX_CORRECTION   10000 // Column index offset for ascending sorting
#define  DESC_IDX_CORRECTION  20000 // Column index offset for descending sorting

Having defined such macro substitutions, we indicated that there can be no more than 10,000 rows in the table and cells in one row. It seems that this is more than enough to work with tables (100 million table cells). 

To the sorting method we will pass a number from zero to 10,000 as the mode parameter. This will be sorting by indexes of table rows. Numbers from 10,000 inclusive to 19,999 will indicate the sorting by the index of the table column in ascending order. Numbers from 20,000 — by column index in descending order:

/*
    Sort(0)                      -  by row index
    
    Sort(ASC_IDX_CORRECTION)     -  ascending by column 0
    Sort(1+ASC_IDX_CORRECTION)   -  ascending by column 1
    Sort(2+ASC_IDX_CORRECTION)   -  ascending by column 2
    etc.
    Sort(DESC_IDX_CORRECTION)    -  descending by column 0
    Sort(1+DESC_IDX_CORRECTION)  -  descending by column 1
    Sort(2+DESC_IDX_CORRECTION)  -  descending by column 2
    etc.
*/  

For the Sort() list sorting method to work correctly, it is necessary to redefine the virtual method for comparing two objects Compare() defined in the base object of the Standard Library. By default, this method returns 0, which means that the objects being compared are equal.

We already have an implemented comparison method in the table row class CTableRow. Refine it so that it is possible to sort rows by column indexes in the table, given the sorting direction:

//+------------------------------------------------------------------+
//| Compare two objects                                              |
//+------------------------------------------------------------------+
int CTableRow::Compare(const CObject *node,const int mode=0) const
  {
   if(node==NULL)
      return -1;
   
//--- Sort by row index
   if(mode==0)
     {
      const CTableRow *obj=node;
      return(this.Index()>obj.Index() ? 1 : this.Index()<obj.Index() ? -1 : 0);
     }
   
//--- Sort by cell index in ascending/descending order
//--- Sorting direction flag and cell index for sorting
   bool asc=(mode>=ASC_IDX_CORRECTION && mode<DESC_IDX_CORRECTION);
   int  col= mode%(asc ? ASC_IDX_CORRECTION : DESC_IDX_CORRECTION);
      
//--- Remove the 'node' constancy
   CTableRow *nonconst_this=(CTableRow*)&this;
   CTableRow *nonconst_node=(CTableRow*)node;

//--- Get the current and compared cells by 'mode' index
   CTableCell *cell_current =nonconst_this.GetCell(col);
   CTableCell *cell_compared=nonconst_node.GetCell(col);
   if(cell_current==NULL || cell_compared==NULL)
      return -1;
   
//--- Compare depending on the cell type
   int cmp=0;
   switch(cell_current.Datatype())
     {
      case TYPE_DOUBLE  :  cmp=(cell_current.ValueD()>cell_compared.ValueD() ? 1 : cell_current.ValueD()<cell_compared.ValueD() ? -1 : 0); break;
      case TYPE_LONG    :
      case TYPE_DATETIME:
      case TYPE_COLOR   :  cmp=(cell_current.ValueL()>cell_compared.ValueL() ? 1 : cell_current.ValueL()<cell_compared.ValueL() ? -1 : 0); break;
      case TYPE_STRING  :  cmp=::StringCompare(cell_current.ValueS(),cell_compared.ValueS());                                              break;
      default           :  break;
     }
//--- Return the result of comparing cells taking into account the sorting direction
   return(asc ? cmp : -cmp);   
  }

The highlighted in color code block compares two table cells in ascending (mode >= 10000 && <20000) and descending (mode>=20000). Since, for comparison we should get necessary cell objects from the string object, and they are not constant (while the string for comparison is passed to the method as a constant pointer), we first need to remove constancy for *node by declaring non-constant objects for comparison. And from them get cell objects for comparison.

These are dangerous transformations, as the constancy of pointers is violated, and objects can be accidentally altered. But here we definitely know that this method only compares values, but not their change. Therefore, here we can take a little controlled liberty to get the result we want.

We will add three new methods to the CTableModel table model class to simplify working with table columns and sorting by them:

public:
//--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Remove or (2) relocate the row, (3) clear the row data
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Return and (2) display the row description in the journal
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Add, (2) remove, (3) relocate a column, (4) clear data, set the column data (5) type,
//--- (6) data accuracy, (7) time, (8) column color names display flags
   bool              ColumnAddNew(const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint col_index, const uint index_to);
   void              ColumnClearData(const uint index);
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   void              ColumnSetDigits(const uint index,const int digits);
   void              ColumnSetTimeFlags(const uint index, const uint flags);
   void              ColumnSetColorNamesFlag(const uint index, const bool flag);
  
//--- Sort the table by the specified column and direction
   void              SortByColumn(const uint column, const bool descending);
   
//--- (1) Return and (2) display the table description in the journal
   virtual string    Description(void);
   void              Print(const bool detail);
   void              PrintTable(const int cell_width=CELL_WIDTH_IN_CHARS);

Outside of the class body, write their implementation.

A Method That Sets Column Time Display Flags:

//+------------------------------------------------------------------+
//| Set the column time display flags                                |
//+------------------------------------------------------------------+
void CTableModel::ColumnSetTimeFlags(const uint index,const uint flags)
  {
//--- In a loop through all table rows
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- get the cell with the column index from each row and set the time display flags
      CTableCell *cell=this.GetCell(i, index);
      if(cell!=NULL)
         cell.SetDatetimeFlags(flags);
     }
  }

A Method That Sets Display Flag of Column Color Name:

//+------------------------------------------------------------------+
//| Sets the flag for displaying column color names                  |
//+------------------------------------------------------------------+
void CTableModel::ColumnSetColorNamesFlag(const uint index,const bool flag)
  {
//--- In a loop through all table rows
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- get the cell with the column index from each row and set the flag for displaying color names
      CTableCell *cell=this.GetCell(i, index);
      if(cell!=NULL)
         cell.SetColorNameFlag(flag);
     }
  }

Both methods, in a simple loop through table rows get the desired cell from each subsequent row and set the specified flag for it.

A Method That Sorts the Table By the Specified Column and Direction:

//+------------------------------------------------------------------+
//| Sort the table by the specified column and direction             |
//+------------------------------------------------------------------+
void CTableModel::SortByColumn(const uint column,const bool descending)
  {
   if(this.m_list_rows.Total()==0)
      return;
   int mode=(int)column+(descending ? DESC_IDX_CORRECTION : ASC_IDX_CORRECTION);
   this.m_list_rows.Sort(mode);
   this.CellsPositionUpdate();   
  }

The index of the table column is passed to the method, by the values of which the table should be sorted, as well as the sorting direction flag. Leave the method if the list of lines is empty. Next, define the sorting mode (mode). If sorting is descending (descending == true), then add 20,000 to the column index, if sorting is ascending, then add 10000 to the column index. Next, call the sorting method with the mode indication and update all the cells in the table in each row.

Now add new methods to the CTable table class. These are methods with the same name for the ones just added to the table model class:

public:
//--- (1) Return and (2) display the cell description and (3) the object assigned to the cell
   string            CellDescription(const uint row, const uint col);
   void              CellPrint(const uint row, const uint col);
//--- Return (1) the object assigned to the cell and (2) the type of the object assigned to the cell
   CObject          *CellGetObject(const uint row, const uint col);
   ENUM_OBJECT_TYPE  CellGetObjType(const uint row, const uint col);
   
//--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Remove or (2) relocate the row, (3) clear the row data
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Return and (2) display the row description in the journal
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Add new, (2) remove, (3) relocate the column and (4) clear the column data
   bool              ColumnAddNew(const string caption,const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint index, const uint index_to);
   void              ColumnClearData(const uint index);
   
//--- Set (1) the value of the specified header and (2) data accuracy,
//--- (3) time and (4) color names for the specified column display flags
   void              ColumnCaptionSetValue(const uint index,const string value);
   void              ColumnSetDigits(const uint index,const int digits);
   void              ColumnSetTimeFlags(const uint index,const uint flags);
   void              ColumnSetColorNamesFlag(const uint col, const bool flag);
   
//--- (1) Set and (2) return the data type for the specified column
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   ENUM_DATATYPE     ColumnDatatype(const uint index);
   
//--- (1) Return and (2) display the object description in the journal
   virtual string    Description(void);
   void              Print(const int column_width=CELL_WIDTH_IN_CHARS);
  
//--- Sort the table by the specified column and direction
   void              SortByColumn(const uint column, const bool descending)
                       {
                        if(this.m_table_model!=NULL)
                           this.m_table_model.SortByColumn(column,descending);
                       }
   
//--- 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(OBJECT_TYPE_TABLE);           }

 ...

//+------------------------------------------------------------------+
//| Set the time display flags for the specified column              |
//+------------------------------------------------------------------+
void CTable::ColumnSetTimeFlags(const uint index,const uint flags)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetTimeFlags(index,flags);
  }
//+------------------------------------------------------------------+
//| Set the color name display flags for the specified column        |
//+------------------------------------------------------------------+
void CTable::ColumnSetColorNamesFlag(const uint index,const bool flag)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetColorNamesFlag(index,flag);
  }

The methods check the validity of the table model object and invoke its corresponding methods of the same name, which were discussed above.

Refine classes in file \MQL5\Indicators\Tables\Controls\Base.mqh.

Add a forward declaration of the classes that were made last time, but were not added to the list, and a new class that we will implement today:

//+------------------------------------------------------------------+
//| 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    CTableCellView;                  // Class for visual representation of a table cell
class    CTableRowView;                   // Class for visual representation of a table row
class    CColumnCaptionView;              // Class for visual representation of table column header
class    CTableHeaderView;                // Class for visual representation of a table header
class    CTableView;                      // Class for visual representation of a table
class    CTableControl;                   // Table management class
class    CPanel;                          // Panel control class
class    CGroupBox;                       // GroupBox control class
class    CContainer;                      // Container control class

Add a new type to the enumeration of UI element types:

//+------------------------------------------------------------------+
//| 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_VIEW,          // Table cell (View)
   ELEMENT_TYPE_TABLE_ROW_VIEW,           // Table row (View)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW,// Table column header (View)
   ELEMENT_TYPE_TABLE_HEADER_VIEW,        // Table header (View)
   ELEMENT_TYPE_TABLE_VIEW,               // Table (View)
   ELEMENT_TYPE_TABLE_CONTROL_VIEW,       // Table control (View)
   ELEMENT_TYPE_PANEL,                    // Panel control
   ELEMENT_TYPE_GROUPBOX,                 // GroupBox control
   ELEMENT_TYPE_CONTAINER,                // Container control
  };

Add a new object type 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_VIEW            :  return "TCELL";   // Table cell (View)
      case ELEMENT_TYPE_TABLE_ROW_VIEW             :  return "TROW";    // Table row (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  return "TCAPT";   // Table column header (View)
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  return "THDR";    // Table header (View)
      case ELEMENT_TYPE_TABLE_VIEW                 :  return "TABLE";   // Table (View)
      case ELEMENT_TYPE_TABLE_CONTROL_VIEW         :  return "TBLCTRL"; // Table control (View)
      case ELEMENT_TYPE_PANEL                      :  return "PNL";     // Panel Table control
      case ELEMENT_TYPE_GROUPBOX                   :  return "GRBX";    // GroupBox control
      case ELEMENT_TYPE_CONTAINER                  :  return "CNTR";    // Container control
      default                                      :  return "Unknown"; // Unknown
     }
  }

Add methods that return cursor coordinates to the base class of graphical elements:

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

//--- Return the cursor coordinates
   int               CursorX(void)                       const { return CCommonManager::GetInstance().CursorX();  }
   int               CursorY(void)                       const { return CCommonManager::GetInstance().CursorY();  }

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

This will allow each graphical element to have access to cursor coordinates at any time. CCommonManager singleton class constantly monitors the cursor coordinates, and accessing it from any graphical element gives the element access to these coordinates.

In the rectangular area class, add a method that removes assignment of an object to the area:

//+------------------------------------------------------------------+
//| 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, (2) unassign and (3) return the pointer to the assigned element
   void              AssignObject(CBaseObj *obj)               { this.m_assigned_obj=obj;                                     }
   void              UnassignObject(void)                      { this.m_assigned_obj=NULL;                                    }           
   CBaseObj         *GetAssignedObj(void)                      { return this.m_assigned_obj;                                  }
   
//--- 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); }
  };

If a UI element is programmatically deleted, then being assigned to an area in another UI element, its pointer will remain registered for that area. Accessing an element using this pointer will lead to a critical program termination. Therefore, when deleting an object, it is necessary to unattach it from the area if it was attached to it. Writing a NULL value to a pointer enables to control pointer validity to an already deleted UI element.

Classes of UI elements in the library under development are arranged so that if an object is attached to a container, when the element leaves its container, it gets cropped along boundaries of its container. If an element is completely outside the container, it is simply hidden. But if there is a command for the container to move it to the foreground, the container automatically brings all the elements attached to it to the foreground in a loop. Accordingly, hidden elements are visible, since shifting an object to the foreground means sequential execution of two hide-display commands. In order not to move hidden elements to the foreground, add a flag to UI element properties that it is hidden because it is located outside its container. And in the method of shifting an object to the foreground, check this flag.

In the base class of CCanvasBase UI element canvas, declare the following flag in the protected section:

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
   
   CColorElement     m_color_background_act;                   // Activated element background color control object
   CColorElement     m_color_foreground_act;                   // Activated element foreground color control object
   CColorElement     m_color_border_act;                       // Activated element frame color control object
   
   CAutoRepeat       m_autorepeat;                             // Event auto-repeat control object
   
   ENUM_ELEMENT_STATE m_state;                                 // Control state (e.g. buttons (on/off))
   long              m_chart_id;                               // Chart ID
   int               m_wnd;                                    // Chart subwindow index
   int               m_wnd_y;                                  // Cursor Y coordinate offset in the subwindow
   int               m_obj_x;                                  // Graphical object X coordinate
   int               m_obj_y;                                  // Graphical object Y coordinate
   uchar             m_alpha_bg;                               // Background transparency
   uchar             m_alpha_fg;                               // Foreground transparency
   uint              m_border_width_lt;                        // Left frame width
   uint              m_border_width_rt;                        // Right frame width
   uint              m_border_width_up;                        // Top frame width
   uint              m_border_width_dn;                        // Bottom frame width
   string            m_program_name;                           // Program name
   bool              m_hidden;                                 // Hidden object flag
   bool              m_blocked;                                // Blocked element flag
   bool              m_movable;                                // Moved element flag
   bool              m_resizable;                              // Resizing flag
   bool              m_focused;                                // Element flag in focus
   bool              m_main;                                   // Main object flag
   bool              m_autorepeat_flag;                        // Event sending auto-repeat flag
   bool              m_scroll_flag;                            // Flag for scrolling content using scrollbars
   bool              m_trim_flag;                              // Flag for clipping the element to the container borders
   bool              m_cropped;                                // Flag indicating that the object is hidden outside the container borders  
   int               m_cursor_delta_x;                         // Distance from the cursor to the left edge of the element
   int               m_cursor_delta_y;                         // Distance from the cursor to the top edge of the element
   int               m_z_order;                                // Graphical object Z-order

In the public section, implement a method that returns this flag:

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

//--- (1) Set and (2) return z-order
   bool              ObjectSetZOrder(const int value);
   int               ObjectZOrder(void)                  const { return this.m_z_order;                                                            }
   
//--- Return (1) the object's belonging to the program, the flag (2) of a hidden element, (3) a blocked element,
//--- (4) moved, (5) resized, (6) main element, (7) in focus, (8, 9) graphical object name (background, text)
   bool              IsBelongsToThis(const string name)  const { return(::ObjectGetString(this.m_chart_id,name,OBJPROP_TEXT)==this.m_program_name);}
   bool              IsHidden(void)                      const { return this.m_hidden;                                                             }
   bool              IsBlocked(void)                     const { return this.m_blocked;                                                            }
   bool              IsMovable(void)                     const { return this.m_movable;                                                            }
   bool              IsResizable(void)                   const { return this.m_resizable;                                                          }
   bool              IsMain(void)                        const { return this.m_main;                                                               }
   bool              IsFocused(void)                     const { return this.m_focused;                                                            }
   bool              IsAutorepeat(void)                  const { return this.m_autorepeat_flag;                                                    }
   bool              IsScrollable(void)                  const { return this.m_scroll_flag;                                                        }
   bool              IsTrimmed(void)                     const { return this.m_trim_flag;                                                          }
   bool              IsCropped(void)                     const { return this.m_cropped;                                                            }
   string            NameBG(void)                        const { return this.m_background.ChartObjectName();                                       }
   string            NameFG(void)                        const { return this.m_foreground.ChartObjectName();                                       }

a method that sets a flag, and a virtual method that returns a flag indicating that the element is located completely outside its container:

//--- Return the object boundaries considering the frame
   int               LimitLeft(void)                     const { return this.ObjectX()+(int)this.m_border_width_lt;                                }
   int               LimitRight(void)                    const { return this.ObjectRight()-(int)this.m_border_width_rt;                            }
   int               LimitTop(void)                      const { return this.ObjectY()+(int)this.m_border_width_up;                                }
   int               LimitBottom(void)                   const { return this.ObjectBottom()-(int)this.m_border_width_dn;                           }
   
//--- Set (1) movability, (2) main object flag for the object and (3) resizability,
//--- (4) auto-repeat events, (5) scrolling within the container and (6) clipping by 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;                                                            }
   void              SetCropped(const bool flag)               { this.m_cropped=flag;                                                              }
   
//--- Return the flag that the object is located outside its container
   virtual bool      IsOutOfContainer(void);
//--- Limit the graphical object by the container dimensions
   virtual bool      ObjectTrim(void);

Implementing a method that returns a flag indicating that the object is located outside its container:

//+------------------------------------------------------------------+
//| CCanvasBase::Return a flag that the object is                    |
//| located outside of its container                                 |
//+------------------------------------------------------------------+
bool CCanvasBase::IsOutOfContainer(void)
  {
//--- Return the result of checking that the object is completely outside the container
   return(this.Right() <= this.ContainerLimitLeft() || this.X() >= this.ContainerLimitRight() ||
          this.Bottom()<= this.ContainerLimitTop()  || this.Y() >= this.ContainerLimitBottom());
  }

The method checks coordinates of object's boundaries relative to container boundaries and returns a flag indicating that the element is completely outside its container. For some graphic elements, this method may be calculated differently. Therefore, the method is declared virtual.

In a method that trims a UI object along the container contour, manage the new flag:

//+-----------------------------------------------------------------------+
//| CCanvasBase::Crop a graphical object to the outline of its container  |
//+-----------------------------------------------------------------------+
bool CCanvasBase::ObjectTrim()
  {
//--- Check the element cropping permission flag and
//--- if the element should not be clipped by the container borders, return 'false'
   if(!this.m_trim_flag)
      return false;
//--- Get the container boundaries
   int container_left   = this.ContainerLimitLeft();
   int container_right  = this.ContainerLimitRight();
   int container_top    = this.ContainerLimitTop();
   int container_bottom = this.ContainerLimitBottom();
   
//--- Get the current object boundaries
   int object_left   = this.X();
   int object_right  = this.Right();
   int object_top    = this.Y();
   int object_bottom = this.Bottom();

//--- Check if the object is completely outside the container and hide it if it is
   if(this.IsOutOfContainer())
     {
      //--- Set the flag that the object is outside the container 
      this.m_cropped=true;
      //--- Hide the object and restore its dimensions
      this.Hide(false);
      if(this.ObjectResize(this.Width(),this.Height()))
         this.BoundResize(this.Width(),this.Height());
      return true;
     }
//--- The object is fully or partially located within the visible area of the container
   else
     {
      //--- Remove the flag indicating that the object is located outside the container
      this.m_cropped=false;
      //--- If the element is completely inside the container
      if(object_right<=container_right && object_left>=container_left &&
         object_bottom<=container_bottom && object_top>=container_top)
        {
         //--- If the width or height of the graphical object does not match the width or height of the element,
         //--- modify the graphical object according to the element dimensions and return 'true'
         if(this.ObjectWidth()!=this.Width() || this.ObjectHeight()!=this.Height())
           {
            if(this.ObjectResize(this.Width(),this.Height()))
               return true;
           }
        }
      //--- If the element is partially within the container visible area
      else
        {
         //--- If the element is vertically within the container visible area
         if(object_bottom<=container_bottom && object_top>=container_top)
           {
            //--- If the height of the graphic object does not match the height of the element,
            //--- modify the graphical object by the element height
            if(this.ObjectHeight()!=this.Height())
               this.ObjectResizeH(this.Height());
           }
         else
           {
            //--- If the element is horizontally within the container visible area
            if(object_right<=container_right && object_left>=container_left)
              {
               //--- If the width of the graphic object does not match the width of the element,
               //--- modify the graphical object by the element width
               if(this.ObjectWidth()!=this.Width())
                  this.ObjectResizeW(this.Width());
              }
           }
        }
     }
     
//--- Check whether the object extends horizontally and vertically beyond the container boundaries
   bool modified_horizontal=false;     // Horizontal change flag
   bool modified_vertical  =false;     // Vertical change flag
   
//--- Horizontal cropping
   int new_left = object_left;
   int new_width = this.Width();
//--- If the object extends beyond the container left border
   if(object_left<=container_left)
     {
      int crop_left=container_left-object_left;
      new_left=container_left;
      new_width-=crop_left;
      modified_horizontal=true;
     }
//--- If the object extends beyond the container right border
   if(object_right>=container_right)
     {
      int crop_right=object_right-container_right;
      new_width-=crop_right;
      modified_horizontal=true;
     }
//--- If there were changes horizontally
   if(modified_horizontal)
     {
      this.ObjectSetX(new_left);
      this.ObjectResizeW(new_width);
     }

//--- Vertical cropping
   int new_top=object_top;
   int new_height=this.Height();
//--- If the object extends beyond the top edge of the container
   if(object_top<=container_top)
     {
      int crop_top=container_top-object_top;
      new_top=container_top;
      new_height-=crop_top;
      modified_vertical=true;
     }
//--- If the object extends beyond the bottom border of the container 
   if(object_bottom>=container_bottom)
     {
      int crop_bottom=object_bottom-container_bottom;
      new_height-=crop_bottom;
      modified_vertical=true;
     }
//--- If there were vertical changes
   if(modified_vertical)
     {
      this.ObjectSetY(new_top);
      this.ObjectResizeH(new_height);
     }

//--- After calculations, the object may be hidden, but is now in the container area - display it
   this.Show(false);
      
//--- If the object has been changed, redraw it
   if(modified_horizontal || modified_vertical)
     {
      this.Update(false);
      this.Draw(false);
      return true;
     }
   return false;
  }

After such an addition, objects that are completely outside the container will not be moved to the foreground, becoming visible if a command is sent to move this object to the foreground, or the entire container with all its contents at once.

In the method that places the object in the foreground, check the flag being set in the above method:

//+------------------------------------------------------------------+
//| CCanvasBase::Bring an object to the foreground                   |
//+------------------------------------------------------------------+
void CCanvasBase::BringToTop(const bool chart_redraw)
  {
   if(this.m_cropped)
      return;
   this.Hide(false);
   this.Show(chart_redraw);
  }

If the flag for the object is set, there is no need to move the element to the foreground — leave the method.

Each UI element has a general mouse wheel scroll handler. This general handler calls a virtual method for handling wheel scrolling. Whereas, the value from the sparam of the general handler is passed to sparam. This is a mistake, since none of the controls can detect that the mouse wheel is scrolling over it. The solution is as follows: in the general handler, we know the name of the active element — the one above which the cursor is located, which means that when calling the scroll wheel handler, the name of the active element should be passed to sparam. And in the handler itself, check the element name and the sparam value. If they are equal, then this is exactly the object over which the mouse wheel scrolls. Implement this in the general event handler:

//--- Mouse wheel scroll event
   if(id==CHARTEVENT_MOUSE_WHEEL)
     {
      //--- If this is an active element, call its scroll wheel event handler
      if(this.IsCurrentActiveElement())
         this.OnWheelEvent(id,lparam,dparam,this.ActiveElementName());  // pass the name of the active element to sparam
     }


We will check for equality of the object name and value in sparam in the handler. The handler is located in another file along with other improvements.

Open \MQL5\Indicators\Tables\Controls\Controls.mqh — now improvements will be entered to this file.

Add new definitions to the macro substitutions section:

//+------------------------------------------------------------------+
//| 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_TABLE_COLUMN_MIN_W     12          // Table column minimal width
#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

#define  DEF_HINT_NAME_TOOLTIP      "HintTooltip"     // Tooltip name
#define  DEF_HINT_NAME_HORZ         "HintHORZ"        // "Double horizontal arrow" hint name
#define  DEF_HINT_NAME_VERT         "HintVERT"        // "Double vertical arrow" hint name
#define  DEF_HINT_NAME_NWSE         "HintNWSE"        // "Double arrow top-left" (NorthWest-SouthEast) hint name
#define  DEF_HINT_NAME_NESW         "HintNESW"        // "Double arrow bottom-left" (NorthEast-SouthWest) hint name
#define  DEF_HINT_NAME_SHIFT_HORZ   "HintShiftHORZ"   // "Horizontal offset arrow" hint name
#define  DEF_HINT_NAME_SHIFT_VERT   "HintShiftVERT"   // "Vertical offset arrow" hint name

The width of the table column must not be less than 12 pixels, so that when the size is reduced, the columns are not too narrow. It is more convenient to set names of tooltips using the compiler directive and substitute them as the name, since if we need to change the tooltip name, we only change the directive, and there is no need to search and change all occurrences of this name in different places of the code. We now have two names for two new tooltip. These will be tooltips that appear when you hover the cursor over an object's edge, which you can "pull" to resize the object.

Add a new enumeration of column sorting modes and new enumeration constants:

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_SORT_BY                       // Compared properties
  {
   ELEMENT_SORT_BY_ID   =  BASE_SORT_BY_ID,     // Comparison by element ID
   ELEMENT_SORT_BY_NAME =  BASE_SORT_BY_NAME,   // Comparison by element name
   ELEMENT_SORT_BY_X    =  BASE_SORT_BY_X,      // Comparison by element X coordinate
   ELEMENT_SORT_BY_Y    =  BASE_SORT_BY_Y,      // Comparison by element Y coordinate
   ELEMENT_SORT_BY_WIDTH=  BASE_SORT_BY_WIDTH,  // Comparison by element width
   ELEMENT_SORT_BY_HEIGHT= BASE_SORT_BY_HEIGHT, // Comparison by element height
   ELEMENT_SORT_BY_ZORDER= BASE_SORT_BY_ZORDER, // Comparison by element Z-order
   ELEMENT_SORT_BY_TEXT,                        // Comparison by element text
   ELEMENT_SORT_BY_COLOR_BG,                    // Comparison by element background color
   ELEMENT_SORT_BY_ALPHA_BG,                    // Comparison by element background transparency
   ELEMENT_SORT_BY_COLOR_FG,                    // Comparison by element foreground color
   ELEMENT_SORT_BY_ALPHA_FG,                    // Comparison by element foreground transparency color
   ELEMENT_SORT_BY_STATE,                       // Comparison by element state
   ELEMENT_SORT_BY_GROUP,                       // Comparison by element group
  };

enum ENUM_TABLE_SORT_MODE                       // Table column sorting modes
  {
   TABLE_SORT_MODE_NONE,                        // No sorting
   TABLE_SORT_MODE_ASC,                         // Sort in ascending order
   TABLE_SORT_MODE_DESC,                        // Sort in descending order
  };

enum ENUM_HINT_TYPE                             // Hint types
  {
   HINT_TYPE_TOOLTIP,                           // Tooltip
   HINT_TYPE_ARROW_HORZ,                        // Double horizontal arrow
   HINT_TYPE_ARROW_VERT,                        // Double vertical arrow
   HINT_TYPE_ARROW_NWSE,                        // Double arrow top-left --- bottom-right (NorthWest-SouthEast)
   HINT_TYPE_ARROW_NESW,                        // Double arrow bottom-left --- top-right (NorthEast-SouthWest)
   HINT_TYPE_ARROW_SHIFT_HORZ,                  // Horizontal offset arrow
   HINT_TYPE_ARROW_SHIFT_VERT,                  // Vertical offset arrow
  };

 In the CImagePainter image drawing class, declare two new methods that draw horizontal and vertical offset arrows:

//--- Clear the area
   bool              Clear(const int x,const int y,const int w,const int h,const bool update=true);
//--- Draw a filled (1) up, (2) down, (3) left and (4) right arrow
   bool              ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) horizontal 17х7 and (2) vertical 7х17 double arrow
   bool              ArrowHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); 
   bool              ArrowVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); 
   
//--- Draw a diagonal (1) top-left --- bottom-right and (2) bottom-left --- up-right 17x17 double arrow
   bool              ArrowNWSE(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowNESW(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw an 18x18 offset arrow (1) horizontally and (2) vertically
   bool              ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) checked and (2) unchecked CheckBox
   bool              CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) checked and (2) unchecked RadioButton
   bool              CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);

Outside of the class body, implement the declared methods.

A Method That Draws an 18x18 Horizontal Offset Arrow:

//+------------------------------------------------------------------+
//| CImagePainter::Draw an 18x18 arrow with horizontal offset        |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Shape coordinates
   int arrx[25]={0, 3, 4, 4, 7, 7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10,  7,  7,  4,  4,  3, 0, 0};
   int arry[25]={8, 5, 5, 7, 7, 0,  0,  7,  7,  5,  5,  8,  9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12, 9, 8};
   
//--- Draw the white background
   this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha));

//--- Draw the line of arrows
   this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha));
//--- Draw a dividing line
   this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha));
//--- Draw the left triangle
   this.m_canvas.Line(2,7, 2,10,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(3,6, 3,11,::ColorToARGB(clr,alpha));
//--- Draw the right triangle
   this.m_canvas.Line(14,6, 14,11,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(15,7, 15,10,::ColorToARGB(clr,alpha));

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

A Method That Draws an 18x18 Vertical Offset Arrow:

//+------------------------------------------------------------------+
//| CImagePainter::Draw an 18x18 arrow with vertical offset          |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Shape coordinates
   int arrx[25]={0, 7, 7, 5, 5, 8, 9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12,  9,  8,  5,  5,  7,  7,  0, 0};
   int arry[25]={7, 7, 4, 4, 3, 0, 0,  3,  4,  4,  7,  7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10, 7};
   
//--- Draw the white background
   this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha));

//--- Draw a dividing line
   this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha));
//--- Draw the line of arrows
   this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha));
//--- Draw the top triangle
   this.m_canvas.Line(7,2, 10,2,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(6,3, 11,3,::ColorToARGB(clr,alpha));
//--- Draw the bottom triangle
   this.m_canvas.Line(6,14, 11,14,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(7,15, 10,15,::ColorToARGB(clr,alpha));

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

   return true;
  }

Both methods draw arrow tooltips for horizontal ( ) and vertical ( ) offset of the element edge to change its dimensions.

In the base class of the CElementBase graphical element, make two methods AddHintsArrowed and ShowCursorHint virtual:

//--- Add an existing hint object to the list
   CVisualHint      *AddHint(CVisualHint *obj, const int dx, const int dy);
//--- (1) Add to the list and (2) remove tooltip objects with arrows from the list
   virtual bool      AddHintsArrowed(void);
   bool              DeleteHintsArrowed(void);
//--- Displays the resize cursor
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
//--- Handler for dragging element edges and corners
   virtual void      ResizeActionDragHandler(const int x, const int y);

In the class destructor, clear the list of tooltips:

//--- Constructors/destructor
                     CElementBase(void) { this.m_painter.CanvasAssign(this.GetForeground()); this.m_visible_in_container=true; }
                     CElementBase(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CElementBase(void) { this.m_list_hints.Clear(); }

When adding objects to the lists, first the exact same object is searched for in the list. Whereas, for proper search, the list is sorted by the property being compared. But initially the list can be sorted by another property. In order not to disrupt the initial sorting of the lists, we will memorize it, then set the sorting necessary for the search. And at the end, return the previously memorized one.

In the method of adding the specified tooltip to the list, make the following refinement:

//+------------------------------------------------------------------+
//| CElementBase::Add the specified hint object to the list          |
//+------------------------------------------------------------------+
bool CElementBase::AddHintToList(CVisualHint *obj)
  {
//--- If an empty pointer is passed, report this and return 'false'
   if(obj==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return false;
     }
//--- Save the list sorting method
   int sort_mode=this.m_list_hints.SortMode();
//--- Set the sorting flag for the list by ID
   this.m_list_hints.Sort(ELEMENT_SORT_BY_ID);
//--- If there is no such element in the list,
   if(this.m_list_hints.Search(obj)==NULL)
     {
      //--- return the list to its original sorting and get back the result of adding it to the list
      this.m_list_hints.Sort(sort_mode);
      return(this.m_list_hints.Add(obj)>-1);
     }
//--- Return the list to its original sorting
   this.m_list_hints.Sort(sort_mode);
//--- An element with this ID is already in the list - return 'false'
   return false;
  }

In the methods that work with the names of tooltip objects, we will now use names of the objects by the previously specified directives:

//+------------------------------------------------------------------+
//| CElementBase::Add hint objects with arrows to the list           |
//+------------------------------------------------------------------+
bool CElementBase::AddHintsArrowed(void)
  {
//--- Arrays of names and hint types
   string array[4]={DEF_HINT_NAME_HORZ,DEF_HINT_NAME_VERT,DEF_HINT_NAME_NWSE,DEF_HINT_NAME_NESW};
   
   ENUM_HINT_TYPE type[4]={HINT_TYPE_ARROW_HORZ,HINT_TYPE_ARROW_VERT,HINT_TYPE_ARROW_NWSE,HINT_TYPE_ARROW_NESW};
   
//--- In the loop, create four hints with arrows
   bool res=true;
   for(int i=0;i<(int)array.Size();i++)
      res &=(this.CreateAndAddNewHint(type[i],array[i],0,0)!=NULL);
      
//--- If there were errors during creation, return 'false'
   if(!res)
      return false;
      
//--- In the loop through the array of names of hint objects
   for(int i=0;i<(int)array.Size();i++)
     {
      //--- get the next object by name,
      CVisualHint *obj=this.GetHint(array[i]);
      if(obj==NULL)
         continue;
      //--- hide the object and draw the appearance (arrows according to the object type)
      obj.Hide(false);
      obj.Draw(false);
     }
//--- All is successful
   return true;
  }

...

//+------------------------------------------------------------------+
//| CElementBase::Displays the resize cursor                         |
//+------------------------------------------------------------------+
bool CElementBase::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y)
  {
   CVisualHint *hint=NULL;          // Pointer to the hint
   int hint_shift_x=0;              // Hint offset by X
   int hint_shift_y=0;              // Hint offset by Y
   
//--- Depending on the location of the cursor on the element borders
//--- specify the tooltip offsets relative to the cursor coordinates,
//--- display the required hint on the chart and get the pointer to this object
   switch(edge)
     {
      //--- Cursor on the right or left border - horizontal double arrow
      case CURSOR_REGION_RIGHT         :
      case CURSOR_REGION_LEFT          :
         hint_shift_x=1;
         hint_shift_y=18;
         this.ShowHintArrowed(HINT_TYPE_ARROW_HORZ,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_HORZ);
        break;
    
      //--- Cursor at the top or bottom border - vertical double arrow
      case CURSOR_REGION_TOP           :
      case CURSOR_REGION_BOTTOM        :
         hint_shift_x=12;
         hint_shift_y=4;
         this.ShowHintArrowed(HINT_TYPE_ARROW_VERT,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_VERT);
        break;
    
      //--- Cursor in the upper left or lower right corner - a diagonal double arrow from top left to bottom right
      case CURSOR_REGION_LEFT_TOP      :
      case CURSOR_REGION_RIGHT_BOTTOM  :
         hint_shift_x=10;
         hint_shift_y=2;
         this.ShowHintArrowed(HINT_TYPE_ARROW_NWSE,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_NWSE);
        break;
    
      //--- Cursor in the lower left or upper right corner - a diagonal double arrow from bottom left to top right
      case CURSOR_REGION_LEFT_BOTTOM   :
      case CURSOR_REGION_RIGHT_TOP     :
         hint_shift_x=5;
         hint_shift_y=12;
         this.ShowHintArrowed(HINT_TYPE_ARROW_NESW,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_NESW);
        break;
      
      //--- By default, do nothing
      default: break;
     }

//--- Return the result of adjusting the position of the tooltip relative to the cursor
   return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false);
  }

etc.

In the tooltip object class, declare two new methods that draw two new tooltips:

//+------------------------------------------------------------------+
//| Hint class                                                       |
//+------------------------------------------------------------------+
class CVisualHint : public CButton
  {
protected:
   ENUM_HINT_TYPE    m_hint_type;                              // Hint type

//--- Draw (1) a tooltip, (2) a horizontal, (3) a vertical arrow,
//--- arrows (4) top-left --- bottom-right, (5) bottom-left --- top-right,
//--- (6) horizontal and (7) vertical offset arrows
   void              DrawTooltip(void);
   void              DrawArrHorz(void);
   void              DrawArrVert(void);
   void              DrawArrNWSE(void);
   void              DrawArrNESW(void);
   void              DrawArrShiftHorz(void);
   void              DrawArrShiftVert(void);
   
//--- Initialize colors for the hint type (1) Tooltip, (2) arrows
   void              InitColorsTooltip(void);
   void              InitColorsArrowed(void);
   
public:
//--- (1) Set and (2) return the hint type
   void              SetHintType(const ENUM_HINT_TYPE type);
   ENUM_HINT_TYPE    HintType(void)                      const { return this.m_hint_type;             }

//--- 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;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);   }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);   }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_HINT);           }
   
//--- Initialize (1) the class object and (2) default object colors
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Constructors/destructor
                     CVisualHint(void);
                     CVisualHint(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CVisualHint (void) {}
  };

Outside of the class body, write their implementation:

//+------------------------------------------------------------------+
//| CVisualHint::Draw horizontal offset arrows                       |
//+------------------------------------------------------------------+
void CVisualHint::DrawArrShiftHorz(void)
  {
//--- 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);
//--- Draw horizontal offset arrows
   this.m_painter.ArrowShiftHorz(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
  }
//+------------------------------------------------------------------+
//| CVisualHint::Draw vertical offset arrows                         |
//+------------------------------------------------------------------+
void CVisualHint::DrawArrShiftVert(void)
  {
//--- 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);
//--- Draw horizontal offset arrows
   this.m_painter.ArrowShiftVert(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
  }

Add the processing of new methods for drawing tooltips to the methods for drawing and setting the tooltip type:

//+------------------------------------------------------------------+
//| CVisualHint::Set the hint type                                   |
//+------------------------------------------------------------------+
void CVisualHint::SetHintType(const ENUM_HINT_TYPE type)
  {
//--- If the passed type matches the set one, leave
   if(this.m_hint_type==type)
      return;
//--- Set a new hint type
   this.m_hint_type=type;
//--- Depending on the hint type, set the object dimensions
   switch(this.m_hint_type)
     {
      case HINT_TYPE_ARROW_HORZ        :  this.Resize(17,7);   break;
      case HINT_TYPE_ARROW_VERT        :  this.Resize(7,17);   break;
      case HINT_TYPE_ARROW_NESW        :
      case HINT_TYPE_ARROW_NWSE        :  this.Resize(13,13);  break;
      case HINT_TYPE_ARROW_SHIFT_HORZ  :
      case HINT_TYPE_ARROW_SHIFT_VERT  :  this.Resize(18,18);  break;
      default                          :  break;
     }
//--- Set the offset and dimensions of the image area,
//--- initialize colors based on the hint type
   this.SetImageBound(0,0,this.Width(),this.Height());
   this.InitColors();
  }
//+------------------------------------------------------------------+
//| CVisualHint::Draw the appearance                                 |
//+------------------------------------------------------------------+
void CVisualHint::Draw(const bool chart_redraw)
  {
//--- Depending on the type of hint, call the corresponding drawing method
   switch(this.m_hint_type)
     {
      case HINT_TYPE_ARROW_HORZ        :  this.DrawArrHorz();        break;
      case HINT_TYPE_ARROW_VERT        :  this.DrawArrVert();        break;
      case HINT_TYPE_ARROW_NESW        :  this.DrawArrNESW();        break;
      case HINT_TYPE_ARROW_NWSE        :  this.DrawArrNWSE();        break;
      case HINT_TYPE_ARROW_SHIFT_HORZ  :  this.DrawArrShiftHorz();   break;
      case HINT_TYPE_ARROW_SHIFT_VERT  :  this.DrawArrShiftVert();   break;
      default                          :  this.DrawTooltip();        break;
     }

//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Refine the CPanel panel class.

Add auxiliary methods for working with lists of attached elements:

//+------------------------------------------------------------------+
//| Panel class                                                      |
//+------------------------------------------------------------------+
class CPanel : public CLabel
  {
private:
   CElementBase      m_temp_elm;                // Temporary object for element searching
   CBound            m_temp_bound;              // Temporary object for area searching
protected:
   CListElm          m_list_elm;                // List of attached elements
   CListElm          m_list_bounds;             // List of areas
//--- Add a new element to the list
   bool              AddNewElement(CElementBase *element);

public:
//--- Return the pointer to the list of (1) attached elements and (2) areas
   CListElm         *GetListAttachedElements(void)             { return &this.m_list_elm;                         }
   CListElm         *GetListBounds(void)                       { return &this.m_list_bounds;                      }
   
//--- Return the attached element by (1) index in the list, (2) ID and (3) specified object name
   CElementBase     *GetAttachedElementAt(const uint index)    { return this.m_list_elm.GetNodeAtIndex(index);    }
   CElementBase     *GetAttachedElementByID(const int id);
   CElementBase     *GetAttachedElementByName(const string name);
   
//--- Returns the number of added elements
   int               AttachedElementsTotal(void)         const { return this.m_list_elm.Total();                  }

//--- Return the area by (1) index in the list, (2) ID and (3) specified area name
   CBound           *GetBoundAt(const uint index)              { return this.m_list_bounds.GetNodeAtIndex(index); }
   CBound           *GetBoundByID(const int id);
   CBound           *GetBoundByName(const string name);
   
//--- Create and add (1) a new and (2) a previously created element to the list
   virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h);
   virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy);
//--- Remove the specified element
   bool              DeleteElement(const int index)            { return this.m_list_elm.Delete(index);            }

//--- (1) Create and add a new area to the list and (2) remove the specified region
   CBound           *InsertNewBound(const string name,const int dx,const int dy,const int w,const int h);
   bool              DeleteBound(const int index)              { return this.m_list_bounds.Delete(index);         }
   
//--- (1) Assign an object to the specified area and (2) unassign an object from the specified area
   bool              AssignObjectToBound(const int bound, CBaseObj *object);
   bool              UnassignObjectFromBound(const int bound);

//--- Resize the object
   virtual bool      ResizeW(const int w);
   virtual bool      ResizeH(const int h);
   virtual bool      Resize(const int w,const int h);
//--- 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;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_PANEL);                      }
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);
   
//--- Set new XY object coordinates
   virtual bool      Move(const int x,const int y);
//--- Shift the object by XY axes by the specified offset
   virtual bool      Shift(const int dx,const int dy);
//--- Set both the element coordinates and dimensions
   virtual bool      MoveXYWidthResize(const int x,const int y,const int w,const int h);
   
//--- (1) Hide and (2) display the object on all chart periods,
//--- (3) bring the object to the front, (4) block, (5) unblock the element,
   virtual void      Hide(const bool chart_redraw);
   virtual void      Show(const bool chart_redraw);
   virtual void      BringToTop(const bool chart_redraw);
   virtual void      Block(const bool chart_redraw);
   virtual void      Unblock(const bool chart_redraw);
   
//--- Display the object description in the journal
   virtual void      Print(void);
   
//--- Print a list of (1) attached objects and (2) areas
   void              PrintAttached(const uint tab=3);
   void              PrintBounds(void);

//--- Event handler
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
   
//--- Timer event handler
   virtual void      TimerEventHandler(void);
   
//--- Constructors/destructor
                     CPanel(void);
                     CPanel(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CPanel (void) { this.m_list_elm.Clear(); this.m_list_bounds.Clear(); }
  };

The method of element width change ResizeW() has a potential error:

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

If the container received by the GetContainer() method is not the CContainer class, the program will terminate due to a critical error due to inability to convert object types.

Fix it:

//+------------------------------------------------------------------+
//| 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  *container=NULL;
   CCanvasBase *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
     {
      container=base;
      container.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;
  }

Now we can accurately assign a pointer to the correct type of object.

In the method of adding a new element to the list, remember the initial sorting of the list, and return it after adding the object to the list:

//+------------------------------------------------------------------+
//| CPanel::Add a new element to the list                            |
//+------------------------------------------------------------------+
bool CPanel::AddNewElement(CElementBase *element)
  {
//--- If an empty pointer is passed, report this and return 'false'
   if(element==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return false;
     }
//--- Save the list sorting method
   int sort_mode=this.m_list_elm.SortMode();
//--- Set the sorting flag for the list by ID
   this.m_list_elm.Sort(ELEMENT_SORT_BY_ID);
//--- If there is no such element in the list,
   if(this.m_list_elm.Search(element)==NULL)
     {
      //--- return the list to its original sorting and get back the result of adding it to the list
      this.m_list_elm.Sort(sort_mode);
      return(this.m_list_elm.Add(element)>-1);
     }
//--- Return the list to its original sorting
   this.m_list_elm.Sort(sort_mode);
//--- An element with this ID is already in the list - return 'false'
   return false;
  }

Refine a method that creates and adds a new area to the list:

//+------------------------------------------------------------------+
//| Create and add a new area to the list                            |
//+------------------------------------------------------------------+
CBound *CPanel::InsertNewBound(const string name,const int dx,const int dy,const int w,const int h)
  {
//--- Check whether the list contains a region with the specified name and, if it does, report this and return NULL
   this.m_temp_bound.SetName(name);
//--- Save the list sorting method
   int sort_mode=this.m_list_bounds.SortMode();
//--- Set the sorting by name flag to the list
   this.m_list_bounds.Sort(ELEMENT_SORT_BY_NAME);
   if(this.m_list_bounds.Search(&this.m_temp_bound)!=NULL)
     {
      //--- Return the list to its original sorting, report that such an object already exists and return NULL
      this.m_list_bounds.Sort(sort_mode);
      ::PrintFormat("%s: Error. An area named \"%s\" is already in the list",__FUNCTION__,name);
      return NULL;
     }
//--- Return the list to its original sorting
   this.m_list_bounds.Sort(sort_mode);
//--- Create a new area object; if unsuccessful, report it and return NULL
   CBound *bound=new CBound(dx,dy,w,h);
   if(bound==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create CBound object",__FUNCTION__);
      return NULL;
     }
//--- Set the area name and ID, and return the pointer to the object
   bound.SetName(name);
   bound.SetID(this.m_list_bounds.Total());
//--- If failed to add the new object to the list, report this, remove the object and return NULL
   if(this.m_list_bounds.Add(bound)==-1)
     {
      ::PrintFormat("%s: Error. Failed to add CBound object to list",__FUNCTION__);
      delete bound;
      return NULL;
     }
   return bound;
  }

There are two methods in the class that were declared but not implemented. Fix it.

A Method That Returns an Area by ID:

//+------------------------------------------------------------------+
//| CPanel::Return area by ID                                        |
//+------------------------------------------------------------------+
CBound *CPanel::GetBoundByID(const int id)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL && bound.ID()==id)
         return bound;
     }
   return NULL;
  }

In a simple loop through objects of element's areas, we search for the area with the indicated identifier and return a pointer to the found object.

A Method That Returns an Area by the Assigned Name of the Area:

//+------------------------------------------------------------------+
//| CPanel::Return the area by the assigned area name                |
//+------------------------------------------------------------------+
CBound *CPanel::GetBoundByName(const string name)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL && bound.Name()==name)
         return bound;
     }
   return NULL;
  }

In a simple loop through objects of element's areas, we search for the area with the indicated name and return a pointer to the found object.

Write implementation of the two declared methods.

A Method That Assigns an Object to an Indicated Area:

//+------------------------------------------------------------------+
//| CPanel::Assign an object to the specified area                   |
//+------------------------------------------------------------------+
bool CPanel::AssignObjectToBound(const int bound,CBaseObj *object)
  {
   CBound *bound_obj=this.GetBoundAt(bound);
   if(bound_obj==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound);
      return false;
     }
   bound_obj.AssignObject(object);
   return true;
  }

We get the area by its identifier and call the area object method, which assigns the object to the area.

A Method That Cancels Assignment Of an Object From the Indicated Area:

//+------------------------------------------------------------------+
//| CPanel::Unassign an object from the specified area               |
//+------------------------------------------------------------------+
bool CPanel::UnassignObjectFromBound(const int bound)
  {
   CBound *bound_obj=this.GetBoundAt(bound);
   if(bound_obj==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound);
      return false;
     }
   bound_obj.UnassignObject();
   return true;
  }

We get the area by its identifier and call the area object method, that cancels a previously assigned object to the area.

There is a flaw in the horizontal and vertical scrollbar classes. This results in a fact that when scrolling the thumb, several scrollbars can shift at once if there is more than one container on the chart. To correct this behavior, the name of the active element should be read from the sparam parameter and it should be compared with the thumb name. If there is a substring with the name of the active element within the thumb name, then it is this thumb that scrolls. Make edits to the mouse wheel scroll handlers in both classes of scrollbar thumbs:

//+------------------------------------------------------------------+
//| CScrollBarThumbH::Wheel scroll handler                           |
//+------------------------------------------------------------------+
void CScrollBarThumbH::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Get the pointer to the base object (the "horizontal scroll bar" control)
   CCanvasBase *base_obj=this.GetContainer();
   
//--- Get the name of the main object in the hierarchy by the value in sparam
   string array_names[];
   string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : "");
   
//--- If the main object in the hierarchy is not ours, leave
   if(::StringFind(this.NameFG(),name_main)!=0)
      return;
      
//--- If the slider's movability flag is not set, or the pointer to the base object is not received, leave
   if(!this.IsMovable() || base_obj==NULL)
      return;
   
//--- Get the width of the base object and calculate the boundaries of the space for the slider
   int base_w=base_obj.Width();
   int base_left=base_obj.X()+base_obj.Height();
   int base_right=base_obj.Right()-base_obj.Height()+1;
   
//--- Set the offset direction depending on the mouse wheel rotation direction
   int dx=(dparam<0 ? 2 : dparam>0 ? -2 : 0);
   if(dx==0)
      dx=(int)lparam;

//--- If the slider goes beyond the left edge of its area when moving, set it to the left edge
   if(dx<0 && this.X()+dx<=base_left)
      this.MoveX(base_left);
//--- otherwise, if the slider moves beyond the right edge of its area, position it along the right edge
   else if(dx>0 && this.Right()+dx>=base_right)
      this.MoveX(base_right-this.Width());
//--- Otherwise, if the slider is within its area, move it by the offset value
   else
     {
      this.ShiftX(dx);
     }

//--- Calculate the slider position
   int thumb_pos=this.X()-base_left;
   
//--- Get cursor coordinates
   int x=CCommonManager::GetInstance().CursorX();
   int y=CCommonManager::GetInstance().CursorY();
   
//--- If the cursor lands on the slider, change the color to "In focus",
   if(this.Contains(x,y))
      this.OnFocusEvent(id,lparam,dparam,sparam);
//--- otherwise, return the color to "Default"
   else
      this.OnReleaseEvent(id,lparam,dparam,sparam);
      
//--- Send a custom event to the chart with the slider position in lparam and the object name in sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG());
//--- Redraw the chart
   if(this.m_chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

//+------------------------------------------------------------------+
//| CScrollBarThumbV::Wheel scroll handler                           |
//+------------------------------------------------------------------+
void CScrollBarThumbV::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Get the pointer to the base object (the "vertical scroll bar" control)
   CCanvasBase *base_obj=this.GetContainer();
   
//--- Get the name of the main object in the hierarchy by the value in sparam
   string array_names[];
   string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : "");
   
//--- If the main object in the hierarchy is not ours, leave
   if(::StringFind(this.NameFG(),name_main)!=0)
      return;
      
//--- If the slider's movability flag is not set, or the pointer to the base object is not received, leave
   if(!this.IsMovable() || base_obj==NULL)
      return;
   
//--- Get the height of the base object and calculate the boundaries of the space for the slider
   int base_h=base_obj.Height();
   int base_top=base_obj.Y()+base_obj.Width();
   int base_bottom=base_obj.Bottom()-base_obj.Width()+1;
   
//--- Set the offset direction depending on the mouse wheel rotation direction
   int dy=(dparam<0 ? 2 : dparam>0 ? -2 : 0);
   if(dy==0)
      dy=(int)lparam;

//--- If the slider goes beyond the top edge of its area when moving, set it to the top edge
   if(dy<0 && this.Y()+dy<=base_top)
      this.MoveY(base_top);
//--- otherwise, if the slider moves beyond the bottom edge of its area, position it along the bottom edge
   else if(dy>0 && this.Bottom()+dy>=base_bottom)
      this.MoveY(base_bottom-this.Height());
//--- Otherwise, if the slider is within its area, move it by the offset value
   else
     {
      this.ShiftY(dy);
     }

//--- Calculate the slider position
   int thumb_pos=this.Y()-base_top;
   
//--- Get cursor coordinates
   int x=CCommonManager::GetInstance().CursorX();
   int y=CCommonManager::GetInstance().CursorY();
   
//--- If the cursor lands on the slider, change the color to "In focus",
   if(this.Contains(x,y))
      this.OnFocusEvent(id,lparam,dparam,sparam);
//--- otherwise, return the color to "Default"
   else
      this.OnReleaseEvent(id,lparam,dparam,sparam);
      
//--- Send a custom event to the chart with the slider position in lparam and the object name in sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG());
//--- Redraw the chart
   if(this.m_chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

In the CContainer container class, in the method of shifting the container contents horizontally, it is important to take into account that for a table, when scrolling horizontally, it is also necessary to scroll the table header. Implement it:

//+-------------------------------------------------------------------+
//|CContainer::Shift the content horizontally by the specified value  |
//+-------------------------------------------------------------------+
bool CContainer::ContentShiftHorz(const int value)
  {
//--- Get the pointer to the container contents
   CElementBase *elm=this.GetAttachedElement();
   if(elm==NULL)
      return false;
   
//--- Get the table header for the CTableView element
   CElementBase     *elm_container=elm.GetContainer();
   CTableHeaderView *table_header=NULL;
   if(elm_container!=NULL && ::StringFind(elm.Name(),"Table")==0)
     {
      CElementBase *obj=elm_container.GetContainer();
      if(obj!=NULL && obj.Type()==ELEMENT_TYPE_TABLE_VIEW)
        {
         CTableView *table_view=obj;
         table_header=table_view.GetHeader();
        }
     }
//--- Calculate the offset value based on the slider position
   int content_offset=this.CalculateContentOffsetHorz(value);

//--- Shift the header
   bool res=true;
   if(table_header!=NULL)
     {
      res &=table_header.MoveX(this.X()-content_offset);
     }
     
//--- Return the result of shifting the content by the calculated value
   res &=elm.MoveX(this.X()-content_offset);
   return res;
  }

In the method that returns the type of the element that sent the event, the base element was incorrectly searched in the container object class:

//--- Get the names of all elements in the hierarchy (if an error occurs, return -1)
   string names[]={};
   int total = GetElementNames(name,"_",names);
   if(total==WRONG_VALUE)
      return WRONG_VALUE;
      
//--- If the name of the base element in the hierarchy does not match the name of the container, then this is not our event - leave
   string base_name=names[0];
   if(base_name!=this.NameFG())
      return WRONG_VALUE;

The base object is not always the very first one in the hierarchy of the container's graphical elements. There may be situations where one element is nested into another multiple times, and then for the last elements in the hierarchy, their base object will not be the first one in the list of names of all nested container elements. Search for the base object correctly:

//+------------------------------------------------------------------+
//| Return the type of the element that sent the event               |
//+------------------------------------------------------------------+
ENUM_ELEMENT_TYPE CContainer::GetEventElementType(const string name)
  {
//--- Get the names of all elements in the hierarchy (if an error occurs, return -1)
   string names[]={};
   int total = GetElementNames(name,"_",names);
   if(total==WRONG_VALUE)
      return WRONG_VALUE;
   
//--- Find the container name in the array that is closest to the name of the element with the event
   int    cntr_index=-1;      // Index of the container name in the array of names in the element hierarchy
   string cntr_name="";       // The name of the container in the array of names in the element hierarchy
   
//--- Search in the loop for the very first occurrence of the CNTR substring from the end
   for(int i=total-1;i>=0;i--)
     {
      if(::StringFind(names[i],"CNTR")==0)
        {
         cntr_name=names[i];
         cntr_index=i;
         break;
        }
     }
//--- If the container name is not found in the array (index is -1), return -1
   if(cntr_index==WRONG_VALUE)
      return WRONG_VALUE;
   
//--- If the element name does not contain a substring with the name of the base element, then this is not our event - leave
   string base_name=names[cntr_index];
   if(::StringFind(this.NameFG(),base_name)==WRONG_VALUE)
      return WRONG_VALUE;

//--- Events that do not arrive from scrollbars are skipped
   string check_name=::StringSubstr(names[cntr_index+1],0,4);
   if(check_name!="SCBH" && check_name!="SCBV")
      return WRONG_VALUE;
      
//--- Get the name of the element the event came from and initialize the element type
   string elm_name=names[names.Size()-1];
   ENUM_ELEMENT_TYPE type=WRONG_VALUE;
   
//--- Check and write the element type
//--- Up arrow button
   if(::StringFind(elm_name,"BTARU")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_UP;
//--- Down arrow button
   else if(::StringFind(elm_name,"BTARD")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_DOWN;
//--- Left arrow button
   else if(::StringFind(elm_name,"BTARL")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_LEFT;
//--- Right arrow button
   else if(::StringFind(elm_name,"BTARR")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_RIGHT;
//--- Horizontal scroll bar slider
   else if(::StringFind(elm_name,"THMBH")==0)
      type=ELEMENT_TYPE_SCROLLBAR_THUMB_H;
//--- Vertical scroll bar slider
   else if(::StringFind(elm_name,"THMBV")==0)
      type=ELEMENT_TYPE_SCROLLBAR_THUMB_V;
//--- ScrollBarHorisontal control
   else if(::StringFind(elm_name,"SCBH")==0)
      type=ELEMENT_TYPE_SCROLLBAR_H;
//--- ScrollBarVertical control
   else if(::StringFind(elm_name,"SCBV")==0)
      type=ELEMENT_TYPE_SCROLLBAR_V;
      
//--- Return the element type
   return type;
  }

Now, refine the visual representation class of the CTableCellView table cell. The refinements will affect limitations of cell rendering — you do not need to draw a cell that is outside its container, so as not to waste resources on drawing outside the graphical object. Also make it possible to change the color of the text displayed in the cell and correct the text output to the cell for different types of text anchor points.

Declare new variables and methods:

//+------------------------------------------------------------------+
//| 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
   color             m_fore_color;                             // Foreground color
   
//--- 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, int &dir_x, int dir_y);

//--- Return the pointer to the table row panel container
   CContainer       *GetRowsPanelContainer(void);
   
public:
//--- Return the pointer to the specified (1) background and (2) foreground canvas
   CCanvas          *GetBackground(void)                                { return this.m_background;                  }
   CCanvas          *GetForeground(void)                                { return this.m_foreground;                  }

//--- Get the boundaries of the parent container object
   int               ContainerLimitLeft(void)   const { return(this.m_element_base==NULL ? this.X()      :  this.m_element_base.LimitLeft());   }
   int               ContainerLimitRight(void)  const { return(this.m_element_base==NULL ? this.Right()  :  this.m_element_base.LimitRight());  }
   int               ContainerLimitTop(void)    const { return(this.m_element_base==NULL ? this.Y()      :  this.m_element_base.LimitTop());    }
   int               ContainerLimitBottom(void) const { return(this.m_element_base==NULL ? this.Bottom() :  this.m_element_base.LimitBottom()); }

//--- Return the flag that the object is located outside its container
   virtual bool      IsOutOfContainer(void);

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

//--- (1) Set and (2) return the cell text color
   void              SetForeColor(const color clr)                      { this.m_fore_color=clr;                     }
   color             ForeColor(void)                              const { return this.m_fore_color;                  }

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

In the method that assigns a row, background and foreground canvases to a cell, set the text color to be equal to the foreground color:

//+---------------------------------------------------------------------+
//| 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();
   this.m_fore_color=this.m_element_base.ForeColor();
  }

In the method that returns the X and Y coordinates of the text depending on the anchor point, add variables that will record the signs of the shift direction (+1 / -1) along the X and Y axes:

//+------------------------------------------------------------------+
//| CTableCellView::Return the X and Y coordinates of the text       |
//| depending on the anchor point                                    |
//+------------------------------------------------------------------+
bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y, int &dir_x,int dir_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;
        dir_x=1;
        dir_y=1;
        break;
      //--- Anchor point in the lower left corner
      case ANCHOR_LEFT_LOWER :
        x=0;
        y=this.Height()-text_h;
        dir_x= 1;
        dir_y=-1;
        break;
      //--- Anchor point at the bottom center
      case ANCHOR_LOWER :
        x=(this.Width()-text_w)/2;
        y=this.Height()-text_h;
        dir_x= 1;
        dir_y=-1;
        break;
      //--- Anchor point in the lower right corner
      case ANCHOR_RIGHT_LOWER :
        x=this.Width()-text_w;
        y=this.Height()-text_h;
        dir_x=-1;
        dir_y=-1;
        break;
      //--- Anchor point is right centered
      case ANCHOR_RIGHT :
        x=this.Width()-text_w;
        y=(this.Height()-text_h)/2;
        dir_x=-1;
        dir_y= 1;
        break;
      //--- Anchor point in the upper right corner
      case ANCHOR_RIGHT_UPPER :
        x=this.Width()-text_w;
        y=0;
        dir_x=-1;
        dir_y= 1;
        break;
      //--- Anchor point at top center
      case ANCHOR_UPPER :
        x=(this.Width()-text_w)/2;
        y=0;
        dir_x=1;
        dir_y=1;
        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;
        dir_x=1;
        dir_y=1;
        break;
      //--- Anchor point in the upper left corner
      //---ANCHOR_LEFT_UPPER
      default:
        x=0;
        y=0;
        dir_x=1;
        dir_y=1;
        break;
     }
   return true;
  }

In the method that draws the view, we now use the values obtained as a multiplier to shift the text relative to X and Y axes, and also do not draw the cell if it is outside its container:

//+------------------------------------------------------------------+
//| CTableCellView::Draw the appearance                              |
//+------------------------------------------------------------------+
void CTableCellView::Draw(const bool chart_redraw)
  {
//--- If the cell is outside the table row container, leave
   if(this.IsOutOfContainer())
      return;
      
//--- Get the text coordinates and the offset direction depending on the anchor point
   int text_x=0, text_y=0;
   int dir_horz=0, dir_vert=0;
   if(!this.GetTextCoordsByAnchor(text_x,text_y,dir_horz,dir_vert))
      return;
//--- 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 the text on the foreground canvas taking into account the offset direction without updating the chart
   this.DrawText(x+this.m_text_x*dir_horz,y+this.m_text_y*dir_vert,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);
  }

Now the text in the cell will be positioned correctly, regardless of the anchor point of the text.

A Method That Returns a Pointer to the Table Row Panel Container:

//+------------------------------------------------------------------+
//| CTableCellView::Return the pointer                               |
//| to the table row panel container                                 |
//+------------------------------------------------------------------+
CContainer *CTableCellView::GetRowsPanelContainer(void)
  {
//--- Check the row
   if(this.m_element_base==NULL)
      return NULL;
//--- Get the panel for placing rows
   CPanel *rows_area=this.m_element_base.GetContainer();
   if(rows_area==NULL)
      return NULL;
//--- Return the panel container with rows
   return rows_area.GetContainer();
  }

The cell is located inside the row. The row, along with other rows, is located on the panel. The panel, in turn, is attached to a container, inside of which scrollbars can scroll it. The method returns a pointer to a container with scrollbars.

A Method That Returns a Flag Indicating That the Object Is Located Outside Its Container:

//+------------------------------------------------------------------+
//| CTableCellView::Return the flag that the object is               |
//| located outside of its container                                 |
//+------------------------------------------------------------------+
bool CTableCellView::IsOutOfContainer(void)
  {
//--- Check the row
   if(this.m_element_base==NULL)
      return false;

//--- Get the panel container with rows
   CContainer *container=this.GetRowsPanelContainer();
   if(container==NULL)
      return false;
  
//--- Get the cell boundaries by all sides
   int cell_l=this.m_element_base.X()+this.X();
   int cell_r=this.m_element_base.X()+this.Right();
   int cell_t=this.m_element_base.Y()+this.Y();
   int cell_b=this.m_element_base.Y()+this.Bottom();
   
//--- Return the result of checking that the object is completely outside the container
   return(cell_r <= container.X() || cell_l >= container.Right() || cell_b <= container.Y() || cell_t >= container.Bottom());
  }

The cell is located inside its own area located in the row. The method calculates cell coordinates relative to the row and returns a flag indicating that the calculated cell coordinates go beyond the container.

Now, refine the visual representation class of the CTableRowView table row.

Declare new methods and in the destructor clear the cell list:

//+------------------------------------------------------------------+
//| 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);
//--- Delete the specified row region and the cell with the corresponding index
   bool              BoundCellDelete(const int index);
   
public:
//--- Return (1) the list, (2) the number of cells and (3) the cell
   CListElm         *GetListCells(void)                                 { return &this.m_list_cells;                       }
   int               CellsTotal(void)                             const { return this.m_list_cells.Total();                }
   CTableCellView   *GetCellView(const uint index)                      { return this.m_list_cells.GetNodeAtIndex(index);  }
   
//--- 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;                   }
//--- Update the row with the updated model
   bool              TableRowModelUpdate(CTableRow *row_model);

//--- Recalculate cell areas
   bool              RecalculateBounds(CListElm *list_bounds);

//--- 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_VIEW);             }
  
//--- 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){ this.m_list_cells.Clear(); }
  };

In the method of creating and adding to the list a new cell representation object, remember the sorting mode of the list, and after adding, return the list to its original sorting:

//+------------------------------------------------------------------+
//| 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);
//--- Save the list sorting method
   int sort_mode=this.m_list_cells.SortMode();
//--- 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)
     {
      //--- Return the list to its original sorting, report that such an object already exists and return NULL
      this.m_list_cells.Sort(sort_mode);
      ::PrintFormat("%s: Error. The TableCellView object with index %d is already in the list",__FUNCTION__,index);
      return NULL;
     }
//--- Return the list to its original sorting
   this.m_list_cells.Sort(sort_mode);
//--- Create a cell object name
   string name="TableCellView"+(string)this.Index()+"x"+(string)index;

//--- Create a new TableCellView object; in case of a failure, report it and return NULL
   CTableCellView *cell_view=new CTableCellView(index,name,text,dx,dy,w,h);
   if(cell_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create CTableCellView object",__FUNCTION__);
      return NULL;
     }
//--- 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 (row) and return the pointer to the object
   cell_view.RowAssign(&this);
   return cell_view;
  }

In the row model installation method, the column width will be calculated based on the width of the row panel, not the row itself, since the row is slightly narrower than the panel. The table header width is also equal to that of the row panel, so we use the panel width to match the widths of cells and columns:

//+------------------------------------------------------------------+
//| 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
   this.m_table_row_model=row_model;
//--- calculate the cell width based on the row panel width
   CCanvasBase *base=this.GetContainer();
   int w=(base!=NULL ? base.Width() : this.Width());
   int cell_w=(int)::fmax(::round((double)w/(double)total),DEF_TABLE_COLUMN_MIN_W);

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

Cells in a table row can update their view, for example, when adding or deleting a table column. Such changes made to the table model must also be displayed in the visual representation of the table.

Create a special method for this.

A Method That Updates a Row With an Updated Model:

//+------------------------------------------------------------------+
//| CTableRowView::Update the row with the updated model             |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelUpdate(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_model=(int)row_model.CellsTotal(); // Number of cells in the row model
   if(total_model==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Save the pointer to the passed row model
   this.m_table_row_model=row_model;

//--- Calculate the cell width based on the row panel width
   CCanvasBase *base=this.GetContainer();
   int w=(base!=NULL ? base.Width() : this.Width());
   int cell_w=(int)::fmax(::round((double)w/(double)total_model),DEF_TABLE_COLUMN_MIN_W);
   
   CBound *cell_bound=NULL;
   int total_bounds=this.m_list_bounds.Total(); // Number of areas
   int diff=total_model-total_bounds;           // Difference between the number of areas in a row and the number of cells in a row model 
   
//--- If the model has more cells than areas in the list, create the missing areas and cells at the end of the lists
   if(diff>0)
     {
      //--- In a loop by the number of missing areas
      for(int i=total_bounds;i<total_bounds+diff;i++)
        {
         //--- create and add the number of cell areas of the row to the diff list.
         //--- 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;
        }
     }
 
//--- If there are more areas in the list than cells in the model, remove the extra areas at the end of the list
   if(diff<0)
     {
      int  start=total_bounds-1;
      int  end=start-diff;
      bool res=true;
      for(int i=start;i>end;i--)
        {
         if(!this.BoundCellDelete(i))
            return false;
        }
     }
   
//--- In the loop by the number of cells in the row model
   for(int i=0;i<total_model;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 cell coordinate
      int x=cell_w*i;
      //--- Get the next cell area
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         return false;
      
      //--- Get the cell visual representation object from the list 
      CTableCellView *cell_view=this.m_list_cells.GetNodeAtIndex(i);
      if(cell_view==NULL)
         return false;
      
      //--- Assign the corresponding visual representation object of the cell and its text to the current cell area
      cell_bound.AssignObject(cell_view);
      cell_view.SetText(cell_model.Value());
     }
//--- All is successful
   return true;
  }

The method's logic is explained in the comments to the code. If columns have been added or deleted in the table model, then the required number of cells is added or deleted in the visual representation of the row. Then, in a loop based on the number of row cells in the table model, assign the corresponding visual representation object of the cell to the cell area.

A Method That Deletes the Specified Row Area and a Cell With the Corresponding Index:

//+------------------------------------------------------------------+
//| CTableRowView::Delete the specified row region                   |
//| and the cell with the corresponding index                        |
//+------------------------------------------------------------------+
bool CTableRowView::BoundCellDelete(const int index)
  {
   if(!this.m_list_cells.Delete(index))
      return false;
   return this.m_list_bounds.Delete(index);
  }

If the cell object is successfully deleted from the list, also delete the corresponding area from the list of areas.

In the method that draws the row view, check whether the row goes beyond the container:

//+------------------------------------------------------------------+
//| CTableRowView::Draw the appearance                               |
//+------------------------------------------------------------------+
void CTableRowView::Draw(const bool chart_redraw)
  {
//--- If the row is outside the container, leave
   if(this.IsOutOfContainer())
      return;

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

A row that is not in the container’s visible area should not be drawn.

When changing the column width, you should change the width of all the cells corresponding to the column and shift the adjacent cells to new coordinates. To do this, implement a new method.

A Method That Recalculates Cell Areas:

//+------------------------------------------------------------------+
//| CTableRowView::Recalculate cell areas                            |
//+------------------------------------------------------------------+
bool CTableRowView::RecalculateBounds(CListElm *list_bounds)
  {
//--- Check the list
   if(list_bounds==NULL)
      return false;

//--- In the loop by the number of areas in the list
   for(int i=0;i<list_bounds.Total();i++)
     {
      //--- get the next header area and the corresponding cell area
      CBound *capt_bound=list_bounds.GetNodeAtIndex(i);
      CBound *cell_bound=this.GetBoundAt(i);
      if(capt_bound==NULL || cell_bound==NULL)
         return false;

      //--- Set the coordinate and size of the header area in the cell area 
      cell_bound.SetX(capt_bound.X());
      cell_bound.ResizeW(capt_bound.Width());
      
      //--- Get the attached cell object from the cell area
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      if(cell_view==NULL)
         return false;

      //--- Set the coordinate and size of the cell area in the cell's visual representation object
      cell_view.BoundSetX(cell_bound.X());
      cell_view.BoundResizeW(cell_bound.Width());
     }
//--- All is successful
   return true;
  }

A list of column header areas is passed to the method. Regarding properties of column areas from the passed list, areas of table cells change — their sizes and coordinates. Both the new dimensions and coordinates of the areas are set to their corresponding cell objects.

In the MVC paradigm, the table header is not just a table part, but is a separate control using which you can effect and control the view of table columns. The column header is also a control element in this context. In the context of this development, the column header allows changing the column size, setting the same properties for all column cells, etc. 

Thus, the column header class and the table header class have accumulated the main improvements that make it possible to "revive" the table.

Refine the visual representation class of the table column header CColumnCaptionView. 

The column header, as a control, will control the size (width) of the column and the sorting direction, or absence thereof. When clicking on the header, an arrow will appear on it indicating the sorting direction. If there is no sorting (if you click on another header), the arrow does not appear. If you hover the cursor over the header’s right bound, the cursor will have an tooltip arrow indicating the shift direction. Whereas, if you hold down the mouse button and drag the edge, the column width will change, and the position of columns on the right will be adjusted — they will shift, following the right edge of the column being resized.

Declare new variables and methods:

//+------------------------------------------------------------------+
//| Class for visual representation of table column header           |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CButton
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Pointer to the column header model
   CBound           *m_bound_node;                                   // Pointer to the header area
   int               m_index;                                        // Index in the column list
   ENUM_TABLE_SORT_MODE m_sort_mode;                                 // Table column sorting mode
   
//--- Add hint objects with arrows to the list
   virtual bool      AddHintsArrowed(void);
//--- Displays the resize cursor
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
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 area of the header the object is assigned to
   void              AssignBoundNode(CBound *bound)                     { this.m_bound_node=bound;                   }
   CBound           *GetBoundNode(void)                                 { return this.m_bound_node;                  }

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

//--- (1) Set and (2) return the sorting mode
   void              SetSortMode(const ENUM_TABLE_SORT_MODE mode)       { this.m_sort_mode=mode;                     }
   ENUM_TABLE_SORT_MODE SortMode(void)                            const { return this.m_sort_mode;                   }
   
//--- Set the reverse sorting direction
   void              SetSortModeReverse(void);
   
//--- Draw (1) the appearance and (2) sorting direction arrow
   virtual void      Draw(const bool chart_redraw);
protected:
   void              DrawSortModeArrow(void);
public:  
//--- Handler for resizing an element by the right side
   virtual bool      ResizeZoneRightHandler(const int x, const int y);
   
//--- Handlers for resizing the element by sides and corners
   virtual bool      ResizeZoneLeftHandler(const int x, const int y)       { return false;                           }
   virtual bool      ResizeZoneTopHandler(const int x, const int y)        { return false;                           }
   virtual bool      ResizeZoneBottomHandler(const int x, const int y)     { return false;                           }
   virtual bool      ResizeZoneLeftTopHandler(const int x, const int y)    { return false;                           }
   virtual bool      ResizeZoneRightTopHandler(const int x, const int y)   { return false;                           }
   virtual bool      ResizeZoneLeftBottomHandler(const int x, const int y) { return false;                           }
   virtual bool      ResizeZoneRightBottomHandler(const int x, const int y){ return false;                           }
   
//--- Change the object width
   virtual bool      ResizeW(const int w);
   
//--- Mouse button click event handler (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);
   
//--- 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_VIEW);}
  
//--- 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){}
  };

Discuss improvements to already written class methods and implementation of new ones.

In the constructor, set the default sorting as missing:

//+------------------------------------------------------------------+
//| 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), m_sort_mode(TABLE_SORT_MODE_NONE)
  {
//--- 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), m_sort_mode(TABLE_SORT_MODE_NONE)
  {
//--- Initialization
   this.Init(text);
   this.SetID(0);
  }

In the initialization method, set a feature of resizing, impossibility of moving an object, and set a display area for the sorting direction arrow:

//+------------------------------------------------------------------+
//| 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();
//--- It is possible to resize
   this.SetResizable(true);
   this.SetMovable(false);
   this.SetImageBound(this.ObjectWidth()-14,4,8,11);
  }

In the method for drawing the view, check that the object is not outside the container and call the method for drawing the sorting arrow:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Draw the appearance                          |
//+------------------------------------------------------------------+
void CColumnCaptionView::Draw(const bool chart_redraw)
  {
//--- If the object is outside its container, leave
   if(this.IsOutOfContainer())
      return;

//--- Fill the object 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);
      
//--- Draw sorting direction arrows
   this.DrawSortModeArrow();

//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

A Method That Draws a Sorting Direction Arrow:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Draw the sorting direction arrow             |
//+------------------------------------------------------------------+
void CColumnCaptionView::DrawSortModeArrow(void)
  {
//--- Set the arrow color for the normal and disabled object states
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   switch(this.m_sort_mode)
     {
      //--- Sort ascending
      case TABLE_SORT_MODE_ASC   :  
         //--- Clear the drawing area and draw the down arrow
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         this.m_painter.ArrowDown(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
         break;
      //--- Sort descending
      case TABLE_SORT_MODE_DESC  :  
         //--- Clear the drawing area and draw the up arrow
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         this.m_painter.ArrowUp(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
         break;
      //--- No sorting
      default : 
         //--- 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);
         break;
     }
  }

Depending on the sorting direction, draw the corresponding arrow:

  • in ascending order — down arrow, 
  • in descending order — up arrow, 
  • no sorting — we just clear the arrow drawing.

A Method That Reverses the Sorting Direction:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Reverse the sorting direction                |
//+------------------------------------------------------------------+
void CColumnCaptionView::SetSortModeReverse(void)
  {
   switch(this.m_sort_mode)
     {
      case TABLE_SORT_MODE_ASC   :  this.m_sort_mode=TABLE_SORT_MODE_DESC; break;
      case TABLE_SORT_MODE_DESC  :  this.m_sort_mode=TABLE_SORT_MODE_ASC;  break;
      default                    :  break;
     }
  }

If the current sorting is set in ascending order, then set it in descending order, and vice versa. Set the missing sorting in another method, since this method is intended only for switching the sorting when clicking on the column header.

A Method That Adds Arrow Hint Objects To the List:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Add hint objects                             |
//| with arrows to the list                                          |
//+------------------------------------------------------------------+
bool CColumnCaptionView::AddHintsArrowed(void)
  {
//--- Create a hint for the horizontal offset arrow
   CVisualHint *hint=this.CreateAndAddNewHint(HINT_TYPE_ARROW_SHIFT_HORZ,DEF_HINT_NAME_SHIFT_HORZ,18,18);
   if(hint==NULL)
      return false;

//--- Set the size of the hint image area
   hint.SetImageBound(0,0,hint.Width(),hint.Height());
   
//--- hide the hint and draw the appearance
   hint.Hide(false);
   hint.Draw(false);
   
//--- All is successful
   return true;
  }

A new tooltip is created and added to the list. Then you can get it from the list by name.

A Method That Displays the Resizing Cursor:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Display the resize cursor                    |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y)
  {
   CVisualHint *hint=NULL;          // Pointer to the hint
   int hint_shift_x=0;              // Hint offset by X
   int hint_shift_y=0;              // Hint offset by Y
   
//--- Depending on the location of the cursor on the element borders
//--- specify the tooltip offsets relative to the cursor coordinates,
//--- display the required hint on the chart and get the pointer to this object
   if(edge!=CURSOR_REGION_RIGHT)
      return false;
   
   hint_shift_x=-8;
   hint_shift_y=-12;
   this.ShowHintArrowed(HINT_TYPE_ARROW_SHIFT_HORZ,x+hint_shift_x,y+hint_shift_y);
   hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ);

//--- Return the result of adjusting the position of the tooltip relative to the cursor
   return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false);
  }

This method only works when the cursor is over the right edge of the element. It displays the tooltip and moves it to the cursor position with the set shift.

A Handler For Resizing With the Right Edge:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Right edge resize handler                    |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeZoneRightHandler(const int x,const int y)
  {
//--- Calculate and set the new element width
   int width=::fmax(x-this.X()+1,DEF_TABLE_COLUMN_MIN_W);
   if(!this.ResizeW(width))
      return false;
//--- Get the pointer to the hint
   CVisualHint *hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ);
   if(hint==NULL)
      return false;
//--- Shift the hint by the specified values relative to the cursor
   int shift_x=-8;
   int shift_y=-12;
   
   CTableHeaderView *header=this.m_container;
   if(header==NULL)
      return false;
   
   bool res=header.RecalculateBounds(this.GetBoundNode(),this.Width());
   res &=hint.Move(x+shift_x,y+shift_y);
   if(res)
      ::ChartRedraw(this.m_chart_id);
   return res;
  }

When dragging the right edge of an element, it should change its width depending on cursor shift direction. First, the width of the element changes, then a tooltip is displayed, and the column header area recalculate method RecalculateBounds() of the table header is called. This method sets a new size for the area of the modified header and shifts all adjacent column header areas to new coordinates.

A Virtual Method That Changes the Object Width:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Change the object width                      |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeW(const int w)
  {
   if(!CCanvasBase::ResizeW(w))
      return false;
//--- Clear the drawing area in the previous location
   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 a new drawing area
   this.SetImageBound(this.Width()-14,4,8,11);
   return true;
  }

When resizing the column header, it is necessary to shift the drawing area (the arrows of the sorting direction) so that the arrow is drawn in the right place near the right edge of the element.

Mouse Button Click Event Handler:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Mouse button click event handler             |
//+------------------------------------------------------------------+
void CColumnCaptionView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- If the mouse button is released in the drag area of the right edge of the element, leave
   if(this.ResizeRegion()==CURSOR_REGION_RIGHT)
      return;
//--- Change the sort direction arrow to the opposite and call the mouse click handler
   this.SetSortModeReverse();
   CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
  }

If a click event is caused by releasing the mouse button in the dragging area, it means that the action to resize the header has just been completed — we are leaving. Next, we change the arrow of sorting direction to a reverse one and call the event handler for mouse buttons of the base object.

In the methods of working with files, save and load the value of sorting direction:

//+------------------------------------------------------------------+
//| 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;
//--- Save the sorting direction
   if(::FileWriteInteger(file_handle,this.m_sort_mode,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);
//--- Load the sorting direction
   this.m_id=this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE);
   
//--- All is successful
   return true;
  }

Now, finalize the visual representation class of the table header CTableHeaderView.

An object of this class contains a list of column headers that this object manages, giving the user control over the view of table columns and sorting by any of them.

Declare new class methods:

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

//--- Recalculate the header areas
   bool              RecalculateBounds(CBound *bound,int new_width);

//--- 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);
   
//--- Set the sorting flag for the column header
   void              SetSortedColumnCaption(const uint index);

//--- Get the column header (1) by index and (2) with sorting flag
   CColumnCaptionView *GetColumnCaption(const uint index);
   CColumnCaptionView *GetSortedColumnCaption(void);
//--- Return the column header index with the sorting flag
   int               IndexSortedColumnCaption(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 { return CPanel::Compare(node,mode);      }
   virtual bool      Save(const int file_handle)                        { return CPanel::Save(file_handle);       }
   virtual bool      Load(const int file_handle)                        { return CPanel::Load(file_handle);       }
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_HEADER_VIEW); }
   
//--- Handler for the element user event when clicking on the object area
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
  
//--- 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){}
  };

Consider improvements to the existing methods and implementation of declared ones.

In the method that sets the header model, we will calculate the width of headers subject to the minimum header width, set the header identifier correctly, set the area corresponding to it in the header, and set the ascending sorting flag for the very first header:

//+------------------------------------------------------------------+
//| 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)::fmax(::round((double)this.Width()/(double)total),DEF_TABLE_COLUMN_MIN_W);

//--- 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;
      caption_bound.SetID(i);
      //--- 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);
      caption_view.AssignBoundNode(caption_bound);
      
      //--- For the very first header, set the sorting flag in ascending order
      if(i==0)
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
     }
//--- All is successful
   return true;
  }

A Method That Recalculates Header Areas:

//+------------------------------------------------------------------+
//| CTableHeaderView::Recalculate the header areas                   |
//+------------------------------------------------------------------+
bool CTableHeaderView::RecalculateBounds(CBound *bound,int new_width)
  {
//--- If an empty area object is passed or its width has not changed, return 'false'
   if(bound==NULL || bound.Width()==new_width)
      return false;
      
//--- Get the area index in the list
   int index=this.m_list_bounds.IndexOf(bound);
   if(index==WRONG_VALUE)
      return false;

//--- Calculate the offset and, if it is absent, return 'false'
   int delta=new_width-bound.Width();
   if(delta==0)
      return false;

//--- Change the width of the current area and the object assigned to it
   bound.ResizeW(new_width);
   CElementBase *assigned_obj=bound.GetAssignedObj();
   if(assigned_obj!=NULL)
      assigned_obj.ResizeW(new_width);

//--- Get the next area after the current one
   CBound *next_bound=this.m_list_bounds.GetNextNode();
//--- Recalculate the X coordinates for all subsequent areas
   while(!::IsStopped() && next_bound!=NULL)
     {
      //--- Shift the region by delta
      int new_x = next_bound.X()+delta;
      int prev_width=next_bound.Width();
      next_bound.SetX(new_x);
      next_bound.Resize(prev_width,next_bound.Height());
      
      //--- If there is an assigned object in the area, update its position
      CElementBase *assigned_obj=next_bound.GetAssignedObj();
      if(assigned_obj!=NULL)
        {
         assigned_obj.Move(assigned_obj.X()+delta,assigned_obj.Y());
         
         //--- This block of code is part of the effort to find and fix artifacts when dragging headers
         CCanvasBase *base_obj=assigned_obj.GetContainer();
         if(base_obj!=NULL)
           {
            if(assigned_obj.X()>base_obj.ContainerLimitRight())
               assigned_obj.Hide(false);
            else
               assigned_obj.Show(false);
           }
        }
      //--- Move on to the next area
      next_bound=this.m_list_bounds.GetNextNode();
     }
     
//--- Calculate the new width of the table header based on the width of the column headers
   int header_width=0;
   for(int i=0;i<this.m_list_bounds.Total();i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL)
         header_width+=bound.Width();
     }

//--- If the calculated width of the table header differs from the current one, change the width
   if(header_width!=this.Width())
     {
      if(!this.ResizeW(header_width))
         return false;
     }

//--- Get the pointer to the table object (View)
   CTableView *table_view=this.GetContainer();
   if(table_view==NULL)
      return false;

//--- Get a pointer to the panel with table rows from the table object
   CPanel *table_area=table_view.GetTableArea();
   if(table_area==NULL)
      return false;
   
//--- Resize  the table row panel to fit the overall size of the column headers
   if(!table_area.ResizeW(header_width))
      return false;
   
//--- Get a list of table rows and loop through all the rows
   CListElm *list=table_area.GetListAttachedElements();
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the next table row
      CTableRowView *row=table_area.GetAttachedElementAt(i);
      if(row!=NULL)
        {
         //--- Change the row size to fit the panel size and recalculate the cell areas
         row.ResizeW(table_area.Width());
         row.RecalculateBounds(&this.m_list_bounds);
        }
     }
//--- Redraw all table rows
   table_area.Draw(false);
   return true;
  }

The method's logic is explained in the comments. A pointer to the changed header area and its new width is passed to the method. The size of the area and the header assigned to it change, and starting from the next area in the list, each subsequent area is shifted to a new coordinate along with the headers assigned to them. Upon completion of the shift of all areas, a new size of the table header is calculated based on the widths of all the column header areas included in it. Subsequently, this new size affects the width of the table’s horizontal scrollbar. According to the new size of the table header, the width of the table row panel is set, all rows are resized to the new panel size, and all cell areas are recalculated for them according to the list of table header areas. As a result, the table is redrawn (the part of it visible in the container).

A Method That Sets a Sort Flag To the Column Header:

//+------------------------------------------------------------------+
//| CTableHeaderView::Set the sorting flag for the column header     |
//+------------------------------------------------------------------+
void CTableHeaderView::SetSortedColumnCaption(const uint index)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next column header and
      //--- get the attached column header object from it
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
      if(caption_view==NULL)
         continue;
      
      //--- If the loop index is equal to the required index, set the ascending sort flag
      if(i==index)
        {
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
         caption_view.Draw(false);
        }
      //--- Otherwise, reset the sorting flag
      else
        {
         caption_view.SetSortMode(TABLE_SORT_MODE_NONE);
         caption_view.Draw(false);
        }
     }
   this.Draw(true);
  }

In the loop, we get the header object from the list of column header areas. If this is a header you are looking for by index, set the ascending sort flag for it, otherwise, uncheck the sort flag. Thus, the sort flag will be unchecked for all column headers, and ascending sorting will be set for the one indicated by index. In other words, when clicking on a column header, it will be sorted in ascending order. When clicking on the same header again, it will be set to sort in descending order, but yet in the handler of click event on the element.

A Method That Returns a Column Header By Index:

//+------------------------------------------------------------------+
//| CTableHeaderView::Get the column header by index                 |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::GetColumnCaption(const uint index)
  {
//--- Get the column header area by index
   CBound *capt_bound=this.GetBoundAt(index);
   if(capt_bound==NULL)
      return NULL;
//--- Return a pointer to the attached column header object from the column header area
   return capt_bound.GetAssignedObj();
  }

A Method That Returns a Column Header With the Sort Flag:

//+------------------------------------------------------------------+
//| CTableHeaderView::Get the column header with the sorting flag    |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::GetSortedColumnCaption(void)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next column header and
      //--- get the attached column header object from it
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
      
      //--- If the object is received and it has the sorting flag set, return the pointer to it
      if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE)
         return caption_view;
     }
   return NULL;
  }

A Method That Returns Index Of a Sorted Column:

//+------------------------------------------------------------------+
//| CTableHeaderView::Return the index of the sorted column          |
//+------------------------------------------------------------------+
int CTableHeaderView::IndexSortedColumnCaption(void)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next column header and
      //--- get the attached column header object from it
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
     
      //--- If the object is received and it has the sorting flag set, return the area index
      if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE)
         return i;
     }
   return WRONG_VALUE;
  }

The three above methods, in a simple loop search for a column header with the desired flag, or by the desired index, and return the data which correspond to the method.

The handler of a custom element event when clicking in the object area:

//+------------------------------------------------------------------+
//| CTableHeaderView::Element custom event handler                   |
//| when clicking on an object area                                  |
//+------------------------------------------------------------------+
void CTableHeaderView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Get the name of the table header object from sparam
   int len=::StringLen(this.NameFG());
   string header_str=::StringSubstr(sparam,0,len);
//--- If the retrieved name does not match the name of this object, it is not our event, we leave
   if(header_str!=this.NameFG())
      return;
   
//--- Find the column header index in sparam
   string capt_str=::StringSubstr(sparam,len+1);
   string index_str=::StringSubstr(capt_str,5,capt_str.Length()-7);
//--- Failed to find the index in the row - leave
   if(index_str=="")
      return;
//--- Set the column header index
   int index=(int)::StringToInteger(index_str);
   
//--- Get the column header by index
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- If the header does not have a sorting flag, set the ascending sorting flag
   if(caption.SortMode()==TABLE_SORT_MODE_NONE)
     {
      this.SetSortedColumnCaption(index);
     }
//--- Send a custom event to the chart with the header index in lparam, the sorting mode in dparam, and the object name in sparam
//--- Since the standard OBJECT_CLICK event passes cursor coordinates in lparam and dparam, we will pass negative values here
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, -(1000+index), -(1000+caption.SortMode()), this.NameFG());
   ::ChartRedraw(this.m_chart_id);
  }

The method sends a custom event to enable sorting by column. Since cursor coordinates are passed to lparam and dparam in the event of clicking on an object, in order to understand that this is an event for activating sorting, we will pass a negative value to lparam and dparam :

  • To lparam  (-1000) + column index,
  • To dparam (-1000) + sorting type.

Finalize the table visual representation class CTableView.

The class is a full-fledged "Table" control. Improvements will include enabling functionality to change column width and sort by selected column in the table.

Declare new class methods:

//+------------------------------------------------------------------+
//| 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, (2) a table object and update the modified table from the model
   bool              CreateHeader(void);
   bool              CreateTable(void);
   bool              UpdateTable(void);
   
public:
//--- (1) Set and (2) return the table object
   bool              TableObjectAssign(CTable *table_obj);
   CTable           *GetTableObj(void)                                  { return this.m_table_obj;             }

//--- Return (1) the header, (2) the table layout area and (3) the table area container
   CTableHeaderView *GetHeader(void)                                    { return this.m_header_view;           }
   CPanel           *GetTableArea(void)                                 { return this.m_table_area;            }
   CContainer       *GetTableAreaContainer(void)                        { return this.m_table_area_container;  }

//--- 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);
   
//--- Get the column header (1) by index and (2) with sorting flag
   CColumnCaptionView *GetColumnCaption(const uint index)
                       { return(this.GetHeader()!=NULL ? this.GetHeader().GetColumnCaption(index) : NULL);     }
   CColumnCaptionView *GetSortedColumnCaption(void)
                       { return(this.GetHeader()!=NULL ? this.GetHeader().GetSortedColumnCaption(): NULL);     }

//--- Returns the visual representation object of the specified (1) row and (2) cell
   CTableRowView    *GetRowView(const uint index)
                       { return(this.GetTableArea()!=NULL ? this.GetTableArea().GetAttachedElementAt(index) : NULL); }
   CTableCellView   *GetCellView(const uint row,const uint col)
                       { return(this.GetRowView(row)!=NULL ? this.GetRowView(row).GetCellView(col) : NULL);    }
                       
//--- Return the number of table rows
   int               RowsTotal(void)
                       { return(this.GetTableArea()!=NULL ? this.GetTableArea().AttachedElementsTotal() : 0);  }

//--- 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_VIEW);     }
   
//--- Handler for the element user event when clicking on the object area
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Sort the table by column value and direction
   bool              Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode);
  
//--- 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){}
  };

Consider the declared methods.

A Method That Updates the Modified Table:

//+------------------------------------------------------------------+
//| CTableView::Updates the modified table                           |
//+------------------------------------------------------------------+
bool CTableView::UpdateTable(void)
  {
   if(this.m_table_area==NULL)
      return false;
   
   int total_model=(int)this.m_table_model.RowsTotal();        // Number of rows in the model
   int total_view =this.m_table_area.AttachedElementsTotal();  // Number of rows in the visual representation
   int diff=total_model-total_view;                            // Difference in the number of rows of two components
   int y=1;                                                    // Vertical offset
   int table_height=0;                                         // Calculated panel height
   CTableRowView *row=NULL;                                    // Pointer to the visual representation of the row
   
//--- If there are more rows in the model than in the visual representation, create the missing rows in the visual representation at the end of the list
   if(diff>0)
     {
      //--- Get the last row of the visual representation of the table (added rows will be placed based on its coordinates)
      row=this.m_table_area.GetAttachedElementAt(total_view-1);
      //--- In the loop by the number of missing rows
      for(int i=total_view;i<total_view+diff;i++)
        {
         //--- create and attach the number of objects of visual representation of the table row to the diff panel
         row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW_VIEW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H);
         if(row==NULL)
            return false;
        }
     }
 
//--- If there are more rows in the visual representation than in the model, remove the extra rows in the visual representation at the end of the list
   if(diff<0)
     {
      CListElm *list=this.m_table_area.GetListAttachedElements();
      if(list==NULL)
         return false;
      
      int  start=total_view-1;
      int  end=start-diff;
      bool res=true;
      for(int i=start;i>end;i--)
         res &=list.Delete(i);
      if(!res)
         return false;
     }
   
//--- In the loop through the list of rows of the table model
   for(int i=0;i<total_model;i++)
     {
      //--- get the next object of visual representation of the table row from the list of the rows panel
      row=this.m_table_area.GetAttachedElementAt(i);
      if(row==NULL)
         return false;
      //--- Check the object type
      if(row.Type()!=ELEMENT_TYPE_TABLE_ROW_VIEW)
         continue;
         
      //--- 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(C'242,242,242');
      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;

      //--- Update the cells of the table row object using the row model
      row.TableRowModelUpdate(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 logic of the method is commented in the code. The number of rows in the table model and in the visual representation is compared. If there is a difference, then the missing rows of the table are either added or deleted in the visual representation. Then, in a loop through rows of the table model, cells in the visual representation of the table are updated. After updating all the rows of the visual representation, the result of resizing the table for the new number of rows is returned.

The handler of a custom element event when clicking in the object area:

//+------------------------------------------------------------------+
//| CTableView::Element custom event handler                         |
//| when clicking on an object area                                  |
//+------------------------------------------------------------------+
void CTableView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam)
  {
   if(id==CHARTEVENT_OBJECT_CLICK && lparam>=0 && dparam>=0)
      return;
      
//--- Get the name of the table header object from sparam
   int len=::StringLen(this.NameFG());
   string header_str=::StringSubstr(sparam,0,len);
//--- If the retrieved name does not match the name of this object, it is not our event, we leave
   if(header_str!=this.NameFG())
      return;
   
//--- Set the column header index
//--- Since the standard OBJECT_CLICK event passes cursor coordinates in lparam and dparam,
//---  a negative value of the header index (the event occurred for) is passed for the handler
   int index=(int)::fabs(lparam+1000);
   
//--- Get the column header by index
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- Sort the list of rows by the sort value in the column header and update the table
   this.Sort(index,caption.SortMode());
   if(this.UpdateTable())
      this.Draw(true);
  }

The method does not process events if lparam and dparam are not less than zero. Next, we get the column index from lparam and sort the table according to the sorting mode specified in the resulting column header. After that the table is updated.

We have finished refining classes. Now, make a new class to create tables from preliminary prepared data quite comfortably. And table management will also be implemented in the same class. Thus, and based on the purpose, it will be a table management class. One object of this class can contain many different tables, which can be accessed by the table ID or by its user name.


Class For Simplified Table Creation

Continue writing the code in \MQL5\Indicators\Tables\Controls\Controls.mqh.

//+------------------------------------------------------------------+
//| Table management class                                           |
//+------------------------------------------------------------------+
class CTableControl : public CPanel
  {
protected:
   CListObj          m_list_table_model;
//--- Add an object (1) of the model (CTable) and (2) table visual representation (CTableView) to the list
   bool              TableModelAdd(CTable *table_model,const int table_id,const string source);
   CTableView       *TableViewAdd(CTable *table_model,const string source);
//--- Update the specified column of the specified table
   bool              ColumnUpdate(const string source, CTable *table_model, const uint table, const uint col, const bool cells_redraw);
   
public:
//--- Returns (1) the model, (2) the table visual representation object and (3) the object type
   CTable           *GetTable(const uint index)                   { return this.m_list_table_model.GetNodeAtIndex(index);  }
   CTableView       *GetTableView(const uint index)               { return this.GetAttachedElementAt(index);               }
   
//--- Create a table based on the passed data
template<typename T>
   CTableView       *TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const uint num_rows, const uint num_columns,const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE);
   
//--- Return (1) the string value of the specified cell (Model), the specified (2) string and (3) the table cell (View)
   string            CellValueAt(const uint table, const uint row, const uint col);
   CTableRowView    *GetRowView(const uint table, const uint index);
   CTableCellView   *GetCellView(const uint table, const uint row, const uint col);
   
//--- Set the (1) value, (2) accuracy, (3) time display flags and (4) color name display flag to the specified cell (Model + View)
template<typename T>
   void              CellSetValue(const uint table, const uint row, const uint col, const T value, const bool chart_redraw);
   void              CellSetDigits(const uint table, const uint row, const uint col, const int digits, const bool chart_redraw);
   void              CellSetTimeFlags(const uint table, const uint row, const uint col, const uint flags, const bool chart_redraw);
   void              CellSetColorNamesFlag(const uint table, const uint row, const uint col, const bool flag, const bool chart_redraw);

//--- Set the foreground color to the specified cell (View)
   void              CellSetForeColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw);
   
//--- (1) Set and (2) return the text anchor point in the specified cell (View)
   void              CellSetTextAnchor(const uint table, const uint row, const uint col, const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw);
   ENUM_ANCHOR_POINT CellTextAnchor(const uint table, const uint row, const uint col);
   
//--- Set the (1) accuracy, (2) time display flags, (3) color name display flag, (4) text anchor point and (5) data type in the specified column (View)
   void              ColumnSetDigits(const uint table, const uint col, const int digits, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetTimeFlags(const uint table, const uint col, const uint flags, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetColorNamesFlag(const uint table, const uint col, const bool flag, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetTextAnchor(const uint table, const uint col, const ENUM_ANCHOR_POINT anchor, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetDatatype(const uint table, const uint col, const ENUM_DATATYPE type, const bool cells_redraw, const bool chart_redraw);

//--- Object type
   virtual int       Type(void)                             const { return(ELEMENT_TYPE_TABLE_CONTROL_VIEW);               }

//--- Constructors/destructor
                     CTableControl(void) { this.m_list_table_model.Clear(); }
                     CTableControl(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableControl(void) {}
  };

The class is a Panel on which tables (one or more) are placed, and consists of two lists:

  1. a list of table models,
  2. a list of visual representations of tables created based on the corresponding models.

The class methods allow getting the desired table and manage its view, rows, columns, and cells.

Consider the declared class methods.

The list of table models is cleared in the class constructor and the default name is set:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTableControl::CTableControl(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,"",chart_id,wnd,x,y,w,h)
  {
   this.m_list_table_model.Clear();
   this.SetName("Table Control");
  }

After creating an object, and if more than one object of this class is planned, then you should set your own unique name so that there are no two or more elements with the same name in the list of objects attached to the panel.

A method that adds a table model object to the list:

//+------------------------------------------------------------------+
//| Add the table model (CTable) object to the list                  |
//+------------------------------------------------------------------+
bool CTableControl::TableModelAdd(CTable *table_model,const int table_id,const string source)
  {
//--- Check the table model object
   if(table_model==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to create Table Model object",source,__FUNCTION__);
      return false;
     }
//--- Set the ID to the table model - either by the size of the list or the specified one
   table_model.SetID(table_id<0 ? this.m_list_table_model.Total() : table_id);
//--- If a table model with the specified ID is in the list, report this, delete the object, and return 'false'
   this.m_list_table_model.Sort(0);
   if(this.m_list_table_model.Search(table_model)!=NULL)
     {
      ::PrintFormat("%s::%s: Error: Table Model object with ID %d already exists in the list",source,__FUNCTION__,table_id);
      delete table_model;
      return false;
     }
//--- If the table model is not added to the list, report this, delete the object, and return 'false'
   if(this.m_list_table_model.Add(table_model)<0)
     {
      ::PrintFormat("%s::%s: Error. Failed to add Table Model object to list",source,__FUNCTION__);
      delete table_model;
      return false;
     }
//--- All is successful
   return true;
  }

A Method That Creates a New Object Of Table Visual Representation And Adds It To the list:

//+------------------------------------------------------------------+
//| Create a new object and add it to the list                       |
//| of the table visual representation (CTableView)                  |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableViewAdd(CTable *table_model,const string source)
  {
//--- Check the table model object
   if(table_model==NULL)
     {
      ::PrintFormat("%s::%s: Error. An invalid Table Model object was passed",source,__FUNCTION__);
      return NULL;
     }
//--- Create a new element - a visual representation of the table attached to the panel
   CTableView *table_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_VIEW,"","TableView"+(string)table_model.ID(),1,1,this.Width()-2,this.Height()-2);
   if(table_view==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to create Table View object",source,__FUNCTION__);
      return NULL;
     }
//--- Assign the table object (Model) and its ID to the Table graphical element (Model)
   table_view.TableObjectAssign(table_model);
   table_view.SetID(table_model.ID());
   return table_view;
  }

Both of the above methods are used in table creation methods.

A Method That Creates a Table With Specification Of a Tables Array And a Headers Array:

//+-------------------------------------------------------------------+
//| Create a table while specifying a table array and a header array. | 
//| Defines the index and names of columns according to column_names  |
//| The number of rows is determined by the size of the row_data array|
//| also used to fill the table                                       |
//+-------------------------------------------------------------------+
template<typename T>
CTableView *CTableControl::TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE)
  {
//--- Create a table object using the specified parameters
   CTable *table_model=new CTable(row_data,column_names);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

A Method That Creates a Table With Definition Of a Number Of Columns And Rows:

//+------------------------------------------------------------------+
//| Create a table with a specified number of columns and rows.      |
//| The columns will have Excel names "A", "B", "C", etc.            |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const uint num_rows,const uint num_columns,const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(num_rows,num_columns);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

A Method That Creates a Table From the Matrix:

//+-------------------------------------------------------------------------------------+
//| Create a table with column initialization according to column_names                 |
//| The number of rows is determined by the row_data parameter, with the 'matrix' type  |
//+-------------------------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(row_data,column_names);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

A Method That Creates a Table Based On a List Of User Parameters And an Array Of Column Headers:

//+------------------------------------------------------------------+
//| Create a table with the specified table array based on the       |
//| row_data list containing objects with structure field data.      | 
//| Define the index and names of columns according to               |
//| column names in column_names                                     |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE)
  {
   CTableByParam *table_model=new CTableByParam(row_data,column_names);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

The logic of all the presented methods for creating a table is the same: first, based on method’s input parameters, a table model object is created and added to the list, and then a visual representation. These are the basic methods for creating tables from a wide range of data. The table created by the considered methods is located on the panel, which is the substrate for placing the tables being created.

A Method That Sets Values To the Specified Cell:

//+------------------------------------------------------------------+
//| Set the value to the specified cell (Model + View)               |
//+------------------------------------------------------------------+
template<typename T>
void CTableControl::CellSetValue(const uint table,const uint row,const uint col,const T value,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
      
//--- Compare the value set in the cell with the passed one
   bool equal=false;
   ENUM_DATATYPE datatype=cell_model.Datatype();
   switch(datatype)
     {
      case TYPE_LONG    :  
      case TYPE_DATETIME:  
      case TYPE_COLOR   :  equal=(cell_model.ValueL()==value);                                           break;
      case TYPE_DOUBLE  :  equal=(::NormalizeDouble(cell_model.ValueD()-value,cell_model.Digits())==0);  break;
      //---TYPE_STRING
      default           :  equal=(::StringCompare(cell_model.ValueS(),(string)value)==0);                break;
     }
//--- If the values are equal, leave
   if(equal)
      return;
      
//--- Set a new value in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetValue(row,col,value);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

The method's logic is explained in the comments to the code. If a cell has the same value as that passed to the method for entering to the cell, leave the method. A new value is first written to the table model cell, then to a cell of table visual representation, and this cell is redrawn.

A Method That Sets Accuracy Of Displaying Fractional Numbers In the Indicated Cell:

//+------------------------------------------------------------------+
//| Set the accuracy to the specified cell (Model + View)            |
//+------------------------------------------------------------------+
void CTableControl::CellSetDigits(const uint table,const uint row,const uint col,const int digits,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.Digits()==digits)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set a new accuracy value in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetDigits(row,col,digits);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

The method is similar to the above. If the specified precision of fractional numbers is equal to the actual one, leave the method. Next, the accuracy is recorded in the cell model, the text from the cell model is recorded in the visual representation (for fractional numbers, the accuracy has already been changed), and the cell is redrawn.

A Method That Sets Time Display Flags To the Specified Cell:

//+------------------------------------------------------------------+
//| Set the time display flags                                       |
//| to the specified cell (Model + View)                             |
//+------------------------------------------------------------------+
void CTableControl::CellSetTimeFlags(const uint table,const uint row,const uint col,const uint flags,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.DatetimeFlags()==flags)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set a new value for the time display flags in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetTimeFlags(row,col,flags);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

The method’s logic is identical to the method that sets accuracy to a cell.

A Method That Sets Color Name Display Flag To the Specified Cell:

//+------------------------------------------------------------------+
//| Set the flag for displaying color names                          |
//| to the specified cell (Model + View)                             |
//+------------------------------------------------------------------+
void CTableControl::CellSetColorNamesFlag(const uint table,const uint row,const uint col,const bool flag,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.ColorNameFlag()==flag)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set a new value for the color name display flag in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetColorNamesFlag(row,col,flag);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

Exactly the same method as the ones above.

A Method That Sets the Foreground Color To a Specified Cell:

//+------------------------------------------------------------------+
//| Set the foreground color to the specified cell (View)            |
//+------------------------------------------------------------------+
void CTableControl::CellSetForeColor(const uint table,const uint row,const uint col,const color clr,const bool chart_redraw)
  {
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set the cell background color in the cell visual representation object
//--- Redraw the cell with the chart update flag
   cell_view.SetForeColor(clr);
   cell_view.Draw(chart_redraw);
  }

Get the visual representation object of the cell, set a new foreground color for it, and redraw the cell with a new text color.

A Method That Sets the Anchor Point of the Text To the Specified Cell:

//+------------------------------------------------------------------+
//| Set the text anchor point to the specified cell (View)           |
//+------------------------------------------------------------------+
void CTableControl::CellSetTextAnchor(const uint table,const uint row,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw)
  {
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set the text anchor point in the cell visual representation object
//--- Redraw the cell with the chart update flag
   cell_view.SetTextAnchor(anchor,cell_redraw,chart_redraw);
  }

We get the visual representation object of the cell, set a new anchor point for it, and redraw the cell with a new text layout in it.

A Method That Returns the Anchor Point of the Text In the Specified Cell:

//+------------------------------------------------------------------+
//| Return the text anchor point in the specified cell (View)        |
//+------------------------------------------------------------------+
ENUM_ANCHOR_POINT CTableControl::CellTextAnchor(const uint table,const uint row,const uint col)
  {
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return ANCHOR_LEFT_UPPER;
   
//--- Return the text anchor point
   return((ENUM_ANCHOR_POINT)cell_view.TextAnchor());
  }

We get the visual representation object of the cell and return the text anchor point set for it.

A Method That Updates the Specified Column Of the Specified Table:

//+------------------------------------------------------------------+
//| Update the specified column of the specified table               |
//+------------------------------------------------------------------+
bool CTableControl::ColumnUpdate(const string source,CTable *table_model,const uint table,const uint col,const bool cells_redraw)
  {
//--- Check the table model
   if(::CheckPointer(table_model)==POINTER_INVALID)
     {
      ::PrintFormat("%s::%s: Error. Invalid table model pointer passed",source,__FUNCTION__);
      return false;
     }
//--- Get the table visual representation
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to get CTableView object",source,__FUNCTION__);
      return false;
     }
   
//--- In the loop by the rows of the table visual representation
   int total=table_view.RowsTotal();
   for(int i=0;i<total;i++)
     {
      //--- get the cell visual representation object in the specified column from the next table row
      CTableCellView *cell_view=this.GetCellView(table,i,col);
      if(cell_view==NULL)
        {
         ::PrintFormat("%s::%s: Error. Failed to get CTableCellView object (row %d, col %u)",source,__FUNCTION__,i,col);
         return false;
        }
      //--- Get the model of the corresponding cell from the row model
      CTableCell *cell_model=table_model.GetCell(i,col);
      if(cell_model==NULL)
        {
         ::PrintFormat("%s::%s: Error. Failed to get CTableCell object (row %d, col %u)",source,__FUNCTION__,i,col);
         return false;
        }
      
      //--- Set the value from the cell model to the cell visual representation object
      cell_view.SetText(cell_model.Value());
      //--- If specified, redraw the cell visual representation
      if(cells_redraw)
         cell_view.Draw(false);
     }
   return true;
  }

The method's logic is explained in the comments. After the table model is updated, pass it to this method. And here, in a loop through all the rows of the visual representation of the table, get each new row in turn, and from the corresponding row of the model get the specified cell. We write data from the cell model to the visual representation of the cell, and the visual representation of the cell is redrawn. This way, the entire table column is redrawn.

A Method That Sets the Accuracy In the Specified Column:

//+------------------------------------------------------------------+
//| Set the accuracy in the specified column (Model + View)          |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetDigits(const uint table,const uint col,const int digits,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set Digits for the specified column in the table model 
   table_model.ColumnSetDigits(col,digits);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

In the table model, set the specified accuracy for the column and call the table column update method discussed above.

A Method That Sets Time Display Flags To the Specified Column:

//+------------------------------------------------------------------+
//| Set the time display flags                                       |
//| in the specified column (Model + View)                           |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetTimeFlags(const uint table,const uint col,const uint flags,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set the time display flags for the specified column in the table model 
   table_model.ColumnSetTimeFlags(col,flags);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

In the table model, set the specified time display flags for the column and call the update method for the table column.

A Method That Sets Color Name Display Flag To the Specified Column:

//+------------------------------------------------------------------+
//| Set the flag for displaying color names                          |
//| in the specified column (Model + View)                           |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetColorNamesFlag(const uint table,const uint col,const bool flag,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set the time display flags for the specified column in the table model 
   table_model.ColumnSetColorNamesFlag(col,flag);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

In the table model, set the specified color name display flag for the column and call the update method for the table column.

A Method That Sets a Data Type In the Specified Column:

//+------------------------------------------------------------------+
//| Set the data type of the specified column ( (Model + View))      |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetDatatype(const uint table,const uint col,const ENUM_DATATYPE type,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set the data type for the specified column in the table model 
   table_model.ColumnSetDatatype(col,type);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

In the table model, set the specified data type for the column and call the update method for the table column.

A Method That Sets an Anchor Point of the Text In the Specified Column:

//+------------------------------------------------------------------+
//| Set the text anchor point in the specified column (View)         |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetTextAnchor(const uint table,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table visual representation
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return;
     }
//--- In a loop through all table rows
   int total=table_view.RowsTotal();
   for(int i=0;i<total;i++)
     {
      //--- get the next object of the cell visual representation
      //--- and set a new anchor point into the object
      CTableCellView *cell_view=this.GetCellView(table,i,col);
      if(cell_view!=NULL && cell_view.TextAnchor()!=anchor)
         cell_view.SetTextAnchor(anchor,cells_redraw,false);
     }
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

In a loop through all the rows of the visual representation of the table, get the specified cell from the next row and enter a new anchor point into it. At the end of the loop update the chart.

A Method That Returns the String Value Of the Specified Cell:

//+------------------------------------------------------------------+
//| Return the string value of the specified cell (Model)            |
//+------------------------------------------------------------------+
string CTableControl::CellValueAt(const uint table,const uint row,const uint col)
  {
   CTable *table_model=this.GetTable(table);
   return(table_model!=NULL ? table_model.CellValueAt(row,col) : ::StringFormat("%s: Error. Failed to get table model",__FUNCTION__));
  }

We get the required cell from the table model and return its value as a row. If an error occurs, the error text is returned.

A Method That Returns the Specified Table Row:

//+------------------------------------------------------------------+
//| Return the specified table row (View)                            |
//+------------------------------------------------------------------+
CTableRowView *CTableControl::GetRowView(const uint table,const uint index)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return NULL;
     }
   return table_view.GetRowView(index);
  }

We get a visual representation of the table by index and return a pointer to the required row from it.

A Method That Returns the Specified Table Cell:

//+------------------------------------------------------------------+
//| Return the specified table cell (View)                           |
//+------------------------------------------------------------------+
CTableCellView *CTableControl::GetCellView(const uint table,const uint row,const uint col)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return NULL;
     }
   return table_view.GetCellView(row,col);
  }

We get a visual representation of the table by index and return from it a pointer to the required cell of the specified row.

As you can see, this class is a simple auxiliary class for building tables and working with them. All of its methods represent service functionality for handling, modifying, setting, and retrieving tabular data using methods from previously written model classes and the visual representation of the table.

Let’s check what we have.


Testing the Result

Open test indicator file \MQL5\Indicators\Tables\iTestTable.mq5 and rewrite it as follows (earlier prepared data and creation of the table are highlighted in the appropriate colors):

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

//--- Pointer to the CTableControl object
CTableControl *table_ctrl;

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

//--- Create table data
//--- Declare and fill the array of column headers with the dimension of 4
   string captions[]={"Column 0","Column 1","Column 2","Column 3"};
   
//--- Declare and fill the 15x4 data array
//--- Acceptable array types: double, long, datetime, color, string
   long array[15][4]={{ 1,  2,  3,  4},
                      { 5,  6,  7,  8},
                      { 9, 10, 11, 12},
                      {13, 14, 15, 16},
                      {17, 18, 19, 20},
                      {21, 22, 23, 24},
                      {25, 26, 27, 28},
                      {29, 30, 31, 32},
                      {33, 34, 35, 36},
                      {37, 38, 39, 40},
                      {41, 42, 43, 44},
                      {45, 46, 47, 48},
                      {49, 50, 51, 52},
                      {53, 54, 55, 56},
                      {57, 58, 59, 60}};
                      
//--- Create the table control graphical element
   table_ctrl=new CTableControl("TableControl0",0,wnd,30,30,460,184);
   if(table_ctrl==NULL)
      return INIT_FAILED;

//--- The chart should have one main element
   table_ctrl.SetAsMain();

//--- It is possible to set the table control parameters
   table_ctrl.SetID(0);                      // ID 
   table_ctrl.SetName("Table Control 0");    // Name

//--- Create the table 0 object (Model + View component) from the above-created 15x4 long array and the string array of column headers
   if(table_ctrl.TableCreate(array,captions)==NULL)
      return INIT_FAILED;
      
//--- Additionally, set the text output to be centered in the cell for columns 1, 2, 3, and to be left-aligned for column 0
   table_ctrl.ColumnSetTextAnchor(0,0,ANCHOR_LEFT,true,false);
   table_ctrl.ColumnSetTextAnchor(0,1,ANCHOR_CENTER,true,false);
   table_ctrl.ColumnSetTextAnchor(0,2,ANCHOR_CENTER,true,false);
   table_ctrl.ColumnSetTextAnchor(0,3,ANCHOR_CENTER,true,false);

//--- Draw the table
   table_ctrl.Draw(true);
   
//--- Get the table with index 0 and print it in the journal
   CTable *table=table_ctrl.GetTable(0);
   table.Print();
   
//--- Successful
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom deindicator initialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Remove the table control and destroy the library's shared resource manager
   delete table_ctrl;
   CCommonManager::DestroyInstance();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for 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 table control
   table_ctrl.OnChartEvent(id,lparam,dparam,sparam);
   
//--- If the event is mouse cursor movement
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- get the cursor coordinates
      int x=table_ctrl.CursorX();
      int y=table_ctrl.CursorY();
      
      //--- set the X coordinate value to cell 0 of row 1
      table_ctrl.CellSetValue(0,1,0,x,false);
      
      //--- set the Y coordinate value to cell 1 of row 1
      //--- the text color in the cell depends on the sign of the Y coordinate (if the value is negative, the text is red
      table_ctrl.CellSetForeColor(0,1,1,(y<0 ? clrRed : table_ctrl.ForeColor()),false);
      table_ctrl.CellSetValue(0,1,1,y,true);
     }
  }
//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
//--- Call the OnTimer handler of the table control
   table_ctrl.OnTimer();
  }
//+------------------------------------------------------------------+

In the event handler, check how some data can be written to cells in real time. We will read the cursor coordinates and enter them into the first two cells of the second row from the top. Whereas, the negative value of the cursor’s Y coordinate will be displayed in red.

Compile the indicator and run it on the chart:

As you can see, the stated table - user interaction is working. 

Of course, there are some nuances when displaying the visual representation of the table during interaction with the cursor, but all this will gradually be fixed and refined.



Conclusion

At the moment, we have created a convenient tool for displaying various tabular data, which allows us to customize the display of the default table, to some extent.

There are several data options for creating tables based on them:

  • a two-dimensional array of the table and an array of column headers,
  • a number of columns and rows,
  • matrix: the header is automatically created in Excel-style,
  • a list of custom parameters CList and an array of column headers.

In the future, it is possible that table classes will be improved to conveniently add and remove rows, columns, and individual cells. But for now, stop on this format of the object for creating and displaying 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/19979

Attached files |
Tables.mqh (272.47 KB)
Base.mqh (300.19 KB)
Controls.mqh (804.81 KB)
iTestTable.mq5 (12.11 KB)
MQL5.zip (130.08 KB)
Last comments | Go to discussion (2)
Alexey Viktorov
Alexey Viktorov | 28 Oct 2025 at 16:19
Thank you Artem. This is indeed a very useful article.
Maxim Kuznetsov
Maxim Kuznetsov | 28 Oct 2025 at 18:19
MetaQuotes:
clicking on a table column header (Controller component operation) will cause a change in the data arrangement in the table model (reorganisation of the Model component),
Artem !!! In vssam-delish MVC, Clicking on a view does NOT change anything in the model. There can be many different views on one model at the same time with different sorts and selections.
Larry Williams Market Secrets (Part 2): Automating a Market Structure Trading System Larry Williams Market Secrets (Part 2): Automating a Market Structure Trading System
Learn how to automate Larry Williams market structure concepts in MQL5 by building a complete Expert Advisor that reads swing points, generates trade signals, manages risk, and applies a dynamic trailing stop strategy.
Reimagining Classic Strategies (Part 20): Modern Stochastic Oscillators Reimagining Classic Strategies (Part 20): Modern Stochastic Oscillators
This article demonstrates how the stochastic oscillator, a classical technical indicator, can be repurposed beyond its conventional use as a mean-reversion tool. By viewing the indicator through a different analytical lens, we show how familiar strategies can yield new value and support alternative trading rules, including trend-following interpretations. Ultimately, the article highlights how every technical indicator in the MetaTrader 5 terminal holds untapped potential, and how thoughtful trial and error can uncover meaningful interpretations hidden from view.
Billiards Optimization Algorithm (BOA) Billiards Optimization Algorithm (BOA)
The BOA method is inspired by the classic game of billiards and simulates the search for optimal solutions as a game with balls trying to fall into pockets representing the best results. In this article, we will consider the basics of BOA, its mathematical model, and its efficiency in solving various optimization problems.
From Novice to Expert: Navigating Market Irregularities From Novice to Expert: Navigating Market Irregularities
Market rules are continuously evolving, and many once-reliable principles gradually lose their effectiveness. What worked in the past no longer works consistently over time. Today’s discussion focuses on probability ranges and how they can be used to navigate market irregularities. We will leverage MQL5 to develop an algorithm capable of trading effectively even in the choppiest market conditions. Join this discussion to find out more.