Русский
preview
Tables in the MVC Paradigm in MQL5: Symbol Correlation Table

Tables in the MVC Paradigm in MQL5: Symbol Correlation Table

MetaTrader 5Examples |
507 0
Artyom Trishkin
Artyom Trishkin

Contents


Introduction

In the previous article, which focused on creating tables within the MVC framework, we expanded table functionality by adding the ability to adjust column widths, specify the types of data to display, and sort data by column. These improvements have made the tables more flexible and easier to use with various datasets. However, for some tasks that require data to be presented with a clear association to rows, an additional interface element is needed — a vertical header that displays the row headers.

Today, we will not create abstract examples of arbitrary tables; instead, we will create a practical example of an indicator for building a symmetric correlation table for the symbols selected in the settings, where the vertical and horizontal headers will display the symbol names, and the table itself will display the correlation values between them. To do this, let’s add a vertical header to the table, which will generally contain row labels — such as their sequence numbers or other identifiers — and, in the selected example, the symbol names. Having two table headers will make it easier to navigate the data, especially when working with tables where rows carry important meaning.

To implement this functionality, we will refine the existing classes by adding support for a vertical header and ensuring proper interaction between components within the MVC paradigm. As a result, we will have a tool that not only improves the visual representation of the data but also simplifies its analysis.


Refining the Library Classes

All project files are located at \MQL5\Indicators\Tables\. The file containing the table model classes (Tables.mqh), together with the test indicator file (iCorrelationTable.mq5), is located in the \MQL5\Indicators\Tables\ folder.

The graphics library files (Base.mqh and Controls.mqh) are located in the \MQL5\Indicators\Tables\Controls\ subfolder. All the files required for this project can be downloaded as a single archive from the previous article.

Let's refine the base class of the graphics library, \MQL5\Indicators\Tables\Controls\Base.mqh.

We will add the forward declarations for the new classes and the enumerations for the new object types:

//+------------------------------------------------------------------+
//| Included libraries                                               |
//+------------------------------------------------------------------+
#include <Canvas\Canvas.mqh>              // CCanvas
#include <Arrays\List.mqh>                // CList
#include "..\Tables.mqh"

//--- Forward declarations for control classes
class    CBoundedObj;                     // Base class that stores an object's dimensions
class    CCanvasBase;                     // Base class for the canvas of graphical elements
class    CCounter;                        // Delay counter class
class    CAutoRepeat;                     // Event auto-repeat class
class    CImagePainter;                   // Image drawing class
class    CVisualHint;                     // Tooltip class
class    CLabel;                          // Text label class
class    CButton;                         // Simple button class
class    CButtonTriggered;                // Two-state 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;                  // Table cell visual representation class
class    CTableRowView;                   // Table row visual representation class
class    CCaptionView;                    // Base object class for a header visual representation
class    CColumnCaptionView;              // Table column header visual representation class
class    CRowCaptionView;                 // Table row header visual representation class
class    CTableHeaderView;                // Table header visual representation class
class    CTableRowsHeaderView;            // Visual representation class for table row headers
class    CTableView;                      // Table visual representation class
class    CTableControl;                   // Table management class
class    CPanel;                          // Panel control class
class    CGroupBox;                       // GroupBox control class
class    CContainer;                      // Container control class

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

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE                    // Enumeration of graphical element types
  {
   ELEMENT_TYPE_BASE = 0x10000,           // Base object for graphical elements
   ELEMENT_TYPE_COLOR,                    // Color object
   ELEMENT_TYPE_COLORS_ELEMENT,           // Color set object for a graphical object element
   ELEMENT_TYPE_RECTANGLE_AREA,           // Rectangular area of an 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,             // Base object for graphical element dimensions
   ELEMENT_TYPE_CANVAS_BASE,              // Base canvas object for graphical elements
   ELEMENT_TYPE_ELEMENT_BASE,             // Base object for graphical elements
   ELEMENT_TYPE_HINT,                     // Tooltip
   ELEMENT_TYPE_LABEL,                    // Text label
   ELEMENT_TYPE_BUTTON,                   // Simple button
   ELEMENT_TYPE_BUTTON_TRIGGERED,         // Two-state 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 scrollbar thumb
   ELEMENT_TYPE_SCROLLBAR_THUMB_V,        // Vertical scrollbar thumb
   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_CAPTION_VIEW,       // Base header object (view)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW,// Table column header (view)
   ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW,   // Table row header (view)
   ELEMENT_TYPE_TABLE_HEADER_VIEW,        // Table header (view)
   ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW,   // Table row 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
  };

In the function that returns the short name of an element by type, let's add the new names for the new elements:

//+------------------------------------------------------------------+
//|  Return the short name of an element by type                     |
//+------------------------------------------------------------------+
string ElementShortName(const ENUM_ELEMENT_TYPE type)
  {
   switch(type)
     {
      case ELEMENT_TYPE_ELEMENT_BASE               :  return "BASE";    // Base object for graphical elements
      case ELEMENT_TYPE_HINT                       :  return "HNT";     // Tooltip
      case ELEMENT_TYPE_LABEL                      :  return "LBL";     // Text label
      case ELEMENT_TYPE_BUTTON                     :  return "SBTN";    // Simple button
      case ELEMENT_TYPE_BUTTON_TRIGGERED           :  return "TBTN";    // Two-state 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 scrollbar slider
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V          :  return "THMBV";   // Vertical scrollbar 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_CAPTION_VIEW         :  return "TCAPT";   // Base header object (view)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  return "TCCAPT";  // Table column header (view)
      case ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW     :  return "TRCAPT";  // Table row header (view)
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  return "TCHDR";   // Table header (view)
      case ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW     :  return "TRHDR";   // Table row headers (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 control
      case ELEMENT_TYPE_GROUPBOX                   :  return "GRBX";    // GroupBox control
      case ELEMENT_TYPE_CONTAINER                  :  return "CNTR";    // Container control
      default                                      :  return "Unknown"; // Unknown
     }
  }

In the CColorElement class for graphical object element colors, let's declare a new method that returns a color interpolated between three colors depending on the coefficient value:

//+------------------------------------------------------------------+
//| Class for graphical object element colors                        |
//+------------------------------------------------------------------+
class CColorElement : public CBaseObj
  {
protected:
   CColor            m_current;                                // Current color. Can be one of the following
   CColor            m_default;                                // Normal-state color
   CColor            m_focused;                                // Hover-state color
   CColor            m_pressed;                                // Pressed-state color
   CColor            m_blocked;                                // Disabled element color
   
//--- Convert RGB to color
   color             RGBToColor(const double r,const double g,const double b) const;
//--- Write the RGB component values to variables
   void              ColorToRGB(const color clr,double &r,double &g,double &b);
//--- Return the color component: (1) Red, (2) Green, (3) Blue
   double            GetR(const color clr)                     { return clr&0xFF;                           }
   double            GetG(const color clr)                     { return(clr>>8)&0xFF;                       }
   double            GetB(const color clr)                     { return(clr>>16)&0xFF;                      }
   
public:
//--- Return a new color
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Returns an interpolated color between three colors depending on the coefficient value (from -1 to +1)
   color             InterpolateColorByCoeff(const color color1, const color color2, const color color3, const double coeff);

//--- Class initialization
   void              Init(void);

//--- Initialize colors for different states
   bool              InitDefault(const color clr)              { return this.m_default.SetColor(clr);       }
   bool              InitFocused(const color clr)              { return this.m_focused.SetColor(clr);       }
   bool              InitPressed(const color clr)              { return this.m_pressed.SetColor(clr);       }
   bool              InitBlocked(const color clr)              { return this.m_blocked.SetColor(clr);       }
   
//--- Set colors for all states
   void              InitColors(const color clr_default, const color clr_focused, const color clr_pressed, const color clr_blocked);
   void              InitColors(const color clr);
    
//--- Return colors for different states
   color             GetCurrent(void)                    const { return this.m_current.Get();               }
   color             GetDefault(void)                    const { return this.m_default.Get();               }
   color             GetFocused(void)                    const { return this.m_focused.Get();               }
   color             GetPressed(void)                    const { return this.m_pressed.Get();               }
   color             GetBlocked(void)                    const { return this.m_blocked.Get();               }
   
//--- Set one of the colors from the list as the current color
   bool              SetCurrentAs(const ENUM_COLOR_STATE color_state);

//--- Return a description of the object
   virtual string    Description(void);
   
//--- Virtual methods for (1) saving to a file, (2) loading from a file, and (3) object type
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_COLORS_ELEMENT);       }
   
//--- Constructors/destructor
                     CColorElement(void);
                     CColorElement(const color clr);
                     CColorElement(const color clr_default,const color clr_focused,const color clr_pressed,const color clr_blocked);
                    ~CColorElement(void) {}
  };

We will write its implementation outside the class body:

//+------------------------------------------------------------------+
//| Return an interpolated color between three colors                |
//| depending on the coefficient value (from -1 to +1)               |
//+------------------------------------------------------------------+
color CColorElement::InterpolateColorByCoeff(const color color1,const color color2,const color color3,const double coeff)
  {
//--- Limit the coefficient value
   double val=::fmax(-1.0,::fmin(1.0,coeff));

//--- Variables for obtaining the RGB components of each color
   double r1, g1, b1, r2, g2, b2;
   double r, g, b, t;

//--- Interpolation between the initial and middle colors
   if(val<0.0)
     {
      this.ColorToRGB(color1,r1,g1,b1);
      this.ColorToRGB(color2,r2,g2,b2);
      t=(val+1.0)/1.0;
      r=r1+(r2-r1)*t;
      g=g1+(g2-g1)*t;
      b=b1+(b2-b1)*t;
     }
//--- Interpolation between the middle and final colors
   else
     {
      this.ColorToRGB(color3,r1,g1,b1); 
      this.ColorToRGB(color2,r2,g2,b2);
      t=val/1.0;
      r=r2+(r1-r2)*t;
      g=g2+(g1-g2)*t;
      b=b2+(b1-b2)*t;
     }
//--- Return the calculated color
   return this.RGBToColor(r,g,b);
  }

The method calculates an interpolated color based on three specified colors (color1, color2, color3) and the coeff coefficient. The first color is the color corresponding to a coefficient of -1. The second color is the color corresponding to a coefficient of 0; the last color is the color corresponding to a coefficient of +1. Since the correlation values between symbols range from -1 to +1, this method will smoothly calculate the cell color based on the coefficient value (symbol correlation) and return the calculated color.

In the event handler of the CCanvasBase base class, we will add a check for the chart subwindow size:

//+------------------------------------------------------------------+
//| CCanvasBase::Event handler                                       |
//+------------------------------------------------------------------+
void CCanvasBase::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- If the height of the indicator subwindow has not yet been determined when the terminal starts,
//--- we will adjust the distance between the top border of the indicator subwindow and the top border of the main chart window
   if(this.m_wnd>0 && this.m_wnd_y==0)
      this.m_wnd_y=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);

//--- Chart change event
//...
//...

When the terminal is launched with an indicator attached to a chart subwindow, the subwindow size may not yet have been determined at the time of class initialization, and the distance between the top border of the indicator subwindow and the top border of the main chart window may still be zero. This prevents the cursor position from being tracked correctly (its coordinates are read from the main chart window, not from the chart subwindow). Adding the code block shown here resolves this issue.

Now let's create new classes for the table's vertical header in the file \MQL5\Indicators\Tables\Controls\Controls.mqh.

In the macro substitutions section, we will define the minimum width of the table row header, and in the enumerations section we will add a new enumeration that defines the table row/cell highlighting modes:

//+------------------------------------------------------------------+
//| Included libraries                                               |
//+------------------------------------------------------------------+
#include "Base.mqh"

//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W                50          // Default text label width
#define  DEF_LABEL_H                16          // Default text label 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_ROWS_HEADER_W    24          // Minimum width of the table row headers
#define  DEF_TABLE_COLUMN_MIN_W     12          // Minimum width of the table column
#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 thumb
#define  DEF_AUTOREPEAT_DELAY       500         // Delay before launching the 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" tooltip name
#define  DEF_HINT_NAME_VERT         "HintVERT"        // "Double vertical arrow" tooltip name
#define  DEF_HINT_NAME_NWSE         "HintNWSE"        // "Double upper-left arrow" --- bottom right (NorthWest-SouthEast) tooltip name
#define  DEF_HINT_NAME_NESW         "HintNESW"        // "Double bottom-left arrow" --- upper right (NorthEast-SouthWest) tooltip name
#define  DEF_HINT_NAME_SHIFT_HORZ   "HintShiftHORZ"   // "Horizontal shift arrow" tooltip name
#define  DEF_HINT_NAME_SHIFT_VERT   "HintShiftVERT"   // "Vertical shift arrow" tooltip name

//+------------------------------------------------------------------+
//| 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 the element foreground color
   ELEMENT_SORT_BY_ALPHA_FG,                    // Comparison by the element foreground transparency
   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,                         // Ascending sorting
   TABLE_SORT_MODE_DESC,                        // Descending sorting
  };

enum ENUM_HINT_TYPE                             // Tooltip 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 from top left to bottom right (NorthWest-SouthEast)
   HINT_TYPE_ARROW_NESW,                        // Double arrow bottom left --- top right (NorthEast-SouthWest)
   HINT_TYPE_ARROW_SHIFT_HORZ,                  // Horizontal displacement arrow
   HINT_TYPE_ARROW_SHIFT_VERT,                  // Vertical displacement arrow
  };

enum ENUM_ROWS_HIGHLIGHT_MODE                   // Table row/cell highlighting modes
  {
   ROWS_HIGHLIGHT_MODE_CELLS,                   // Highlight individual cells (cell mode)
   ROWS_HIGHLIGHT_MODE_ROW,                     // Highlight the entire row (row mode)
  };  

While the minimum header width is clear enough, the row/cell highlighting mode needs some clarification: table rows are graphical elements of the "Panel" type. This element has its own handler for mouse hover events: by default, the element's color becomes slightly lighter. A table row has its own list of cells located within that row. The enumeration specifies how to handle the event when the cursor hovers over a row — either the entire row becomes slightly lighter, or each cell can be highlighted by changing its own color.

At this time, only the first option, which was implemented earlier, is fully operational. For the second option, the handler is simply disabled. However, it is the second option that will be applied to the table rows, where each cell will be colored with its own color depending on the symbol correlation value in that cell. To avoid unnecessary row flickering, we will use the second option, which simply does nothing with the color. In the future, this option could be refined so that each cell has its own handler for when the cursor hovers over it.

Right now, we have only one table header — a horizontal header containing the column headers. All of this is organized into two classes:

  1. CColumnCaptionView — column header,
  2. CTableHeaderView — a horizontal table header that contains a list of column header objects.

We now need to reorganize this structure, since we will also have a vertical table header. Just like the horizontal header, it will contain a list of row header objects.

So, we will need one more header class — for the row header — and a vertical header class. Accordingly, the class structure will be more elaborate: we will create a single common abstract header class with properties shared by all headers — both column and row headers — while the derived classes will have their own properties and methods specific to a particular header, whether a column header or a row header:

  1. CCaptionView — the base header object
    • CColumnCaptionView — a column header object class that inherits from CCaptionView,
    • CRowCaptionView — a row header object class that inherits from CCaptionView.
  2. CTableHeaderView — a horizontal table header that contains a list of CColumnCaptionView column header objects,
  3. CTableRowsHeaderView — the table's vertical header; contains a list of CRowCaptionView row headers.

In the method for creating a list element of the CListElm class, we will add the creation of objects of the new classes:

//+------------------------------------------------------------------+
//| Method for creating a list element                               |
//+------------------------------------------------------------------+
CObject *CListElm::CreateElement(void)
  {
//--- Depending on the object type in m_element_type, create a new object
   switch(this.m_element_type)
     {
      case ELEMENT_TYPE_BASE                       :  return new CBaseObj();           // Base object for graphical elements
      case ELEMENT_TYPE_COLOR                      :  return new CColor();             // Color object
      case ELEMENT_TYPE_COLORS_ELEMENT             :  return new CColorElement();      // Color set object for a graphical object element
      case ELEMENT_TYPE_RECTANGLE_AREA             :  return new CBound();             // Rectangular area of the element
      case ELEMENT_TYPE_IMAGE_PAINTER              :  return new CImagePainter();      // An object for drawing images
      case ELEMENT_TYPE_CANVAS_BASE                :  return new CCanvasBase();        // Base canvas object for graphical elements
      case ELEMENT_TYPE_ELEMENT_BASE               :  return new CElementBase();       // Base object for graphical elements
      case ELEMENT_TYPE_HINT                       :  return new CVisualHint();        // Tooltip
      case ELEMENT_TYPE_LABEL                      :  return new CLabel();             // Text label
      case ELEMENT_TYPE_BUTTON                     :  return new CButton();            // Simple button
      case ELEMENT_TYPE_BUTTON_TRIGGERED           :  return new CButtonTriggered();   // Two-state button
      case ELEMENT_TYPE_BUTTON_ARROW_UP            :  return new CButtonArrowUp();     // Up arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN          :  return new CButtonArrowDown();   // Down arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT          :  return new CButtonArrowLeft();   // Left arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT         :  return new CButtonArrowRight();  // Right arrow button
      case ELEMENT_TYPE_CHECKBOX                   :  return new CCheckBox();          // CheckBox control
      case ELEMENT_TYPE_RADIOBUTTON                :  return new CRadioButton();       // RadioButton control
      case ELEMENT_TYPE_TABLE_CELL_VIEW            :  return new CTableCellView();     // Table cell (View)
      case ELEMENT_TYPE_TABLE_ROW_VIEW             :  return new CTableRowView();      // Table row (View)
      case ELEMENT_TYPE_TABLE_CAPTION_VIEW         :  return new CCaptionView();       // Base header object (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  return new CColumnCaptionView(); // Table column header (View)
      case ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW     :  return new CRowCaptionView();    // Table row header (View)
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  return new CTableHeaderView();   // Table header (View)
      case ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW     :  return new CTableRowsHeaderView();// Vertical table header (View)
      case ELEMENT_TYPE_TABLE_VIEW                 :  return new CTableView();         // Table (View)
      case ELEMENT_TYPE_PANEL                      :  return new CPanel();             // Panel control
      case ELEMENT_TYPE_GROUPBOX                   :  return new CGroupBox();          // GroupBox control
      case ELEMENT_TYPE_CONTAINER                  :  return new CContainer();         // GroupBox control
      default                                      :  return NULL;
     }
  }

If you look at an Excel table, at the intersection of the horizontal and vertical headers in the upper-left corner of the table, you will see a triangular icon in the lower-right corner of the area where the two headers meet:

Let's make one just like that.

All images are drawn with a special drawing class, CImagePainter.

We will declare methods in the class for drawing triangles positioned at each corner of a rectangular area:

//+------------------------------------------------------------------+
//| Image drawing class                                              |
//+------------------------------------------------------------------+
class CImagePainter : public CBaseObj
  {
protected:
   CCanvas          *m_canvas;                                 // Pointer to the canvas we draw on
   CBound            m_bound;                                  // Image coordinates and boundaries
   uchar             m_alpha;                                  // Transparency
   
//--- Check the validity of the canvas and the correctness of its dimensions
   bool              CheckBound(const string source);

public:
//--- (1) Assign the canvas for drawing, (2) set and (3) return the transparency
   void              CanvasAssign(CCanvas *canvas)             { this.m_canvas=canvas;                }
   void              SetAlpha(const uchar value)               { this.m_alpha=value;                  }
   uchar             Alpha(void)                         const { return this.m_alpha;                 }
   
//--- (1) Set the coordinates and (2) resize the area
   void              SetXY(const int x,const int y)            { this.m_bound.SetXY(x,y);             }
   void              SetSize(const int w,const int h)          { this.m_bound.Resize(w,h);            }
//--- Set the coordinates and dimensions of the area
   void              SetBound(const int x,const int y,const int w,const int h)
                       {
                        this.SetXY(x,y);
                        this.SetSize(w,h);
                       }

//--- Return the bounds and dimensions of the drawing
   int               X(void)                             const { return this.m_bound.X();             }
   int               Y(void)                             const { return this.m_bound.Y();             }
   int               Right(void)                         const { return this.m_bound.Right();         }
   int               Bottom(void)                        const { return this.m_bound.Bottom();        }
   int               Width(void)                         const { return this.m_bound.Width();         }
   int               Height(void)                        const { return this.m_bound.Height();        }
   
//--- Clear the area
   bool              Clear(const int x,const int y,const int w,const int h,const bool update=true);
//--- Draw a filled arrow (1) up, (2) down, (3) left, (4) right
   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) a horizontal 17×7 and (2) a 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 17×17 double arrow (1) from the top left --- to the bottom right, (2) from the bottom left --- to the top right
   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 18×18 offset arrow (1) horizontally, (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) a checked, (2) an 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) a selected, (2) an unselected 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);

//--- Draw a border around a group of elements
   bool              FrameGroupElements(const int x,const int y,const int w,const int h,const string text,
                                        const color clr_text,const color clr_dark,const color clr_light,
                                        const uchar alpha,const bool update=true);
   
//--- Draw a filled triangle in (1) the upper-left corner, (2) the lower-left corner, (3) the upper-right corner, and (4) the lower-right corner
   bool              TriangleLT(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              TriangleLB(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              TriangleRT(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              TriangleRB(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);

//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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_IMAGE_PAINTER);  }
   
//--- Constructors/destructor
                     CImagePainter(void) : m_canvas(NULL)               { this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2); this.SetName("Image Painter");  }
                     CImagePainter(CCanvas *canvas) : m_canvas(canvas)  { this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2); this.SetName("Image Painter");  }
                     CImagePainter(CCanvas *canvas,const int id,const string name) : m_canvas(canvas)
                       {
                        this.m_id=id;
                        this.SetName(name);
                        this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2);
                       }
                     CImagePainter(CCanvas *canvas,const int id,const int dx,const int dy,const int w,const int h,const string name) : m_canvas(canvas)
                       {
                        this.m_id=id;
                        this.SetName(name);
                        this.SetBound(dx,dy,w,h);
                       }
                    ~CImagePainter(void) {}
  };

We will implement them outside the class body:

//+------------------------------------------------------------------+
//| Draw a filled triangle in the upper-left corner                  |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleLT(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is invalid, return false
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Shape coordinates
   int x1=x;
   int y1=y+h;
   int x2=x1;
   int y2=y;
   int x3=x2+w;
   int y3=y2;
   
//--- Draw a triangle
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }
//+------------------------------------------------------------------+
//| Draw a filled triangle in the lower-left corner                  |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleLB(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the drawing area is invalid, return 'false'
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Coordinates of the shape
   int x1=x;
   int y1=y;
   int x2=x1+w;
   int y2=y1+h;
   int x3=x1;
   int y3=y2;
   
//--- Draw a triangle
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }
//+------------------------------------------------------------------+
//| Draw a filled triangle in the upper-right corner                 |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleRT(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the drawing area is invalid, return 'false'
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Coordinates of the shape
   int x1=x;
   int y1=y;
   int x2=x1+w;
   int y2=y;
   int x3=x2;
   int y3=y2+h;
   
//--- Draw a triangle
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }
//+------------------------------------------------------------------+
//| Draw a filled triangle in the lower-right corner                 |
//+------------------------------------------------------------------+
bool CImagePainter::TriangleRB(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the drawing area is invalid, return 'false'
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Coordinates of the shape
   int x1=x+w;
   int y1=y;
   int x2=x1;
   int y2=y+h;
   int x3=x;
   int y3=y2;
   
//--- Draw a triangle
   this.m_canvas.FillTriangle(x1,y1,x2,y2,x3,y3,::ColorToARGB(clr,alpha));

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

Each method receives the coordinates of the rectangle inside which a filled triangle of the specified color should be drawn. The methods calculate all the coordinates of the triangle to be drawn and call the filled-triangle drawing method of the CCanvas class. If the canvas update flag passed to the method is set, the canvas is updated.

In the panel object class, let's add a method that returns the number of areas created in the object:

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

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

//--- Return an area by (1) list index, (2) identifier, or (3) assigned area name
   CBound           *GetBoundAt(const uint index)              { return this.m_list_bounds.GetNodeAtIndex(index); }
   CBound           *GetBoundByID(const int id);
   CBound           *GetBoundByName(const string name);

Not every created area can have a graphical element assigned to it; therefore, it is incorrect to use a method that returns the number of attached elements to determine the number of areas. The new method specifically returns the number of areas created in the element.

In the method that creates and adds a new element to the list, let's add the creation of new controls:

//+------------------------------------------------------------------+
//| CPanel::Creates and adds a new element to the list               |
//+------------------------------------------------------------------+
CElementBase *CPanel::InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h)
  {
//--- Create a name for a graphical object
   int elm_total=this.m_list_elm.Total();
   string obj_name=this.NameFG()+"_"+ElementShortName(type)+(string)elm_total;
//--- Calculate coordinates
   int x=this.X()+dx;
   int y=this.Y()+dy;
//--- Depending on the object type, create a new object
   CElementBase *element=NULL;
   switch(type)
     {
      case ELEMENT_TYPE_LABEL                      :  element = new CLabel(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);             break;   // Text label
      case ELEMENT_TYPE_BUTTON                     :  element = new CButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);            break;   // Simple button
      case ELEMENT_TYPE_BUTTON_TRIGGERED           :  element = new CButtonTriggered(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Two-state button
      case ELEMENT_TYPE_BUTTON_ARROW_UP            :  element = new CButtonArrowUp(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);     break;   // Up arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN          :  element = new CButtonArrowDown(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Down arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT          :  element = new CButtonArrowLeft(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Left arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT         :  element = new CButtonArrowRight(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);  break;   // Right arrow button
      case ELEMENT_TYPE_CHECKBOX                   :  element = new CCheckBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // CheckBox control
      case ELEMENT_TYPE_RADIOBUTTON                :  element = new CRadioButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);       break;   // RadioButton control
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H          :  element = new CScrollBarThumbH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Thumb of the horizontal ScrollBar
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V          :  element = new CScrollBarThumbV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Thumb of the vertical ScrollBar
      case ELEMENT_TYPE_SCROLLBAR_H                :  element = new CScrollBarH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Horizontal ScrollBar control
      case ELEMENT_TYPE_SCROLLBAR_V                :  element = new CScrollBarV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Vertical ScrollBar control
      case ELEMENT_TYPE_TABLE_ROW_VIEW             :  element = new CTableRowView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);      break;   // Object for the visual representation of a table row
      case ELEMENT_TYPE_TABLE_CAPTION_VIEW         :  element = new CCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);       break;   // Base header object (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  element = new CColumnCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break;   // Object for the visual representation of a table column header
      case ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW     :  element = new CRowCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);    break;   // Object for the visual representation of a table row header
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  element = new CTableHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Object for the visual representation of a table header
      case ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW     :  element = new CTableRowsHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);break;  // Object for the visual representation of table row headers
      case ELEMENT_TYPE_TABLE_VIEW                 :  element = new CTableView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Object for the visual representation of a table
      case ELEMENT_TYPE_PANEL                      :  element = new CPanel(obj_name,"",this.m_chart_id,this.m_wnd,x,y,w,h);               break;   // Panel control
      case ELEMENT_TYPE_GROUPBOX                   :  element = new CGroupBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // GroupBox control
      case ELEMENT_TYPE_CONTAINER                  :  element = new CContainer(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Container control
      default                                      :  element = NULL;
     }

//--- If the new element has not been created, report this and return NULL
   if(element==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create graphic element %s",__FUNCTION__,ElementDescription(type));
      return NULL;
     }
//--- Set the element's ID, name, container, and z-order
   element.SetID(elm_total);
   element.SetName(user_name);
   element.SetContainerObj(&this);
   element.ObjectSetZOrder(this.ObjectZOrder()+1);
   
//--- If the created element has not been added to the list, report this, delete the created element, and return NULL
   if(!this.AddNewElement(element))
     {
      ::PrintFormat("%s: Error. Failed to add %s element with ID %d to list",__FUNCTION__,ElementDescription(type),element.ID());
      delete element;
      return NULL;
     }
//--- Get the parent element to which the child elements are attached
   CElementBase *elm=this.GetContainer();
//--- If the parent element is of the "Container" type, then it has scrollbars
   if(elm!=NULL && elm.Type()==ELEMENT_TYPE_CONTAINER)
     {
      //--- Convert CElementBase to CContainer
      CContainer *container_obj=elm;
      //--- If the horizontal scrollbar is visible,
      if(container_obj.ScrollBarHorzIsVisible())
        {
         //--- Get a pointer to the horizontal scrollbar and bring it to the front
         CScrollBarH *sbh=container_obj.GetScrollBarH();
         if(sbh!=NULL)
            sbh.BringToTop(false);
        }
      //--- If the vertical scrollbar is visible,
      if(container_obj.ScrollBarVertIsVisible())
        {
         //--- Get a pointer to the vertical scrollbar and bring it to the front
         CScrollBarV *sbv=container_obj.GetScrollBarV();
         if(sbv!=NULL)
            sbv.BringToTop(false);
        }
     }
//--- Return a pointer to the created and attached element
   return element;
  }

Now these elements can be created from the panel object and attached to the list of attached elements.

In the CContainer container object class, we will optimize the method that shifts the content horizontally by a specified amount:

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

//--- Return the result of shifting the content by the calculated amount
   res &=elm.MoveX(this.X()-content_offset);
   return res;
  }

Here, we simply removed one unnecessary condition. The modifications are minimal.

Now, in the method that shifts the content vertically, we will also add shifting of the vertical header:

//+------------------------------------------------------------------+
//| CContainer::Shift the content vertically by the specified value  |
//+------------------------------------------------------------------+
bool CContainer::ContentShiftVert(const int value)
  {
//--- Get a pointer to the container's content
   CElementBase *elm=this.GetAttachedElement();
   if(elm==NULL)
      return false;
   
//--- Calculate the offset amount based on the scrollbar slider position
   int content_offset=this.CalculateContentOffsetVert(value);
   
//--- For a CTableView element, get the table's vertical header
   bool res=true;
   CElementBase         *elm_container=elm.GetContainer();
   CTableRowsHeaderView *table_header=NULL;
   if(elm_container!=NULL && ::StringFind(elm.Name(),"Table")==0)
     {
      CElementBase *obj=elm_container.GetContainer();
      if(obj!=NULL && obj.Type()==ELEMENT_TYPE_TABLE_VIEW)
        {
         CTableView *table_view=obj;
         table_header=table_view.GetRowsHeader();
         //--- Shift the header
         if(table_header!=NULL)
            res &=table_header.MoveY(this.Y()-content_offset);
        }
     }

//--- Return the result of shifting the content by the calculated amount
   res &=elm.MoveY(this.Y()-content_offset);
   return res;
  }

A table cell should now be able to have its own background color setting instead of inheriting it from the row in which it is located.

In the cell object class, let's declare a variable to store the background color and methods for setting and getting this color:

//+------------------------------------------------------------------+
//| 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 the cell)
   int               m_text_x;                                 // X coordinate of the text (offset relative to the left edge of the object area)
   int               m_text_y;                                 // Y coordinate of the text (offset relative to the top edge of the object area)
   ushort            m_text[];                                 // Text
   color             m_fore_color;                             // Foreground color
   color             m_back_color;                             // Background color
   
//--- Return the offsets of the initial drawing coordinates on the drawing canvas relative to the canvas and the base element coordinates
   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 canvas offset 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 text anchor point
   bool              GetTextCoordsByAnchor(int &x, int &y, int &dir_x, int dir_y);

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

//--- Get the bounds 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 a flag indicating that the object is outside its container
   virtual bool      IsOutOfContainer(void);

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

//--- (1) Set, (2) return the cell background color
   void              SetBackColor(const color clr)                      { this.m_back_color=clr;                     }
   color             BackColor(void)                              const { return this.m_back_color;                  }

//--- Set the identifier
   virtual void      SetID(const int id)                                { this.m_id=id;                              }
//--- (1) Set, (2) return the cell index
   void              SetIndex(const int index)                          { this.m_index=index;                        }
   int               Index(void)                                  const { return this.m_index;                       }

//--- (1) Set, (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, (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, (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;                 }
   
//--- Set the text 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, (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 to the log
   void              TableCellModelPrint(void);
   
//--- (1) Fill the object with the background color, (2) update the object to display the changes, (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 text
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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 a description of the object
   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 and the background and foreground canvases to a cell, we initialize the cell background color to the row background color on which the cell is located:

//+--------------------------------------------------------------------------+
//| CTableCellView::Assign a row and the 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();
   this.m_back_color=this.m_element_base.BackColor();
  }

By default, the cell background color will be the same as the row background color.

In the method that draws a cell, let's add drawing of the cell background:

//+------------------------------------------------------------------+
//| CTableCellView::Draw the appearance                              |
//+------------------------------------------------------------------+
void CTableCellView::Draw(const bool chart_redraw)
  {
//--- If the cell is outside the table row container, exit
   if(this.IsOutOfContainer())
      return;
      
//--- Get the text coordinates and the offset direction based on the text 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;
//--- Adjust the text coordinates
   int x=this.AdjX(this.X()+text_x);
   int y=this.AdjY(this.Y()+text_y);
   
//--- Set the coordinates of the separator line
   int x1=this.AdjX(this.X());
   int y1=this.AdjY(this.Y());
   int x2=this.AdjX(this.X());
   int y2=this.AdjY(this.Bottom());

//--- Draw the text on the foreground canvas, taking the offset direction into account, without updating the chart
   this.DrawText(x+this.m_text_x*dir_horz,y+this.m_text_y*dir_vert,this.Text(),false);
   
//--- Set the coordinates for a rectangular fill
   x1=this.AdjX(this.X());
   y1=this.AdjY(this.Y());
   x2=this.AdjX(this.Right());
   y2=this.AdjY(this.Bottom()-1);
   this.m_background.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.BackColor(),this.m_element_base.AlphaBG()));

//--- If this is not the rightmost cell, draw a vertical divider to the right of the cell
   if(this.m_element_base!=NULL && this.Index()<this.m_element_base.CellsTotal()-1)
     {
      int line_x=this.AdjX(this.Right());
      this.m_background.Line(line_x,y1,line_x,y2,::ColorToARGB(this.m_element_base.BorderColor(),this.m_element_base.AlphaBG()));
     }
//--- Update the background canvas with the specified chart redraw flag
   this.m_background.Update(chart_redraw);
  }

The color-highlighted code block unconditionally draws a filled rectangle the size of the entire cell. Later, we will add a check here for the row highlighting mode, and if the entire row is highlighted when the cursor hovers over it, this code block should not be executed. For now, to keep things simple, let's just fill the cell background completely with the specified color.

Now we will refine the table row visual representation class CTableRowView.

Let's declare new variables, methods, and event handlers:

//+------------------------------------------------------------------+
//| 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;                                   // List of cells
   int               m_index;                                        // Index in the list of rows
   ENUM_ROWS_HIGHLIGHT_MODE m_highlight_mode;                        // Row highlighting mode
   
//--- Create and add a new cell view 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 area and the cell with the corresponding index
   bool              BoundCellDelete(const int index);
//--- Return the visual representation of (1) the table, (2) the column header, and (3) the row header
   CTableView       *GetTableView(void);
   CTableHeaderView *GetHeaderView(void);
   CTableRowsHeaderView *GetRowsHeaderView(void);
   
//--- Set the specified (1) column header or (2) row header as selected
   void              SetColumnCaptionSelected(const uint index);
   void              SetRowCaptionSelected(const uint index);
//--- Clear selection from all (1) column headers and (2) row headers
   void              SetAllColumnCaptionsUnselected(const int exclude=-1);
   void              SetAllRowCaptionsUnselected(const int exclude=-1);
   
public:
//--- Return (1) the list, (2) the number of cells, (3) a cell, (4) the column header, and (5) the row header
   CListElm         *GetListCells(void)                                 { return &this.m_list_cells;                       }
   int               CellsTotal(void)                             const { return this.m_list_cells.Total();                }
   CTableCellView   *GetCellView(const uint index)                      { return this.m_list_cells.GetNodeAtIndex(index);  }
   CColumnCaptionView *GetColumnCaption(const uint index);
   CRowCaptionView  *GetRowCaption(const uint index);
   
//--- Set the identifier
   virtual void      SetID(const int id)                                { this.m_id=id;                                    }
//--- (1) Set, (2) return the row index
   void              SetIndex(const int index)                          { this.m_index=index;                              }
   int               Index(void)                                  const { return this.m_index;                             }

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

//--- (1) Set, (2) return the row highlighting mode
   void              SetHighlightMode(const ENUM_ROWS_HIGHLIGHT_MODE mode) { this.m_highlight_mode=mode;                   }
   ENUM_ROWS_HIGHLIGHT_MODE HighlightMode(void)                   const { return this.m_highlight_mode;                    }
   
//--- Recalculate cell areas
   bool              RecalculateBounds(CListElm *list_bounds);

//--- Print the assigned row model to the log
   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: (1) comparison, (2) saving to a file, (3) loading from a 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);             }
  
//--- Initialization of (1) a class object and (2) the object's default colors
   void              Init(void);
   virtual void      InitColors(void);

//--- Event handlers for (1) cursor hover (Focus), (2) mouse button presses (Press),
   virtual void      OnFocusEvent(const int id, const long lparam, const double dparam, const string sparam);
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);
   
//--- 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 class constructors, we initialize the highlighting mode to "highlight the entire row" by default:

//+------------------------------------------------------------------------+
//| CTableRowView::Default constructor. Creates the object in the main     |
//| window of the current chart at coordinates 0,0 with default dimensions |
//+------------------------------------------------------------------------+
CTableRowView::CTableRowView(void) : CPanel("TableRow","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1), m_highlight_mode(ROWS_HIGHLIGHT_MODE_ROW)
  {
//--- Initialization
   this.Init();
  }
//+----------------------------------------------------------------------+
//| CTableRowView::Parameterized constructor. Creates the object in      |
//| the specified window of the specified chart with the specified text, |
//| coordinates, and dimensions                                          |
//+----------------------------------------------------------------------+
CTableRowView::CTableRowView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h), m_index(-1), m_highlight_mode(ROWS_HIGHLIGHT_MODE_ROW)
  {
//--- Initialization
   this.Init();
  }

Let's implement the new methods.

Method that returns the table's visual representation:

//+------------------------------------------------------------------+
//| CTableRowView::Return the table's visual representation          |
//+------------------------------------------------------------------+
CTableView *CTableRowView::GetTableView(void)
  {
   CTableView *obj=NULL;
//--- Get the table row panel
   CElementBase *base0=this.GetContainer();
   if(base0==NULL)
      return NULL;
   
//--- Get the table row panel container
   CElementBase *base1=base0.GetContainer();
   if(base1==NULL)
      return NULL;
   
//--- Get the table visual representation object
   CElementBase *base2=base1.GetContainer();
   if(base2!=NULL && base2.Type()==ELEMENT_TYPE_TABLE_VIEW)
     {
      obj=base2;
      return obj;
     }
   return NULL;
  }

The table is organized as follows:

A panel of type "Table" (1) contains the table header and a container (2), inside which there is a scrollable panel (3); that panel, in turn, contains the table rows (4), which are the class currently under consideration.
To obtain the table object (1), you need to get the base panel object (3) to which the row is attached. Next, from panel (3), get its base container object (2), and from the container, get its base table object (1).

A method that returns the visual representation of the column header:

//+------------------------------------------------------------------+
//| CTableRowView::Returns a visual representation of the            |
//| column headers                                                   |
//+------------------------------------------------------------------+
CTableHeaderView *CTableRowView::GetHeaderView(void)
  {
   CTableView *table=this.GetTableView();
   return(table!=NULL ? table.GetHeader() : NULL);
  }

Here, we obtain the "table" object using the method described above, and then obtain the horizontal header object from it.

A method that returns a visual representation of the row header:

//+------------------------------------------------------------------+
//|CTableRowView::Return a visual representation of the row header   |
//+------------------------------------------------------------------+
CTableRowsHeaderView *CTableRowView::GetRowsHeaderView(void)
  {
   CTableView *table=this.GetTableView();
   return(table!=NULL ? table.GetRowsHeader() : NULL);
  }

We obtain the "table" object and, from it, the vertical header object, whose implementation we will discuss below.

A method that returns a column header:

//+------------------------------------------------------------------+
//| CTableRowView::Return the column header                          |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableRowView::GetColumnCaption(const uint index)
  {
   CTableHeaderView *header=this.GetHeaderView();
   return(header!=NULL ? header.GetColumnCaption(index) : NULL);
  }

Here, first we retrieve the horizontal header object using the method described above, and from it, the column header specified by its index.

A method that returns a row header:

//+------------------------------------------------------------------+
//| CTableRowView::Return a row header                               |
//+------------------------------------------------------------------+
CRowCaptionView *CTableRowView::GetRowCaption(const uint index)
  {
   CTableRowsHeaderView *header=this.GetRowsHeaderView();
   return(header!=NULL ? header.GetRowCaption(index) : NULL);
  }

Using the method described above, we obtain the vertical header object — the implementation of which we will examine below — and from it, the row header specified by its index.

A method that sets the specified column header as selected:

//+------------------------------------------------------------------+
//|CTableRowView::Set the specified column header as selected        |
//+------------------------------------------------------------------+
void CTableRowView::SetColumnCaptionSelected(const uint index)
  {
   CColumnCaptionView *capt=this.GetColumnCaption(index);
   if(capt==NULL || capt.State()==ELEMENT_STATE_ACT)
      return;
   capt.SetState(ELEMENT_STATE_ACT);
   capt.GetBackground().FillRectangle(0,capt.Height()-2,capt.Width()-1,capt.Height()-1,ColorToARGB(clrCadetBlue));
   capt.GetBackground().Update(false);
  }

A column header can be in the selected state. For example, to select the header corresponding to the selected table cell. The method checks that the obtained table header is not already selected, sets the selected-object flag, and draws a thin selection line beneath the header.

A method that sets the specified row header as selected:

//+------------------------------------------------------------------+
//| CTableRowView::Set the specified row header as selected          |
//+------------------------------------------------------------------+
void CTableRowView::SetRowCaptionSelected(const uint index)
  {
   CRowCaptionView *capt=this.GetRowCaption(index);
   if(capt==NULL || capt.State()==ELEMENT_STATE_ACT)
      return;
   capt.SetState(ELEMENT_STATE_ACT);
   capt.GetBackground().FillRectangle(capt.Width()-2,2,capt.Width()-1,capt.Height()-0,ColorToARGB(clrCadetBlue));
   capt.GetBackground().Update(false);
  }

The method's logic is similar to that of the previous one, except that the right edge of the header is highlighted.

A method that removes the selection from all column headers:

//+------------------------------------------------------------------+
//| CTableRowView::Deselect all column headers                       |
//+------------------------------------------------------------------+
void CTableRowView::SetAllColumnCaptionsUnselected(const int exclude=-1)
  {
   CTableHeaderView *header=this.GetHeaderView();
   if(header==NULL)
      return;
   int total=header.BoundsTotal();
   for(int i=0;i<total;i++)
     {
      CColumnCaptionView *capt=this.GetColumnCaption(i);
      if(capt==NULL || (exclude>-1 && i==exclude))
         continue;
      if(capt.State()!=ELEMENT_STATE_DEF)
        {
         capt.SetState(ELEMENT_STATE_DEF);
         capt.Draw(false);
        }
     }
  }

The method is passed the index of the header that should remain selected. The selection is removed from all the others. In a loop over the number of headers, we get each column header in turn and check whether it is the header specified for exclusion. If the loop index matches the one specified in exclude, this header remains selected. The selection is removed from all other headers. If a negative value is passed to exclude, the selected state is cleared from all headers.

A method that clears the selection from all row headers:

//+------------------------------------------------------------------+
//| CTableRowView::Deselect all row headers                          |
//+------------------------------------------------------------------+
void CTableRowView::SetAllRowCaptionsUnselected(const int exclude=-1)
  {
   CTableRowsHeaderView *header=this.GetRowsHeaderView();
   if(header==NULL)
      return;
   int total=header.BoundsTotal();
   for(int i=0;i<total;i++)
     {
      CRowCaptionView *capt=this.GetRowCaption(i);
      if(capt==NULL || (exclude>-1 && capt.ID()==exclude))
         continue;
      if(capt.State()!=ELEMENT_STATE_DEF)
        {
         capt.SetState(ELEMENT_STATE_DEF);
         capt.Draw(false);
        }
     }
  }

The logic of this method is identical to that of the method discussed above.

Cursor hover handler:

//+------------------------------------------------------------------+
//| CTableRowView::Cursor hover handler                              |
//+------------------------------------------------------------------+
void CTableRowView::OnFocusEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- If the entire row is being processed, call the event handler of the parent class
   if(this.m_highlight_mode==ROWS_HIGHLIGHT_MODE_ROW)
     {
      CCanvasBase::OnFocusEvent(id,lparam,dparam,sparam);
      return;
     }

//--- Get the cursor coordinates
   int x=int(lparam-this.X());
   int y=int(dparam-this.m_wnd_y-this.Y());

//--- Loop through the row's cell areas
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next area
      CBound *bound=this.GetBoundAt(i);
      if(bound==NULL)
         continue;

      //--- Get the element assigned to the current area
      CBaseObj *obj=bound.GetAssignedObj();
      CTableCellView *cell=NULL;
      
      //--- If the retrieved element is not a table cell, continue
      if(obj==NULL || obj.Type()!=ELEMENT_TYPE_TABLE_CELL_VIEW)
         continue;

      //--- This is a table cell. Determine its coordinates in the table (row/column)
      cell=obj;
      int row=this.ID();
      int col=obj.ID();
      
      //--- Get the corresponding row and column headers
      CColumnCaptionView *col_capt=this.GetColumnCaption(col);
      CRowCaptionView    *row_capt=this.GetRowCaption(row);
      if(col_capt==NULL || row_capt==NULL)
         continue;
      
      //--- If the cursor is over the cell area
      if(bound.Contains(x,y))
        {
         //--- Set the column and row headers as selected,
         this.SetColumnCaptionSelected(i);
         this.SetRowCaptionSelected(this.ID());
         //--- Clear the “selected header” flag from all row headers except the current one
         this.SetAllRowCaptionsUnselected(this.ID());
        }
      //--- If the cursor is outside the cell area
      else
        {
         //--- If the header is selected,
         if(col_capt.State()!=ELEMENT_STATE_DEF)
           {
            //--- clear its selection and redraw the object as unselected
            col_capt.SetState(ELEMENT_STATE_DEF);
            col_capt.Draw(false);
           }
        }
     }
  }

The handler logic is described in its comments. If the entire row is highlighted, the parent class' “object in focus” handler is called. Otherwise, the cell is looked up by its coordinates in the table and, if the cursor is hovering over it, the corresponding row and column headers for that cell are selected. No other changes to the cell appearance are implemented here; this is a task for future improvements.

Object click handler:

//+------------------------------------------------------------------+
//| CTableRowView::Object click handler                              |
//+------------------------------------------------------------------+
void CTableRowView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- If the entire row is being processed, call the parent class event handler
   if(this.m_highlight_mode==ROWS_HIGHLIGHT_MODE_ROW)
     {
      CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
      return;
     }

//--- In a loop through all row areas
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the next area
      CBound *bound=this.GetBoundAt(i);
      if(bound==NULL)
         continue;

      //--- Get the cursor coordinates and
      int x=int(lparam-this.X());
      int y=int(dparam-this.m_wnd_y-this.Y());
      //--- Check that the cursor is inside the area
      if(bound.Contains(x,y))
        {
         //--- Get the attached object (cell) from the area
         CBaseObj *obj=bound.GetAssignedObj();
         if(obj!=NULL)
           {
            //--- Write the cell address in the table (row/column)
            int row=this.ID();
            int col=obj.ID();
            
            //--- Based on the row and column identifiers, get pointers to the corresponding headers
            CRowCaptionView    *row_capt=this.GetRowCaption(row);
            CColumnCaptionView *col_capt=this.GetColumnCaption(col);
            if(row_capt==NULL || col_capt==NULL)
               return;
            
            //--- Create a text value for the custom event from the row name and header texts
            string sprm=obj.Name()+";"+row_capt.Text()+";"+col_capt.Text();
            //--- Send a custom object-click event with the row and column coordinates and text
            ::EventChartCustom(this.m_chart_id,CHARTEVENT_OBJECT_CLICK,row,col,sprm);
           }
        }
     }
  }

The method logic is described in its comments. If the entire row is being processed, the parent class's "object in focus" handler is called. Otherwise, the cell is found by its coordinates in the table, and if the cursor is hovering over it, a string is created from the row and column header texts (the symbol name in the context of this article) with ";" as the separator; then a custom object-click event is sent with the row index (lparam), the column index (dparam), and the created string of header texts in sparam. This event can then be captured in the program to determine which cell was clicked and handle the event.

In the file-handling methods, we save and load the highlighting mode value:

//+------------------------------------------------------------------+
//| CTableRowView::Save to a file                                    |
//+------------------------------------------------------------------+
bool CTableRowView::Save(const int file_handle)
  {
//--- Save the parent object's data
   if(!CPanel::Save(file_handle))
      return false;

//--- Save the list of cells
   if(!this.m_list_cells.Save(file_handle))
      return false;
//--- Save the row number
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the highlighting mode
   if(::FileWriteInteger(file_handle,this.m_highlight_mode,INT_VALUE)!=INT_VALUE)
      return false;
   
//--- Completed successfully
   return true;
  }
//+------------------------------------------------------------------+
//| CTableRowView::Load from a file                                  |
//+------------------------------------------------------------------+
bool CTableRowView::Load(const int file_handle)
  {
//--- Load data from the parent object
   if(!CPanel::Load(file_handle))
      return false;
     
//--- Load the list of cells
   if(!this.m_list_cells.Load(file_handle))
      return false;
//--- Load the row number
   this.m_index=(int)::FileReadInteger(file_handle,INT_VALUE);
//--- Load the highlighting mode
   this.m_highlight_mode=(ENUM_ROWS_HIGHLIGHT_MODE)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Completed successfully
   return true;
  }

Since there will now be two types of headers — the column header and the table row header — the previously created and used table column header class needs to be split into an abstract header class and derived classes for the column header and table row header. We will move the common properties and functionality of any header into the abstract class, while the derived classes will refine the functionality according to the specific type of header.

Abstract class for the visual representation of a header:

//+------------------------------------------------------------------+
//| Abstract class for the visual representation of a header         |
//+------------------------------------------------------------------+
class CCaptionView : public CButton
  {
protected:
   CBound           *m_bound_node;                                   // Pointer to the header area
   int               m_index;                                        // Index in the list of rows
   
public:
//--- Set the identifier
   virtual void      SetID(const int id)                                { this.m_id=id;                              }
//--- (1) Set, (2) return the row index
   void              SetIndex(const int index)                          { this.m_index=index;                        }
   int               Index(void)                                  const { return this.m_index;                       }

//--- (1) Assign, (2) return the header area to which the object is assigned
   void              AssignBoundNode(CBound *bound)                     { this.m_bound_node=bound;                   }
   CBound           *GetBoundNode(void)                                 { return this.m_bound_node;                  }

//--- Draw (1) the appearance and (2) the sort direction arrow
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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_CAPTION_VIEW);   }
  
//--- Initialization of (1) a class object and (2) the object's default colors
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Return a description of the object
   virtual string    Description(void);
   
//--- Constructors/destructor
                     CCaptionView(void);
                     CCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); 
                    ~CCaptionView (void){}
  };
//+------------------------------------------------------------------+
//| CCaptionView::Default constructor. Constructs an object          |
//| in the main chart window of the current chart at coordinates 0,0 |
//| with default dimensions                                          |
//+------------------------------------------------------------------+
CCaptionView::CCaptionView(void) : CButton("Caption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0)
  {
//--- Initialization
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   this.SetName("Caption");
  }
//+----------------------------------------------------------------------------------+
//| CCaptionView::Parameterized constructor.                                         |
//| Construct the object in the specified chart window of the specified chart with   |
//| the specified text, coordinates, and dimensions                                  |
//+----------------------------------------------------------------------------------+
CCaptionView::CCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) :
   CButton(object_name,text,chart_id,wnd,x,y,w,h), m_index(0)
  {
//--- Initialization
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Initialization                                     |
//+------------------------------------------------------------------+
void CCaptionView::Init(const string text)
  {
//--- Default text offsets
   this.m_text_x=4;
   this.m_text_y=2;
//--- Set colors for different states
   this.InitColors();
//--- Can be resized
   this.SetResizable(false);
   this.SetMovable(false);
   this.SetImageBound(this.ObjectWidth()-14,4,8,11);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Default object color initialization                |
//+------------------------------------------------------------------+
void CCaptionView::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and set the normal-state color as the current background color
   this.InitBackColors(C'230,230,230',C'159,213,183',this.GetBackColorControl().NewColor(C'159,213,183',-6,-6,-6),clrSilver);
   this.InitBackColorsAct(C'230,230,230',C'159,213,183',this.GetBackColorControl().NewColor(C'159,213,183',-6,-6,-6),clrSilver);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and set the normal-state color as the current text color
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and set the normal-state color as the current border color
   this.InitBorderColors(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.InitBorderColorsAct(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.BorderColorToDefault();
   
//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrNULL);
   this.InitForeColorBlocked(clrSilver);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Draw the appearance                                |
//+------------------------------------------------------------------+
void CCaptionView::Draw(const bool chart_redraw)
  {
//--- If the object is outside its container, return
   if(this.IsOutOfContainer())
      return;

//--- Fill the object with the background color, draw a light vertical line on the left and a dark one on the right
   this.Fill(this.BackColor(),false);
   color clr_dark =this.BorderColor();                                                       // "Dark color"
   color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100);   // "Light color"
   this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG()));                          // Line on the left
   this.m_background.Line(this.AdjX(this.Width()-1),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(clr_dark,this.AlphaBG())); // Line on the right
//--- Update the background canvas
   this.m_background.Update(false);
   
//--- Display the header text
   CLabel::Draw(false);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| CCaptionView::Return the object description                      |
//+------------------------------------------------------------------+
string CCaptionView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height());
  }
//+------------------------------------------------------------------+
//| CCaptionView::Save to file                                       |
//+------------------------------------------------------------------+
bool CCaptionView::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CButton::Save(file_handle))
      return false;
  
//--- Save the header number
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- Success
   return true;
  }
//+------------------------------------------------------------------+
//| CCaptionView::Load from file                                     |
//+------------------------------------------------------------------+
bool CCaptionView::Load(const int file_handle)
  {
//--- Load the parent object data
   if(!CButton::Load(file_handle))
      return false;
      
//--- Load the header number
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Success
   return true;
  }

All properties and methods that are used in exactly the same way in any of the derived header classes have been moved to this class.

The previously created table column header class now inherits from the abstract header class and has a trimmed-down form, because many methods have been moved to the parent abstract header class.

Let's look at the entire class:

//+------------------------------------------------------------------+
//| Class for the visual representation of a table column header     |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CCaptionView
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Pointer to the column header model
   ENUM_TABLE_SORT_MODE m_sort_mode;                                 // Table column sorting mode
   bool              m_sortable;                                     // Sort control flag
   
//--- Add tooltip objects with arrows to the list
   virtual bool      AddHintsArrowed(void);
//--- Display the resize cursor
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
public:
//--- (1) Set, (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 to the log
   void              ColumnCaptionModelPrint(void);

//--- (1) Set, (2) return the sortability flag
   void              SetSortableFlag(const bool flag)
                       {
                        this.m_sortable=flag;
                        this.SetSortMode(flag ? TABLE_SORT_MODE_ASC : TABLE_SORT_MODE_NONE);
                       }
   bool              IsSortabe(void)                              const { return this.m_sortable;                    }

//--- (1) Set, (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 opposite sort direction
   void              SetSortModeReverse(void);
   
//--- Draw (1) the appearance and (2) the sort direction arrow
   virtual void      Draw(const bool chart_redraw);
protected:
   void              DrawSortModeArrow(void);
public:  
//--- Handler for resizing the element from the right edge
   virtual bool      ResizeZoneRightHandler(const int x, const int y);
   
//--- Handlers for resizing an element from its 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's width
   virtual bool      ResizeW(const int w);
   
//--- Mouse button press event handler (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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);}
  
//--- Initialization of (1) a class object and (2) the object's default colors
   void              Init(const string text);
   //virtual void      InitColors(void);
   
//--- Return a description of the object
   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){}
  };
//+-------------------------------------------------------------------+
//| CColumnCaptionView::Default constructor. Constructs the object    |
//| in the main chart window of the current chart at coordinates 0, 0 |
//| with default dimensions                                           |
//+-------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(void) : CCaptionView("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Initialization
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   this.SetName("ColumnCaption");
  }
//+-----------------------------------------------------------------------------+
//| CColumnCaptionView::Parameterized constructor.                              |
//| Construct the object in the specified window of the specified chart 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) :
   CCaptionView(object_name,text,chart_id,wnd,x,y,w,h),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Initialization
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Initialization                               |
//+------------------------------------------------------------------+
void CColumnCaptionView::Init(const string text)
  {
//--- Initialization of the parent object
   CCaptionView::Init(text);

//--- Can be resized
   this.SetResizable(true);
   this.SetMovable(false);
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Draw the object's appearance                 |
//+------------------------------------------------------------------+
void CColumnCaptionView::Draw(const bool chart_redraw)
  {
//--- If the object is outside its container, exit
   if(this.IsOutOfContainer())
      return;

//--- Fill the object with the background color; draw a light vertical line on the left and a 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(), 20, 20, 20);      // "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

//--- Display the header text
   CLabel::Draw(false);
      
//--- Draw sort direction arrows
   this.DrawSortModeArrow();

//--- Update the background canvas
   this.m_background.Update(false);
   
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Draw the sort direction arrow                |
//+------------------------------------------------------------------+
void CColumnCaptionView::DrawSortModeArrow(void)
  {
//--- Set the arrow color for the object's normal and disabled states
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   switch(this.m_sort_mode)
     {
      //--- Sort in ascending order
      case TABLE_SORT_MODE_ASC   :  
         //--- Clear the drawing area and draw a downward 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 in descending order
      case TABLE_SORT_MODE_DESC  :  
         //--- Clear the drawing area and draw an upward 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;
     }
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Reverse the sort 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;
     }
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Return the object's description              |
//+------------------------------------------------------------------+
string CColumnCaptionView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   string sort=(this.SortMode()==TABLE_SORT_MODE_ASC ? "ascending" : this.SortMode()==TABLE_SORT_MODE_DESC ? "descending" : "none");
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d, sort %s",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height(),sort);
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Set the column header model                  |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ColumnCaptionModelAssign(CColumnCaption *caption_model)
  {
//--- If an invalid column header model object is passed, report it and return false
   if(caption_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Save the column header model
   this.m_column_caption_model=caption_model;
//--- Set the dimensions of the drawing area for the visual representation of the column header
   this.m_painter.SetBound(0,0,this.Width(),this.Height());
//--- Completed successfully
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Print to the log                             |
//  the assigned column header model                                 
//+------------------------------------------------------------------+
void CColumnCaptionView::ColumnCaptionModelPrint(void)
  {
   if(this.m_column_caption_model!=NULL)
      this.m_column_caption_model.Print();
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Add to the list                              |
//  tooltip objects with arrows
//+------------------------------------------------------------------+
bool CColumnCaptionView::AddHintsArrowed(void)
  {
//--- Create the horizontal-offset arrow tooltip
   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 tooltip image area
   hint.SetImageBound(0,0,hint.Width(),hint.Height());
   
//--- Hide the tooltip and draw its visual appearance
   hint.Hide(false);
   hint.Draw(false);
   
//--- Everything completed successfully
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Display a resize cursor                      |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y)
  {
   CVisualHint *hint=NULL;          // Pointer to a tooltip
   int hint_shift_x=0;              // Tooltip X offset
   int hint_shift_y=0;              // Tooltip Y offset
   
//--- Depending on the cursor's position at the element's boundaries
//--- Specify the tooltip offsets relative to the cursor coordinates,
//--- Display the required tooltip on the chart and obtain a 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 tooltip's position relative to the cursor
   return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false);
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Handler for resizing by the right edge       |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeZoneRightHandler(const int x,const int y)
  {
//--- Calculate and set the new width of the element
   int width=::fmax(x-this.X()+1,DEF_TABLE_COLUMN_MIN_W);
   if(!this.ResizeW(width))
      return false;
//--- Get a pointer to the tooltip
   CVisualHint *hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ);
   if(hint==NULL)
      return false;
//--- Move the tooltip by the specified amounts 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;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Change the object width                      |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeW(const int w)
  {
   if(!CCanvasBase::ResizeW(w))
      return false;
//--- Clear the drawing area at 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;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Mouse button press event handler             |
//+------------------------------------------------------------------+
void CColumnCaptionView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- If the mouse button is released within the right-edge drag area of the element, exit
   if(this.ResizeRegion()==CURSOR_REGION_RIGHT)
      return;
//--- Reverse the sort direction arrow and call the mouse click handler
   if(this.m_sortable)
      this.SetSortModeReverse();
   CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
   ::EventChartCustom(this.m_chart_id,CHARTEVENT_OBJECT_CLICK,this.ID(),-(10000+this.SortMode()),this.NameFG());
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Save to file                                 |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Save(const int file_handle)
  {
//--- Save the parent object's data
   if(!CButton::Save(file_handle))
      return false;
  
//--- Save the header number
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the sort direction
   if(::FileWriteInteger(file_handle,this.m_sort_mode,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the sort control flag
   if(::FileWriteInteger(file_handle,this.m_sortable,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- Done successfully
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Load from file                               |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Load(const int file_handle)
  {
//--- Load the parent object's data
   if(!CButton::Load(file_handle))
      return false;
      
//--- Load the header number
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the sort direction
   this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE);
//--- Load the sort control flag
   this.m_sortable=(bool)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Done successfully
   return true;
  }

A sort control flag and methods for managing this flag have been added to the class:

class CColumnCaptionView : public CCaptionView
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Pointer to the column header model
   ENUM_TABLE_SORT_MODE m_sort_mode;                                 // Table column sorting mode
   bool              m_sortable;                                     // Sort control flag
   
//--- Add tooltip objects with arrows to the list
   virtual bool      AddHintsArrowed(void);
//--- Display the resize cursor
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
public:
//--- (1) Set, (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 to the log
   void              ColumnCaptionModelPrint(void);

//--- (1) Set, (2) return the sortability flag
   void              SetSortableFlag(const bool flag)
                       {
                        this.m_sortable=flag;
                        this.SetSortMode(flag ? TABLE_SORT_MODE_ASC : TABLE_SORT_MODE_NONE);
                       }
   bool              IsSortabe(void)                              const { return this.m_sortable;                    }

//--- (1) Set, (2) return the sorting mode

Now we can enable the option to sort the table by the values in a column's cells.

In the class constructors, its default value is set to true:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Default constructor. Construct the object    |
//| in the main chart window of the current chart at coordinates 0,0 |
//| with default dimensions                                          |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(void) : CCaptionView("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Initialization
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   this.SetName("ColumnCaption");
  }
//+-----------------------------------------------------------------------------+
//| CColumnCaptionView::Parameterized constructor.                              |
//| Construct the object in the specified window of the specified chart 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) :
   CCaptionView(object_name,text,chart_id,wnd,x,y,w,h),m_sort_mode(TABLE_SORT_MODE_NONE),m_sortable(true)
  {
//--- Initialization
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
  }

In the mouse button click event handler, the sort control flag is now checked and a custom event for a click on the object is sent:

//+------------------------------------------------------------------+
//| 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 within the drag area of the element's right edge, exit
   if(this.ResizeRegion()==CURSOR_REGION_RIGHT)
      return;
//--- Change the sort direction arrow to the opposite direction and call the mouse click handler
   if(this.m_sortable)
      this.SetSortModeReverse();
   CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
   ::EventChartCustom(this.m_chart_id,CHARTEVENT_OBJECT_CLICK,this.ID(),-(10000+this.SortMode()),this.NameFG());
  }

In the custom event, we specify the header identifier in `lparam` and the negative sorting mode value incremented by 10,000 in `dparam`, so we know for sure that it is not a cursor coordinate. The object name (the name of the foreground canvas) is specified in `sparam`. All of this will allow the program to receive a header click event and determine its parameters and the sorting mode set for it.

In the file-handling methods, we save/load the sort control flag:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Save to file                                 |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Save(const int file_handle)
  {
//--- Save the parent object's data
   if(!CButton::Save(file_handle))
      return false;
  
//--- Save the header number
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the sort direction
   if(::FileWriteInteger(file_handle,this.m_sort_mode,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the sort control flag
   if(::FileWriteInteger(file_handle,this.m_sortable,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- Success
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Load from file                               |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Load(const int file_handle)
  {
//--- Load the parent object's data
   if(!CButton::Load(file_handle))
      return false;
      
//--- Load the header number
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the sort direction
   this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE);
//--- Load the sort control flag
   this.m_sortable=(bool)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Success
   return true;
  }

Based on this revised class, let's create a new row header class that also inherits from the abstract header class:

//+------------------------------------------------------------------+
//| Class for the visual representation of the table row header      |
//+------------------------------------------------------------------+
class CRowCaptionView : public CCaptionView
  {
protected:
   
public:
//--- Draw the visual appearance
   virtual void      Draw(const bool chart_redraw);

public:  
//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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_ROW_CAPTION_VIEW);}
  
//--- Initialize a class object
   void              Init(const string text);
   
//--- Return a description of the object
   virtual string    Description(void);
   
//--- Constructors/destructor
                     CRowCaptionView(void);
                     CRowCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); 
                    ~CRowCaptionView (void){}
  };
//+--------------------------------------------------------------------+
//| CRowCaptionView::Default constructor. Construct the object         |
//| in the main chart window of the current chart at coordinates (0,0) |
//| with default dimensions                                            |
//+--------------------------------------------------------------------+
CRowCaptionView::CRowCaptionView(void) : CCaptionView("RowCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H)
  {
//--- Initialization
   this.Init("Caption");
   this.SetID(0);
   this.SetIndex(-1);
   this.SetName("RowCaption");
   this.SetTextShiftH(8);
  }
//+----------------------------------------------------------------------------------+
//| CRowCaptionView::Parameterized constructor.                                      |
//| Construct the object in the specified chart window of the specified chart with   |
//| the specified text, coordinates, and dimensions                                  |
//+----------------------------------------------------------------------------------+
CRowCaptionView::CRowCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) :
   CCaptionView(object_name,text,chart_id,wnd,x,y,w,h)
  {
//--- Initialization
   this.Init(text);
   this.SetID(0);
   this.SetIndex(-1);
   this.SetTextShiftH(8);
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Initialization                                  |
//+------------------------------------------------------------------+
void CRowCaptionView::Init(const string text)
  {
//--- Initialize the parent object
   CCaptionView::Init(text);
//--- Fixed dimensions
   this.SetResizable(false);
   this.SetMovable(false);
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Draw the visual appearance                      |
//+------------------------------------------------------------------+
void CRowCaptionView::Draw(const bool chart_redraw)
  {
//--- If the object is outside its container, exit
   if(this.IsOutOfContainer())
      return;

//--- Fill the object with the background color, draw a light vertical line on the left and a dark one on the right
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(2),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
//--- Update the background canvas
   this.m_background.Update(false);
   
//--- Display the header text
   CLabel::Draw(false);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Return the object's description                 |
//+------------------------------------------------------------------+
string CRowCaptionView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height());
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Save to a file                                  |
//+------------------------------------------------------------------+
bool CRowCaptionView::Save(const int file_handle)
  {
//--- Save the parent object's data
   if(!CButton::Save(file_handle))
      return false;
  
//--- Save the header number
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- Completed successfully
   return true;
  }
//+------------------------------------------------------------------+
//| CRowCaptionView::Load from a file                                |
//+------------------------------------------------------------------+
bool CRowCaptionView::Load(const int file_handle)
  {
//--- Load the parent object's data
   if(!CButton::Load(file_handle))
      return false;
      
//--- Load the header number
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Completed successfully
   return true;
  }

This class is a bit simpler than the column header class, since it does not require any sorting or the display of tooltips for resizing; nor does it implement the actual adjustment of a row’s vertical size when the header's vertical size changes, as is done for the column header when its horizontal size changes. For the sake of brevity, all of this has been omitted here, but it could very well be implemented later.

Let's refine the table header visual representation class to set the table sortability flag.

Add new variables and methods for working with the sort flag:

//+------------------------------------------------------------------+
//| Table header visual representation class                         |
//+------------------------------------------------------------------+
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
   bool              m_sortable;                                     // Sort control flag

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

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

//--- Print the assigned table header model to the log
   void              TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Draw the visual appearance
   virtual void      Draw(const bool chart_redraw);

//--- (1) Set, (2) return the sortability flag
   void              SetSortableFlag(const bool flag);
   bool              IsSortabe(void)                              const { return this.m_sortable;                 }

//--- Set the sort flag for the column header
   void              SetSortedColumnCaption(const uint index);

//--- Get the column header (1) by index, (2) with the sort flag set
   CColumnCaptionView *GetColumnCaption(const uint index);
   CColumnCaptionView *GetSortedColumnCaption(void);
//--- Return the index of the column header with the sort flag set
   int               IndexSortedColumnCaption(void);
   
//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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); }
   
//--- Custom event handler for an element when the object area is clicked
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
  
//--- Initialization of (1) the class object and (2) the object's default 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){}
  };

By default, the flag is enabled in the class constructors:

//+------------------------------------------------------------------+
//| CTableHeaderView::Default constructor. Construct an object in    |
//| the main chart window of the current chart at coordinates 0,0    |
//| with default dimensions                                          |
//+------------------------------------------------------------------+
CTableHeaderView::CTableHeaderView(void) : CPanel("TableHeader","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H),m_sortable(true)
  {
//--- Initialization
   this.Init();
  }
//+--------------------------------------------------------------------------------+
//  CTableHeaderView::Parameterized constructor. Construct an object in            |
//| in the specified chart window of the specified chart with the specified text,  |
//| coordinates and dimensions                                                     |
//+--------------------------------------------------------------------------------+
CTableHeaderView::CTableHeaderView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_sortable(true)
  {
//--- Initialization
   this.Init();
  }

In the method that sets the header model, we now check this flag:

//+------------------------------------------------------------------+
//| CTableHeaderView::Sets 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 contains no 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);

//--- Loop through the column headers in the table header model
   for(int i=0;i<total;i++)
     {
      //--- Get the next column header model,
      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 visual representation object for the column header
      CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height());
      if(caption_view==NULL)
         return false;
      caption_view.SetIndex(i);
      
      //--- Assign the corresponding column header visual representation object to the current column header area
      caption_bound.AssignObject(caption_view);
      caption_view.AssignBoundNode(caption_bound);
      
      //--- For the very first column header, set the ascending sort flag
      if(i==0 && caption_view.IsSortabe())
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
     }
//--- All done successfully
   return true;
  }

Method that sets the sortability flag:

//+------------------------------------------------------------------+
//| CTableHeaderView::Sets the sortability flag                      |
//+------------------------------------------------------------------+
void CTableHeaderView::SetSortableFlag(const bool flag)
  {
//--- Save the flag value
   this.m_sortable=flag;
   
//--- In a loop over the number of column headers 
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- retrieve the next column header object and set its sort flag
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
      if(caption_view!=NULL)
         caption_view.SetSortableFlag(flag);
     }
//--- If the table is sortable, set column 0 as sorted in ascending order
   if(this.m_sortable)
      this.SetSortedColumnCaption(0);

//--- Redraw the header
   this.Draw(true);
  }

A flag is passed to the method, stored in a variable, and written to each column header. If the passed sort flag is true, set the sort direction for the very first column to ascending.
Once the flags have been set for all column headers, the entire header is redrawn.

In the element's custom event handler, when the object's area is clicked, we will perform a more thorough search for the column header index in the sparam value and take the sort flag into account:

//+------------------------------------------------------------------+
//| CTableHeaderView::Handler for the element's custom event         |
//| when clicking in the object area                                 |
//+------------------------------------------------------------------+
void CTableHeaderView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Retrieve the table header object name 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, so we exit
   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,6,capt_str.Length()-8);
   
//--- The first character before "FG" (the last digit of the target index)
   int pos=(int)capt_str.Length()-3;
   int end=pos;
   
//--- Search for all digits to the left up to the first non-digit
   while(!::IsStopped() && pos>=0 && capt_str.GetChar(pos)>='0' && capt_str.GetChar(pos)<='9')
      pos--;

//--- Start of the digits of the target index
   int start=pos+1;
//--- If the index digits are not found, exit
   if(start>end)
      return;

//--- Retrieve the index from the string
   index_str=StringSubstr(capt_str,start,end-start+1);

//--- Record the column header index
   int index=(int)::StringToInteger(index_str);
   
//--- Retrieve the column header by index
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- If the header does not have a sort flag, set the ascending sort flag
   if(caption.IsSortabe() && 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 the cursor coordinates in lparam and dparam, we will pass negative values here
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, -(10000+index), -(10000+caption.SortMode()), this.NameFG());
   ::ChartRedraw(this.m_chart_id);
  }

Based on the revised horizontal table header class, let's create a new vertical table header class:

//+------------------------------------------------------------------+
//| Visual representation class for the table row header             |
//+------------------------------------------------------------------+
class CTableRowsHeaderView : public CPanel
  {
protected:
   CRowCaptionView   m_temp_caption;                                 // Temporary row header object for searching
   string            m_table_row_columns[];                          // Array of table row headers

//--- Create a new visual representation object for the row header and adds it to the list
   CRowCaptionView *InsertNewRowCaptionView(const string text, const int x, const int y, const int w, const int h);
   
public:
//--- (1) Set the array of table row headers
   bool              TableRowCaptionsAssign(string &captions_array[]);

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

//--- Print the assigned table header model to the log
   void              TableRowHeaderModelPrint(void)                     { ::ArrayPrint(this.m_table_row_columns);       }
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Retrieve the row header by index
   CRowCaptionView  *GetRowCaption(const uint index);
   
//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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_ROWS_HEADER_VIEW);  }
   
//--- Initialization of (1) a class object and (2) the object's default colors
   void              Init(void);
   virtual void      InitColors(void);

//--- Constructors/destructor
                     CTableRowsHeaderView(void);
                     CTableRowsHeaderView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableRowsHeaderView (void){}
  };
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Default constructor. Create the object     |
//| in the main chart window of the current chart at coordinates 0,0 |
//| with default dimensions                                          |
//+------------------------------------------------------------------+
CTableRowsHeaderView::CTableRowsHeaderView(void) : CPanel("TableRowHeader","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H)
  {
//--- Initialization
   this.Init();
  }
//+-------------------------------------------------------------------------+
//| CTableRowsHeaderView::Parameterized constructor. Construct an object    |
//| in the specified window of the specified chart with the specified text, |
//| coordinates, and dimensions                                             |
//+-------------------------------------------------------------------------+
CTableRowsHeaderView::CTableRowsHeaderView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h)
  {
//--- Initialization
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Initialization                             |
//+------------------------------------------------------------------+
void CTableRowsHeaderView::Init(void)
  {
//--- Initialize the parent object
   CPanel::Init();
//--- Background color - opaque
   this.SetAlphaBG(255);
//--- Border width
   this.SetBorderWidth(1);
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Initialize the object's default colors     |
//+------------------------------------------------------------------+
void CTableRowsHeaderView::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and set the normal-state color as the current background color
   this.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrWhiteSmoke);
   this.InitBackColorsAct(C'230,230,230',C'230,230,230',C'230,230,230',clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and set the normal-state color as the current text color
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and set the normal-state color as the current border color
   this.InitBorderColors(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.InitBorderColorsAct(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.BorderColorToDefault();
   
//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrSilver);
   this.InitForeColorBlocked(clrSilver);
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Creates and adds to the list               |
//| a new row header view object                                     |
//+------------------------------------------------------------------+
CRowCaptionView *CTableRowsHeaderView::InsertNewRowCaptionView(const string text,const int x,const int y,const int w,const int h)
  {
//--- Create the object name and return the result of creating a new column header
   string user_name="RowCaptionView"+(string)this.m_list_elm.Total();
   CRowCaptionView *caption_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_ROW_CAPTION_VIEW,text,user_name,x,y,w,h);
   return(caption_view!=NULL ? caption_view : NULL);
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Set the vertical header                    |
//+------------------------------------------------------------------+
bool CTableRowsHeaderView::TableRowCaptionsAssign(string &captions_array[])
  {
//--- Get a pointer to the table object (View)
   CPanel *obj=this.GetContainer();
   if(obj==NULL)
      return false;
   CTableView *table_view=obj.GetContainer();
   if(table_view==NULL)
      return false;
      
//--- From the table object, obtain a pointer to the table row panel
   CPanel *table_area=table_view.GetTableArea();
   if(table_area==NULL)
      return false;
   
//--- Retrieve a list of table rows
   CListElm *list=table_area.GetListAttachedElements();
   int total_rows=list.Total();

//--- Save the passed array of table row headers
   ::ArrayCopy(this.m_table_row_columns,captions_array);
   int total_captions=(int)this.m_table_row_columns.Size();
//---
   int total=::fmax(total_rows,total_captions);
//--- Loop through the number of headers to be created
   for(int i=0;i<total;i++)
     {
      //--- get the next row
      CTableRowView *row=table_area.GetAttachedElementAt(i);
      if(row==NULL)
         continue;
      
      //--- Calculate the coordinate and create the name for the row header area
      int y=row.Height()*i;
      string name="CaptionBound"+(string)i;
      //--- Create a new row header area
      CBound *caption_bound=this.InsertNewBound(name,0,y,this.Width(),row.Height());
      if(caption_bound==NULL)
         return false;
      caption_bound.SetID(row.ID());
      //--- Determine the text for the row header
      //--- If the header array has fewer elements than there are rows in the table, the headers will first take values from the array and then row numbers
      //--- If the header array is empty, all rows will be headed with sequential numbers
      string text=(this.m_table_row_columns.Size()>0 ? (i<(int)this.m_table_row_columns.Size() ? this.m_table_row_columns[i] : string(i+1)) : string(i+1));
      //--- Create a new visual representation object for the row header
      CRowCaptionView *caption_view=this.InsertNewRowCaptionView(text,0,y,this.Width(),row.Height());
      if(caption_view==NULL)
         return false;
      caption_view.SetIndex(i);
      
      //--- Assign the appropriate row header visual representation object to the current row header area
      caption_bound.AssignObject(caption_view);
      caption_view.AssignBoundNode(caption_bound);
     }
//--- Everything completed successfully
   return true;
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Recalculate row header areas               |
//+------------------------------------------------------------------+
bool CTableRowsHeaderView::RecalculateBounds(CBound *bound,int new_width)
  {
//--- If an empty area object is passed, or if its width has not changed, return false
   if(bound==NULL || bound.Width()==new_width)
      return false;
      
//--- Get the index of the area in the list
   int index=this.m_list_bounds.IndexOf(bound);
   if(index==WRONG_VALUE)
      return false;

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

//--- Change the width of the current area and of 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 area by the delta value
      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 object assigned to 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 code block is part of the effort to find and eliminate artifacts that occur 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 to the next area
      next_bound=this.m_list_bounds.GetNextNode();
     }
     
//--- Calculate the new table header width 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 table header width differs from the current width, change the width
   if(header_width!=this.Width())
     {
      if(!this.ResizeW(header_width))
         return false;
     }

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

//--- From the table object, get a pointer to the table row panel
   CPanel *table_area=table_view.GetTableArea();
   if(table_area==NULL)
      return false;
   
//--- Resize the table row panel to match the total width of the column headers
   if(!table_area.ResizeW(header_width))
      return false;
   
//--- Get the list of table rows and loop through all 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)
        {
         //--- Resize the row 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;
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Get the row header by index                |
//+------------------------------------------------------------------+
CRowCaptionView *CTableRowsHeaderView::GetRowCaption(const uint index)
  {
//--- Get the row header area by index
   CBound *capt_bound=this.GetBoundAt(index);
   if(capt_bound==NULL)
      return NULL;
//--- From the row header area, return a pointer to the attached row header object
   return capt_bound.GetAssignedObj();
  }
//+------------------------------------------------------------------+
//| CTableRowsHeaderView::Draw the visual appearance                 |
//+------------------------------------------------------------------+
void CTableRowsHeaderView::Draw(const bool chart_redraw)
  {
//--- Fill the object with the background color, draw the row line, and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
   
//--- Draw the row headers
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the row header object by the loop index
      CRowCaptionView *caption_view=this.GetRowCaption(i);
      //--- Draw the visual representation of the row header
      if(caption_view!=NULL)
        {
         caption_view.Draw(false);
        }
     }
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

This class is a slightly trimmed-down version of the table column header class. The class does not use the vertical header model; instead, it uses a regular array of row header names. When creating a table, you need to specify an array of row names, and the corresponding row headers will be created. If an empty array is passed, ordinal numbers will be inserted as the text in the row headers.
All methods of this class are commented in sufficient detail, and you can study them on your own, keeping in mind that the vertical header class follows the same logic as the horizontal table header class discussed earlier, so it should already be familiar from previous articles in this series.

Let's refine the CTableView table visual representation class. We will declare new variables and methods:

//+------------------------------------------------------------------+
//| Table visual representation class                                |
//+------------------------------------------------------------------+
class CTableView : public CPanel
  {
private:
   int               m_rows_header_panel_w;        // Width when creating the table row header panel
   
protected:
//--- Retrieved table data
   CTable           *m_table_obj;                  // A pointer to a table object (includes table and header models)
   CTableModel      *m_table_model;                // A pointer to the table model (obtained from CTable)
   CTableHeader     *m_header_model;               // A pointer to the table header model (obtained from CTable)
   
//--- View component data
   CPanel           *m_header_panel;               // Panel for holding the table header
   CTableHeaderView *m_header_view;                // Pointer to the table header (View)
   CPanel           *m_rows_header_panel;          // Panel for holding the table row header
   CTableRowsHeaderView *m_rows_header_view;       // Pointer to the table row header (View)
   CPanel           *m_table_area;                 // Panel for holding table rows
   CContainer       *m_table_area_container;       // Container for holding the panel with table rows
   bool              m_sortable;                   // Table sortability flag
   
//--- (1) Set, (2) return the table model
   bool              TableModelAssign(CTableModel *table_model);
   CTableModel      *GetTableModel(void)                                { return this.m_table_model;           }
   
//--- (1) Set, (2) return the table header model
   bool              HeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetHeaderModel(void)                               { return this.m_header_model;          }
   
//--- (1) Set the required size of the row header panel, (2) return the width of the table row header
   void              SetRowsHeaderPanelSize(const int width)            { this.m_rows_header_panel_w=width;    }
   int               RowsHeaderWidth(void) const
                       {
                        return(this.m_rows_header_view!=NULL ? this.m_rows_header_view.Width() : 0);
                       }

//--- Create an object from the model: (1) a table, (2–3) a header, (4) update the modified table
   bool              CreateTable(void);
   bool              CreateHeader(void);
public:
   bool              CreateRowsHeader(string &captions_array[]);
   bool              UpdateTable(void);
   

//--- (1) Set, (2) return a table object
   bool              TableObjectAssign(CTable *table_obj);
   CTable           *GetTableObj(void)                                  { return this.m_table_obj;             }

//--- Return (1–2) the header, (3) the table placement area, (4) the table area container
   CTableHeaderView *GetHeader(void)                                    { return this.m_header_view;           }
   CTableRowsHeaderView *GetRowsHeader(void)                            { return this.m_rows_header_view;      }
   CPanel           *GetTableArea(void)                                 { return this.m_table_area;            }
   CContainer       *GetTableAreaContainer(void)                        { return this.m_table_area_container;  }

//--- Print the assigned model for (1) the table, (2) the header, and (3) the table object to the log
   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);
   
//--- Retrieve the column header (1) by index, (2) with the sort 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);     }

//--- Return a visual representation object for the specified (1) row or (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);  }
                       
//--- Return the number of cells in the specified table row
   int               CellsInRow(const uint row)
                       { return(this.GetRowView(row)!=NULL ? this.GetRowView(row).CellsTotal() : 0);           }

//--- Set the row highlighting method
   void              SetRowsHighlightMode(const ENUM_ROWS_HIGHLIGHT_MODE mode);
   
//--- (1) Set, (2) return the table sortability flag
   void              SetSortable(const bool flag);
   bool              IsSortable(void)                             const { return this.m_sortable;              }
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods: (1) comparison, (2) saving to a file, (3) loading from a 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);     }
   
//--- Custom event handler for an element when the object area is clicked
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Sort the table by column value and sort direction
   bool              Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode);
  
//--- Initialization of (1) the class object and (2) the object's default 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){}
  };

In the class constructors, we initialize new variables:

//+-----------------------------------------------------------------------+
//| CTableView::Default constructor.                                      |
//| Construct an element in the main chart window of the current chart    |
//| at coordinates 0,0 with default dimensions                            |
//+-----------------------------------------------------------------------+
CTableView::CTableView(void) : CPanel("TableView","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H),
   m_table_model(NULL),m_header_model(NULL),m_table_obj(NULL),m_header_view(NULL),m_rows_header_view(NULL),
   m_table_area(NULL),m_table_area_container(NULL),m_rows_header_panel_w(0),m_sortable(true)
  {
//--- Initialization
   this.Init();
  }
//+-----------------------------------------------------------------------------+
//| CTableView::Parameterized constructor.                                      |
//| Construct an element in the specified chart window of the specified chart   |
//| with the specified text, coordinates, and dimensions                        |
//+-----------------------------------------------------------------------------+
CTableView::CTableView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_table_model(NULL),m_header_model(NULL),m_rows_header_view(NULL),m_table_obj(NULL),m_header_view(NULL),
   m_table_area(NULL),m_table_area_container(NULL),m_rows_header_panel_w(0),m_sortable(true)
  {
//--- Initialization
   this.Init();
  }

The initialization method creates a table object with all of its components—the table header panels and the container with the table row panel. The table headers are now located on their own panels, making it easy to shift them while clipping them to the panel boundaries, since the panel implements functionality for clipping attached elements to its boundaries if this object extends beyond its panel:

//+------------------------------------------------------------------+
//| CTableView::Initialization                                       |
//+------------------------------------------------------------------+
void CTableView::Init(void)
  {
//--- Parent object initialization
   CPanel::Init();
//--- Border width, opacity
   this.SetBorderWidth(1);
   this.SetAlphaBG(255);
   this.SetAlphaFG(255);
//--- Initialize the panel background colors and set the current background color
   this.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrSilver);
   this.BackColorToDefault();
//--- Initialize the panel border colors and set the current border color
   this.InitBorderColors(C'180,180,180',C'180,180,180',C'180,180,180',clrSilver);
   this.BorderColorToDefault();
   
//--- X-coordinate offset for the header and table rows (width of the vertical row header)
   int dx=(int)::StringToInteger(this.Text());

   this.m_rows_header_panel_w=dx;
   this.SetText("");
   if(dx>DEF_TABLE_ROWS_HEADER_W)
      dx+=12;
   
//--- Coordinates and dimensions of the table header panel (horizontal header)
   int x=1+dx;
   int y=1;
   int w=this.Width()-2-dx;
   int h=DEF_TABLE_HEADER_H;
//--- Create a panel for the table header
   this.m_header_panel=this.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableHeaderPanel",x,y,w,h);
   if(this.m_header_panel==NULL)
      return;
//--- Initialize the panel background colors and set the current background color
   this.m_header_panel.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrSilver);
   this.m_header_panel.BackColorToDefault();
   this.m_header_panel.SetBorderWidth(0);
   this.m_header_panel.SetAlphaBG(255);
//--- Create a table header
   this.m_header_view=this.m_header_panel.InsertNewElement(ELEMENT_TYPE_TABLE_HEADER_VIEW,"","TableHeader",0,0,this.m_header_panel.Width(),this.m_header_panel.Height());
   if(this.m_header_view==NULL)
      return;
   this.m_header_view.SetBorderWidth(0);
   
//--- Coordinates and dimensions of the panel for the table row header (vertical header)
   x=1;
   y=DEF_TABLE_HEADER_H;
   w=(dx>0 ? dx : 1);
   h=this.Height()-2-DEF_TABLE_HEADER_H;
//--- Create a panel
   this.m_rows_header_panel=this.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableRowsHeaderPanel",x,y,w,h);
   if(this.m_rows_header_panel==NULL)
      return;
//--- Initialize the panel's background color and set it as the current background color
   this.m_rows_header_panel.InitBackColors(C'230,230,230',C'230,230,230',C'230,230,230',clrSilver);
   this.m_rows_header_panel.BackColorToDefault();
   this.m_rows_header_panel.SetBorderWidth(0);
   this.m_rows_header_panel.SetAlphaBG(255);
//--- Create the table row header
   this.m_rows_header_view=this.m_rows_header_panel.InsertNewElement(ELEMENT_TYPE_TABLE_ROWS_HEADER_VIEW,"","TableRowsHeader",0,0,this.m_rows_header_panel.Width(),this.m_rows_header_panel.Height());
   if(this.m_rows_header_view==NULL)
      return;
   this.m_rows_header_view.SetBorderWidth(0);
   this.m_rows_header_view.SetAlphaBG(0);
   if(this.m_rows_header_panel_w==0)
      this.m_rows_header_view.Hide(false);
   
//--- Coordinates and dimensions of the container that will hold the table row panel
   x=1+dx;
   y=1+DEF_TABLE_HEADER_H;
   w=this.Width()-2-dx;
   h=this.Height()-2-DEF_TABLE_HEADER_H;
//--- Create a container
   this.m_table_area_container=this.InsertNewElement(ELEMENT_TYPE_CONTAINER,"","TableAreaContainer",x,y,w,h);
   if(this.m_table_area_container==NULL)
      return;
   this.m_table_area_container.SetBorderWidth(0);
   this.m_table_area_container.SetScrollable(true);
   
//--- Attach the panel for storing table rows to the container
   this.m_table_area=this.m_table_area_container.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableAreaPanel",0,0,this.m_table_area_container.Width()-0,this.m_table_area_container.Height()-0);

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

A method that creates a table row header object:

//+------------------------------------------------------------------+
//| CTableView::Create a table row header object                     |
//+------------------------------------------------------------------+
bool CTableView::CreateRowsHeader(string &captions_array[])
  {
   if(this.m_rows_header_view==NULL)
     {
      ::PrintFormat("%s: Error. Table rows header object not created",__FUNCTION__);
      return false;
     }
   return this.m_rows_header_view.TableRowCaptionsAssign(captions_array);
  }

The method takes an array of row header names as a parameter and returns the result of assigning this array to the row header object of the CTableRowsHeaderView class.

Let's refine the method that draws the appearance:

//+------------------------------------------------------------------+
//| CTableView::Render the appearance                                |
//+------------------------------------------------------------------+
void CTableView::Draw(const bool chart_redraw)
  {
//--- Draw the base
   CPanel::Draw(false);
//--- Draw the header and table rows
   if(this.m_header_view!=NULL)
      this.m_header_view.Draw(false);
   if(this.m_table_area_container!=NULL)
      this.m_table_area_container.Draw(false);
      
//--- Set the offset and dimensions of the image area
   int x=this.m_rows_header_panel.Width()-16;
   int y=this.m_header_panel.Height()-16;
   int w=11;
   int h=w;
//--- Clear the area and draw the corner
   m_painter.Clear(x,y,w,h,false);
   m_painter.TriangleRB(x,y,w,h,BorderColor(),AlphaFG(),true);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Now, in addition to drawing the table header and the container with the panel of table rows, the method also draws a filled triangle in the upper-left corner of the panel.

A method that sets the row highlighting mode:

//+------------------------------------------------------------------+
//| CTableView::Set the row highlighting mode                        |
//+------------------------------------------------------------------+
void CTableView::SetRowsHighlightMode(const ENUM_ROWS_HIGHLIGHT_MODE highlight_mode)
  {
   int total=this.RowsTotal();
   for(int i=0;i<total;i++)
     {
      CTableRowView *row=this.GetRowView(i);
      if(row!=NULL)
         row.SetHighlightMode(highlight_mode);
     }
  }

In a loop over the number of table rows, we retrieve the next table row object and set the specified highlighting mode for it.

Method that sets the table sortability flag:

//+------------------------------------------------------------------+
//| CTableView::Set the table sortability flag                       |
//+------------------------------------------------------------------+
void CTableView::SetSortable(const bool flag)
  {
   this.m_sortable=flag;
   CTableHeaderView *header=this.GetHeader();
   if(header!=NULL)
      header.SetSortableFlag(flag);
  }

The sort flag is passed to the method and set both in the m_sortable variable and in the table's horizontal header object.

In the table sorting method, we now take into account the sortability flag set for the table:

//+------------------------------------------------------------------+
//| CTableView::Sort the table by column value and sort direction    |
//+------------------------------------------------------------------+
bool CTableView::Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode)
  {
//--- If no table model is assigned, report this and return false
   if(this.m_table_model==NULL)
     {
      ::PrintFormat("%s: Error. The table model is not assigned. Please use the TableObjectAssign() method first",__FUNCTION__);
      return false;
     }

//--- If the table has no header or sorting is disabled, return 'false'
   if(this.m_header_model==NULL || !this.m_sortable || sort_mode==TABLE_SORT_MODE_NONE)
      return false;
   
//--- Set the sort direction flag and sort the table model by the specified column and direction
   bool descending=(sort_mode==TABLE_SORT_MODE_DESC);
   this.m_table_model.SortByColumn(column,descending);
//--- Success
   return true;
  }

Now let's refine the table management class.

To create tables that have only the top horizontal header, the class defines four public methods and one protected method that creates a table and adds it to the list of created tables. This last method needs to be modified so that it also accepts an array of row header names. We will add four more public methods for creating tables with two headers: a horizontal header and a vertical header. This will allow us to create tables with and without a vertical header.

Let's declare additional methods in the class:

//+------------------------------------------------------------------+
//| Table Management Class                                           | 
//+------------------------------------------------------------------+
class CTableControl : public CPanel
  {
private:
//--- Return the maximum value of an integer array
   bool              ArrayMaximumValue(int &array[],int &value);
//--- Return the maximum text width in the array of row headers
   int               GetMaximumRowCaptionTextSize(string &array_row_captions[]);
   
protected:
   CListObj          m_list_table_model;
//--- Add the table's (1) model object (CTable) and (2) visual representation object (CTableView) to the list
   bool              TableModelAdd(CTable *table_model,const int table_id,const string source);
   CTableView       *TableViewAdd(CTable *table_model,string &row_names[],const string source);
//--- Update the specified column in the specified table
   bool              ColumnUpdate(const string source, CTable *table_model, const uint table, const uint col, const bool cells_redraw);
   
public:
//--- Return (1) the model, (2) the table visual representation object, and (3) the object type
   CTable           *GetTableModel(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 provided 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);
template<typename T>
   CTableView       *TableCreate(T &row_data[][],const string &column_names[],string &row_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const uint num_rows, const uint num_columns,string &row_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const matrix &row_data,const string &column_names[],string &row_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(CList &row_data,const string &column_names[],string &row_names[],const int table_id=WRONG_VALUE);
   
//--- Return (1) the string value of the specified cell (Model), (2) the specified row, 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 (1) the value, (2) the precision, (3) the time display flags, and (4) the flag for displaying color names for 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);

//--- Sets the (1) foreground and (2) background colors for the specified cell (View)
   void              CellSetForeColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw);
   void              CellSetBackColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw);
   
//--- (1) Set and (2) return the text anchor point for 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 (1) the precision, (2) the time display flags, (3) the color name display flag, (4) the text anchor point, and (5) the data type for 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);

//--- Return the number of (1) rows and (2) cells per row in the specified table
   uint              RowsTotal(const uint table);
   uint              CellsInRow(const uint table,const uint row);

//--- Set (1) the row highlighting mode and (2) the sortability of the specified table
   void              SetRowsHighlightMode(const uint table,const ENUM_ROWS_HIGHLIGHT_MODE highlight_mode);
   void              SetSortable(const uint table,const bool flag);
   
//--- 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) {}
  };

Let's look at the implementation of the new methods.

A method that returns the maximum value of an integer array:

//+------------------------------------------------------------------+
//| Return the maximum value of an integer array                     |
//+------------------------------------------------------------------+
bool CTableControl::ArrayMaximumValue(int &array[],int &value)
  {
   ::ResetLastError();
   int index=::ArrayMaximum(array);
   if(index<0)
     {
      ::PrintFormat("%s: ArrayMaximum() failed. Error %d",__FUNCTION__,::GetLastError());
      return false;
     }
   value=array[index];
   return true;
  }

The method is passed an array in which the maximum value must be found, and a variable in which that value will be stored. The method returns true and assigns the maximum value found in the array to the variable.
If an error occurs, it returns false.

A method that returns the maximum text width in an array of row headers:

//+------------------------------------------------------------------+
//| Return the maximum text width in the array of row headers        |
//+------------------------------------------------------------------+
int CTableControl::GetMaximumRowCaptionTextSize(string &row_captions[])
  {
   int total=(int)row_captions.Size();
   if(total==0)
      return 0;

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

When creating a vertical header, you need to calculate its width so that all row names fit within the width of the row headers being created. This method determines the maximum text width in an array of row header names passed to the method by reference. If an error occurs, the method returns 0.

Let's refine the method that creates a new table visual representation object and adds it to the list:

//+------------------------------------------------------------------+
//| Create a new object and adds it to the list                      |
//| visual representation of the table (CTableView)                  |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableViewAdd(CTable *table_model,string &row_names[],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;
     }
//--- Get the maximum text width of the row headers
   int w=this.GetMaximumRowCaptionTextSize(row_names);
   if(w>0 && w<DEF_TABLE_ROWS_HEADER_W)
      w=DEF_TABLE_ROWS_HEADER_W;
      
//--- Create a new element—a table visual representation attached to the panel
   CTableView *table_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_VIEW,(string)w,"TableView"+(string)table_model.ID(),1,1,this.Width()-2,this.Height()-2);
   if(table_view==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to create Table View object",source,__FUNCTION__);
      return NULL;
     }
//--- Assign the table object (Model) and its identifier to the "Table" (View) graphical element
   table_view.TableObjectAssign(table_model);
   table_view.CreateRowsHeader(row_names);
   table_view.SetID(table_model.ID());
   return table_view;
  }

Now, an array of row header names is additionally passed to the method. Next, the width of the table's vertical header object is calculated (either 0 if the array is empty, or otherwise a width of at least DEF_TABLE_ROWS_HEADER_W). When creating a table visual representation object, the calculated width of the vertical header is passed to the panel text parameter as a string value (after the table is created using the vertical header width, an empty string is written to the panel text parameter). After a table object is created, its method for creating a vertical header is called.

Let's refine the four public methods for creating a table visual representation object:

//| Create a table by specifying a table array and a header array.      | 
//| Determine the number and names of the columns based on column_names |
//| The number of rows is determined by the size of the row_data array, |
//| which is also used to populate 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 based on the specified parameters
   CTable *table_model=new CTable(row_data,column_names);
//--- If errors occur while creating or adding the table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return a table with an empty row header array
   string array[]={};
   return this.TableViewAdd(table_model,array,__FUNCTION__);
  }
//+------------------------------------------------------------------+
//| Create a table with a specified number of columns and rows.      |
//| The columns will have Excel-style 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 errors occur while creating or adding the table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return a table with an empty row header array
   string array[]={};
   return this.TableViewAdd(table_model,array,__FUNCTION__);
  }
//+----------------------------------------------------------------------------+
//| Create a table with columns initialized according to column_names          |
//| The number of rows is determined by the row_data parameter of type matrix  |
//+----------------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(row_data,column_names);
//--- If errors occur while creating or adding the table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return a table with an empty row header array
   string array[]={};
   return this.TableViewAdd(table_model,array,__FUNCTION__);
  }
//+-----------------------------------------------------------------------+
//| Create a table by specifying a table array based on                   |
//| the row_data list, which contains objects with structure field data.  | 
//| Determine the number and names of columns based on the number of      |
//| column names in the column_names array                                |
//+-----------------------------------------------------------------------+
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 errors occur while creating or adding the table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return a table with an empty row header array
   string array[]={};
   return this.TableViewAdd(table_model,array,__FUNCTION__);
  }

Since the TableViewAdd() method now requires an array of row headers to be passed to it, and tables here are created without a vertical header, we simply declare an empty array and pass it to the method for creating a new table.

Let's write four overloaded methods for creating tables with a vertical header:

//+-------------------------------------------------------------------------+
//| Create a table by specifying a table array and an array of row headers. | 
//| Determine the number and names of columns based on column_names         |
//| The number of rows is determined by the size of the row_data array,     |
//| which is also used to populate the table                                |
//+-------------------------------------------------------------------------+
template<typename T>
CTableView *CTableControl::TableCreate(T &row_data[][],const string &column_names[],string &row_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,row_names,__FUNCTION__);
  }
//+------------------------------------------------------------------+
//| Create a table with a specified number of columns and rows.      |
//| The columns will have Excel-style names: "A", "B", "C", etc.     |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const uint num_rows,const uint num_columns,string &row_names[],const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(num_rows,num_columns);
//--- 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,row_names,__FUNCTION__);
  }
//+----------------------------------------------------------------------------+
//| Create a table with columns initialized according to column_names          |
//| The number of rows is determined by the row_data parameter of type matrix  |
//+----------------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],string &row_names[],const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(row_data,column_names);
//--- 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,row_names,__FUNCTION__);
  }
//+-----------------------------------------------------------------------+
//| Create a table by specifying the table array based on                 |
//| the row_data list, which contains objects with structure field data.  | 
//| Determine the number and names of columns based on the number         |
//| of column names in the column_names array                             |
//+-----------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(CList &row_data,const string &column_names[],string &row_names[],const int table_id=WRONG_VALUE)
  {
   CTableByParam *table_model=new CTableByParam(row_data,column_names);
//--- 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,row_names,__FUNCTION__);
  }

Here, an array of row header names is passed to the methods, and the same array is passed to the method that creates a new table object.

Method that sets the background color for the specified cell:

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

The method logic is fully commented in the code.

A method that returns the number of rows in the specified table:

//+------------------------------------------------------------------+
//| Return the number of rows in the specified table                 |
//+------------------------------------------------------------------+
uint CTableControl::RowsTotal(const uint table)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return NULL;
     }
   return table_view.RowsTotal();
  }

We retrieve a table object by its index in the list and return the number of table rows from it.

A method that returns the number of cells in a row in the specified table:

//+------------------------------------------------------------------+
//| Return the number of cells in a row in the specified table       |
//+------------------------------------------------------------------+
uint CTableControl::CellsInRow(const uint table,const uint row)
  {
   CTableRowView *row_view=this.GetRowView(table,row);
   return(row_view!=NULL ? row_view.CellsTotal() : 0);
  }

We retrieve the table by index and return the number of cells in the specified row of this table.

A method that sets the row highlighting mode for the specified table:

//+------------------------------------------------------------------+
//| Set the row highlighting mode for the specified table            |
//+------------------------------------------------------------------+
void CTableControl::SetRowsHighlightMode(const uint table,const ENUM_ROWS_HIGHLIGHT_MODE highlight_mode)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return;
     }
   table_view.SetRowsHighlightMode(highlight_mode);
  }

We retrieve a table object by its index and set the table row highlighting mode for it.

A method that sets sortability for the specified table:

//+------------------------------------------------------------------+
//| Set sortability for the specified table                          |
//+------------------------------------------------------------------+
void CTableControl::SetSortable(const uint table,const bool flag)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return;
     }
   table_view.SetSortable(flag);
  }

We retrieve a table object by its index and set its column sortability flag.

That's it — we have finished updating all the table classes of the graphical library. Now let's create an indicator that displays, in a table, the symmetric correlation between the symbols specified in the input parameters.


Tabular Symbol Correlation Indicator

The indicator will be located in the \MQL5\Indicators\Tables\ folder.

Let's create a new indicator in this folder named iCorrelationTable.mq5, which displays data in the chart subwindow and has the following properties, input parameters, and global variables:

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

#define   CHART_FLOAT_WIDTH   750                                    // Opened symbol chart width
#define   CHART_FLOAT_HEIGHT  500                                    // Opened symbol chart height

//+------------------------------------------------------------------+
//| Included libraries                                               |
//+------------------------------------------------------------------+
#include "Controls\Controls.mqh"    // Control library

//--- input parameters
input(name="Bars Total (at least 10)") uint              InpBarsTotal   =  1000;                                                 // Number of data bars for correlation calculation (at least 10)
input(name="Timeframe")                ENUM_TIMEFRAMES   InpTimeframe   =  PERIOD_CURRENT;                                       // Data timeframe for calculating correlation
input(name="Symbols for Correlation")  string            InpSymbols     =  "EURUSD,GBPUSD,USDJPY,USDCHF,AUDUSD,NZDUSD,USDCAD";   // Symbols for calculating correlation

//--- global variables
string   ExtSymbolsArray[];                                          // Array of symbols for calculating correlation
matrix   ExtPricesData;                                              // Symbol data matrix (Close prices)
uint     ExtBarsTotal;                                               // Number of data bars used to calculate the correlation
matrix   ExtCorrelationMatrix;                                       // Matrix of calculated pairwise correlations between all symbols
bool     ExtDataReady;                                               // Data readiness flag for all symbols
long     ExtSymbolsChart;                                            // Identifier of the new chart for correlation symbols
CTableControl *ExtTableCtrl;                                         // A pointer to a CTableControl object
CTableView *ExtTableView;                                            // Pointer to the table visual representation object

In the indicator's OnInit() handler, we will create a list of symbols from those specified in the input parameters and request their data:

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

//--- ID of the chart being opened
   ExtSymbolsChart=0;

//--- Find the chart subwindow
   int wnd=ChartWindowFind();

//--- Populate an array of symbols from those specified in the InpSymbols input parameter
   string sep=",";   // a separator as a character
   ushort u_sep;     // separator character code
//--- Get the separator code 
   u_sep=StringGetCharacter(sep,0); 
//--- Extract substrings from the InpSymbols string using the u_sep separator and store them in the ExtSymbolsArray array
   StringSplit(InpSymbols,u_sep,ExtSymbolsArray);
   
//--- Print the set of symbols for calculating the correlation to the log
   Print("\nSymbols Array:");
   ArrayPrint(ExtSymbolsArray);
   
//--- Enable all symbols in Market Watch
   SymbolsSelect(ExtSymbolsArray);

//--- Get symbol data (at least 10 bars) to calculate symbol correlation
   ExtBarsTotal=(InpBarsTotal<10 ? 10 : InpBarsTotal);
   ExtDataReady=GetAndCalculateData(ExtBarsTotal);
   
//--- Create a graphical table control
   int w=500;
   int h=138;
   ExtTableCtrl=new CTableControl("TableControl0",0,wnd,8,8,w-0,h-0);
   if(ExtTableCtrl==NULL)
     {
      Print("Error. Failed to create TableControl object");
      return INIT_FAILED;
     }

//--- There must be one main element on the chart
   ExtTableCtrl.SetAsMain();

//--- You can set the parameters of the created table control
   ExtTableCtrl.SetID(0);                      // Identifier 
   ExtTableCtrl.SetName("Table Control 0");    // Name

//--- If the symbol data and their correlations have been obtained successfully,
//--- create table object 0 (Model + View component) inside the table control
//--- from the ExtCorrelationMatrixSymmetric matrix created above and
//--- the ExtSymbolsArray string array as column headers
   if(ExtDataReady && !CreateTable(ExtTableCtrl))
      return INIT_FAILED;
   
//--- Everything completed successfully
   return(INIT_SUCCEEDED);
  }

After requesting the symbol data, we will create a table control. If all data for all symbols have been successfully obtained and a symmetric correlation matrix has been created from them, we build a single table in the table control based on these data. If the data has not yet been received, this will be indicated by the ExtDataReady flag; in this case, in OnCalculate() we will wait until all requested data is received, and after it has been fully received and the correlation matrix has been calculated, we will create the table and then only update the data in the already built table of symbols and their correlations:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//--- Retrieve data until it is ready
   ExtDataReady=GetAndCalculateData(ExtBarsTotal);
   if(!ExtDataReady)
     {
      Print("The symbol data and their correlations have not yet been obtained. Waiting for the next tick...");
      return 0;
     }
   
//--- If the table has not been created yet
//--- create table object 0 (Model + View component) inside the table control
//--- from the ExtCorrelationMatrixSymmetric matrix created above and
//--- the ExtSymbolsArray string array as column headers
   if(ExtTableView==NULL && !CreateTable(ExtTableCtrl))
      return 0;

//--- Update the data in the table and set the correlation colors
   UpdateTableValuesAndColors(ExtTableCtrl.GetTableView(0),ExtCorrelationMatrix);

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

In the OnDeinit() handler, we delete the created objects:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int32_t reason)
  {
//--- Delete the table control and destroy the library's shared resource manager
   delete ExtTableCtrl;
   CCommonManager::DestroyInstance();
  }

In the indicator timer, we request data for all symbols stored in the working array every minute and a half:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Every minute and a half, get symbol data for the symbols in the array
   static int count=0;
   count++;
   if(count>=3000)
     {
      double array[];
      for(int i=0;i<(int)ExtSymbolsArray.Size();i++)
         CopyClose(ExtSymbolsArray[i],InpTimeframe,0,ExtBarsTotal,array);
      count=0;
     }
//--- Call the OnTimer handler of the table control
   ExtTableCtrl.OnTimer();
  }

This is a multi-symbol indicator, so symbol data must be requested to "keep" the data for each symbol, so that it is always up to date and does not have to be loaded again.

In the event handler, we will track clicks on table cells, determine the cell address (row/column) from the sparam parameter, and open two charts for these symbols in a separate chart window:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Call the OnChartEvent handler of the table control
   ExtTableCtrl.OnChartEvent(id,lparam,dparam,sparam);
   if(id>=CHARTEVENT_CUSTOM)
     {
      //--- Convert the ID of the received custom event to standard event values
      ENUM_CHART_EVENT chart_event=ENUM_CHART_EVENT(id-CHARTEVENT_CUSTOM);
      
      //--- If this is a graphical object click event
      if(chart_event==CHARTEVENT_OBJECT_CLICK)
        {
         //--- If the event name (sparam value) contains the table row name (starts with "TableCellView")
         if(StringFind(sparam,"TableCellView")==0)
           {
            //--- Retrieve the row and column numbers from the event parameters
            int row=(int)lparam;
            int col=(int)dparam;
            
            string sep=";";                  // a separator as a character 
            ushort u_sep;                    // separator character code 
            string result[];                 // an array for receiving strings 
            
            //--- Get the separator code and split sparam into substrings
            u_sep=StringGetCharacter(sep,0); 
            int n=StringSplit(sparam,u_sep,result);
            
            //--- There should be three substrings
            if(n==3)
              {
               //--- Get the row symbol and column symbol
               string row_symb=result[1];
               string col_symb=result[2];
               
               //--- If the chart is not open yet, open it
               if(ExtSymbolsChart==0 || !IsExistChart(ExtSymbolsChart))
                  ExtSymbolsChart=OpenCharts(row_symb,col_symb);

               //--- If the chart is already open
               if(ExtSymbolsChart!=0)
                 {
                  //--- Set the symbols for the two chart objects and redraw the chart
                  ObjectSetString(ExtSymbolsChart,"ChartRowSymbol",OBJPROP_SYMBOL,row_symb);
                  ObjectSetString(ExtSymbolsChart,"ChartColSymbol",OBJPROP_SYMBOL,col_symb);
                  ChartRedraw(ExtSymbolsChart);
                 }
              }
           }
        }
     }
  }

In other words, we can click the table cell of interest, which contains the symbol correlation value for two symbols (at the intersection of a row and a column), and open the charts for those symbols.

Let's take a look at the functions used in the indicator.

Function that adds symbols from an array to Market Watch:

//+------------------------------------------------------------------+
//| Add symbols from the array to Market Watch                       |
//+------------------------------------------------------------------+
bool SymbolsSelect(string &array[])
  {
   bool res=true;
   for(int i=0;i<(int)array.Size();i++)
      res &=SymbolSelect(array[i],true);
   return res;
  }

We iterate through the array of symbols and add each symbol to Market Watch. The function returns the overall result of adding the symbols to Market Watch. Here, and when creating an array of symbols from the input parameters, there is no check to see whether the symbols exist on the server. This is done to simplify the example. Therefore, you must specify valid, existing symbols in the settings.

Function that returns a symbol by array index:

//+------------------------------------------------------------------+
//| Return the symbol by array index                                 |
//+------------------------------------------------------------------+
string GetSymbolByIndex(const int index,string &array[])
  {
   int total=(int)array.Size();
   if(index<0 || index>total-1)
      return StringFormat("%s: Error. Invalid index (%d)",__FUNCTION__,index);
   return array[index];
  }

Function that fills the symbol data matrix:

//+------------------------------------------------------------------+
//| Fill the symbol data matrix                                      |
//+------------------------------------------------------------------+
bool SymbolsDataMatrixFill(const ENUM_TIMEFRAMES timeframe,string &array[],matrix &data,const int data_count)
  {
//--- Loop over the number of symbols in the array variable
   int total=(int)array.Size();
   for(int i=0; i<total; i++)
     {
      //--- obtain data_count Close prices
      double close[];
      int copied=CopyClose(array[i], timeframe, 0, data_count, close);
      if(copied!=data_count)
         return false;
      
      //--- write the prices to the matrix row so that cell 0 of the row corresponds to bar 0
      for(int j=0; j<data_count; j++)
        {
         string symbol=GetSymbolByIndex(i,array);
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
         data[i][data_count-1-j]=NormalizeDouble(close[j],digits);
        }
     }
   return true;
  }

The function takes the timeframe of the data to be obtained, the array of symbols from which the data is taken, the matrix to which the data will be written, and the number of data items to be taken for each symbol stored in the array.

There is one limitation here: for the sake of brevity, this code does not check the amount of data available on the server for each symbol. Here, the value from the InpBarsTotal indicator setting is simply used, but it must be no fewer than 10 bars. If the function always ends with an error, it means that there is not enough data for one of the symbols. In this case, you can reduce the amount of data requested in the InpBarsTotal variable.

Function that calculates a symmetric correlation matrix between all symbols in an array:

//+------------------------------------------------------------------+
//|Calculate the symmetric correlation matrix between all symbols    |
//+------------------------------------------------------------------+
bool SymbolsCorrelationMatrixSymmetric(const matrix &data, matrix &correlation)
  {
   int symb_total=(int)data.Rows();    // number of symbols

//--- Set the size of the correlation matrix
   if(!correlation.Resize(symb_total,symb_total))
      return false;

//--- Outer loop over all symbols (rows)
   for(int i=0;i<symb_total;i++)
     {
      //--- Get the price time series for symbol i
      vector vi=data.Row(i);
      //--- Inner loop over all symbols (columns)
      for(int j=0;j<symb_total;j++)
        {
         //--- If the symbols in the row and column are the same, this is self-correlation
         if(i==j)
            correlation[i][j]=1.0;
         
         //--- The symbols in the row and column are different
         else
           {
            //--- Get the price time series for symbol j and calculate the correlation between symbols i and j
            vector vj=data.Row(j);
            correlation[i][j]=vi.CorrCoef(vj);
           }
        }
     }
//--- Completed successfully
   return true;
  }

The function receives a previously filled symbol data matrix and a matrix to which the symbol correlation values will be written. The logic of the function is explained in the code comments.

A function that prints a symmetric correlation matrix to the log:

//+------------------------------------------------------------------+
//| Print a symmetric correlation matrix to the log                  |
//+------------------------------------------------------------------+
void SymbolsCorrelationMatrixSymmetricPrint(const string &symb_array[],matrix &correlation)
  {
//--- Create and print the header
   Print("Correlation matrix:");
   string header="        ";
   for(int j=0;j<(int)symb_array.Size();j++)
      header+=symb_array[j]+" ";
   Print(header);

//--- Print symbol correlation data
   for(int i=0;i<(int)symb_array.Size();i++)
     {
      string row=symb_array[i]+" ";
      for(int j=0;j<(int)symb_array.Size();j++)
         row+=DoubleToString(correlation[i][j],2)+" ";
      Print(row);
     }
  }

A list of symbols and the calculated correlation matrix are passed to the function. In two loops, we print the symbols and the correlation values between them.

A function that retrieves and calculates all necessary data for the symbols:

//+------------------------------------------------------------------+
//| Retrieve and calculates all necessary data                       |
//+------------------------------------------------------------------+
bool GetAndCalculateData(uint bars_total)
  {
//--- Retrieve values for the symbol data
   const int symb_total=(int)ExtSymbolsArray.Size();     // number of symbols

//--- Resize the matrix: rows = symbols, columns = bars
   if(!ExtPricesData.Resize(symb_total,bars_total))
     {
      Print("Error. Failed to resize the symbol data matrix");
      return false;
     }

//--- Fill the matrix with the symbols' Close prices and set the data readiness flag
   ExtDataReady=SymbolsDataMatrixFill(InpTimeframe,ExtSymbolsArray,ExtPricesData,bars_total);
   if(!ExtDataReady)
      return false;
      
//--- Calculate a symmetric correlation matrix
   if(!SymbolsCorrelationMatrixSymmetric(ExtPricesData,ExtCorrelationMatrix))
     {
      Print("Error calculating correlation matrix");
      return false;
     }
   return true; 
  }

The amount of data requested is passed to the function. For each symbol, the specified amount of data is requested. If the data is successfully received, a symbol correlation matrix is calculated. If all data is successfully retrieved and processed, the function returns true; otherwise, it returns false. The function must be called until all the data has been received and processed. This is done first during initialization, and then on every tick. Once all data has been successfully retrieved and processed, there is no further need to keep calling this function for initialization purposes.

A function that creates a table of symbols with their correlation data on the panel:

//+----------------------------------------------------------------------+
//| Create a table of symbols with their correlation data on the panel   |
//+----------------------------------------------------------------------+
bool CreateTable(CTableControl *table_ctrl)
  {
//--- create table object 0 (Model + View component) inside the table control
//--- from the ExtCorrelationMatrixSymmetric matrix created above and
//--- the ExtSymbolsArray string array of symbols as column headers
   ExtTableView=table_ctrl.TableCreate(ExtCorrelationMatrix,ExtSymbolsArray,ExtSymbolsArray);
   if(ExtTableView==NULL)
      return false;
   
//--- Let's center the text in each cell for the columns
   int total=(int)table_ctrl.RowsTotal(0);
   for(int i=0;i<total;i++)
     {
      table_ctrl.ColumnSetTextAnchor(0,i,ANCHOR_CENTER,true,false);
     }
   
//--- Set the table row/cell highlighting mode for individual cells
   table_ctrl.SetRowsHighlightMode(0,ROWS_HIGHLIGHT_MODE_CELLS);
//--- and make the table non-sortable
   table_ctrl.SetSortable(0,false);
   
//--- Let's draw and color the table using symbol correlation colors
   table_ctrl.Draw(false);
   UpdateTableValuesAndColors(table_ctrl.GetTableView(0),ExtCorrelationMatrix);
   
//--- Let's retrieve the table model with index 0 and print it to the log
   CTable *table_model=table_ctrl.GetTableModel(0);
   table_model.Print(7);
   
//--- Everything completed successfully
   return true;
  }

This function creates a single table within the table control based on the symbol data and the correlations between them. The table headers list the symbol names horizontally and vertically. The symbol correlation values corresponding to the row/column positions are written into the table cells. Each cell is colored according to its correlation, ranging from red (correlation -1) through yellow (correlation 0) to green (correlation +1), thereby creating a heat map of correlations.

A function that updates the values and colors of table cells based on a correlation matrix:

//+--------------------------------------------------------------------------------+
//| Update the values and colors of table cells based on the correlation matrix    |
//+--------------------------------------------------------------------------------+
void UpdateTableValuesAndColors(CTableView *table_view, matrix &corr_matrix)
  {
//--- Let's check the validity of the table pointer
   if(table_view==NULL)
      return;

//--- Number of table rows and columns
   int total_row=table_view.RowsTotal();
   int total_col=table_view.CellsInRow(0);

//--- In a loop through the table rows
   for(int r=0; r<total_row; r++)
     {
      //--- retrieve the next row visual representation object
      CTableRowView *row_obj=table_view.GetRowView(r);
      if(row_obj==NULL)
         continue;
      //--- Get the color control
      CColorElement *ce=row_obj.GetBackColorControl();
      if(ce==NULL)
         continue;
      
      //--- In a loop over the number of cells in the row
      for(int c=0; c<total_col; c++)
        {
         //--- get the next cell visual representation object
         CTableCellView *cell=table_view.GetCellView(r,c);
         if(cell==NULL)
            continue;
         
         //--- Get the correlation value from the matrix
         double val=corr_matrix[r][c];
         //--- Update the cell text,
         cell.SetText(DoubleToString(val,2));
         //--- update the cell color
         color new_color=ce.InterpolateColorByCoeff(clrRed,clrYellow,clrGreen,val);
         cell.SetBackColor(new_color);
         
         //--- Redraw the cell (update the chart when processing the last table cell)
         bool flag=(r==total_row-1 && c==total_col-1 ? true : false); 
         cell.Draw(flag);
        }
     }
  }

The entire logic of the function is explained in detail in the comments. The function is called on every tick immediately after all data has been successfully received and the table has been created. When the function is called, it recalculates the symbol correlation values for each cell in the table and updates the values and colors of the table cells.

Function that returns a flag indicating whether a chart with the specified identifier exists:

//+--------------------------------------------------------------------------------+
//| Return a flag indicating whether a chart with the specified identifier exists  |
//+--------------------------------------------------------------------------------+
bool IsExistChart(const long id)
  {
//--- Variables for chart identifiers 
   long curr_chart=0, prev_chart=0; 
   int i=0; 

//--- Iterate over all charts
   while(!IsStopped() && i<CHARTS_MAX)
     { 
      //--- Get the next chart based on the previous one
      curr_chart=ChartNext(prev_chart);   // prev_chart==0 - get the first chart
      //--- If we have reached the end of the chart list, exit the loop
      if(curr_chart<0)
         break;
      //--- If the chart identifier matches the target one, such a chart exists
      if(curr_chart==id)
         return true;
      //--- Store the current chart ID for the next ChartNext() call
      prev_chart=curr_chart;
      //--- Increment the counter
      i++;
     }
//--- The target chart does not exist
   return false;
  }

To avoid opening a new chart window every time we click a table cell, we need to check whether a previously opened chart already exists. That is exactly what the function does.

Function that opens charts for symbols:

//+------------------------------------------------------------------+
//| Open charts for symbols                                          |
//+------------------------------------------------------------------+
long OpenCharts(const string row_symb,const string col_symb)
  {
   //--- Set the symbol and timeframe for the new chart 
   string symbol=row_symb; 
   if(symbol==NULL || symbol=="") 
      symbol=Symbol(); 
    
//--- Open a new chart with the specified symbol and timeframe 
   long id=ChartOpen(symbol,PERIOD_CURRENT);
   if(id==0) 
     { 
      Print("ChartOpen() failed. Error ", GetLastError()); 
      return 0; 
     }
//--- Detach the chart and make it empty
   ChartSetInteger(id,CHART_IS_DOCKED,false);
   ChartSetInteger(id,CHART_SHOW,false);

//--- Get the coordinates of the edges of the detached chart
   int top=(int)ChartGetInteger(id,CHART_FLOAT_TOP);
   int bottom=(int)ChartGetInteger(id,CHART_FLOAT_BOTTOM);
   int left=(int)ChartGetInteger(id,CHART_FLOAT_LEFT);
   int right=(int)ChartGetInteger(id,CHART_FLOAT_RIGHT);
   
//--- Set the new width and height of the chart
   ChartSetInteger(id,CHART_FLOAT_RIGHT,left+CHART_FLOAT_WIDTH);
   ChartSetInteger(id,CHART_FLOAT_BOTTOM,top+CHART_FLOAT_HEIGHT);
   
//--- Get the dimensions of the chart in pixels
   int cw=(int)ChartGetInteger(id,CHART_WIDTH_IN_PIXELS);
   int ch=(int)ChartGetInteger(id,CHART_HEIGHT_IN_PIXELS);
   
//--- Set the height for the top and bottom chart objects
   int h0=(int)round(ch/2);
   int h1=ch-h0;
   
//--- Create two chart objects with the row and column symbols on the detached chart
   if(!CreateChartObject(id,"ChartRowSymbol",row_symb,PERIOD_CURRENT,0,0,cw,h0))
      return 0;
   if(!CreateChartObject(id,"ChartColSymbol",col_symb,PERIOD_CURRENT,0,h0-1,cw,h1+1))
      return 0;
      
//--- Update the opened chart and return its ID
   ChartRedraw(id);
   return id;
  }

This function opens a single chart, makes it floating, and prevents the price chart from being drawn on it. Next, two graphical objects of the Chart type are created, and their width, height, and symbol names are set. This gives us a single floating window containing two charts. When you click on a table cell, a window like this will open, displaying price charts for the symbols corresponding to the selected table cell. Tracking changes in the size of the floating window has not been implemented in order to simplify the example. In other words, when the window size changes, the symbol charts will not change their size.

Function that creates a chart object for the specified symbol:

//+------------------------------------------------------------------+
//| Create a chart object for the specified symbol                   |
//+------------------------------------------------------------------+
bool CreateChartObject(const long chart_id,const string name,const string symbol,const ENUM_TIMEFRAMES timeframe,const int x,const int y,const int w,const int h)
  {
//--- Create a chart object with the specified coordinates and dimensions
//--- and set its properties: symbol, timeframe, coordinates, and dimensions
   if(ObjectCreate(chart_id,name,OBJ_CHART,0,x,y,w,h))
     {
      ObjectSetString(chart_id,name,OBJPROP_SYMBOL,symbol);
      ObjectSetInteger(chart_id,name,OBJPROP_PERIOD,timeframe);
      ObjectSetInteger(chart_id,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(chart_id,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(chart_id,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(chart_id,name,OBJPROP_YSIZE,h);
      return true;
     }
//--- Error creating a chart object
   return false;
  } 

The function takes the ID of the chart on which the Chart graphical object is to be created, the name of the object being created, the symbol and timeframe of this object’s chart, and its coordinates and dimensions. After the object is created, all the specified properties are set for it.

That's it — the indicator is ready.

Let's compile the indicator and run it on the chart:

All the intended functionality of the indicator and the tables works correctly, as shown in the attached image above.


Conclusion

Today, we refined the table classes to make them easier to read by adding a vertical table row header, and reviewed the process of creating an indicator that displays a symbol correlation table in the form of correlation values and a heat map. This approach makes it possible not only to analyze numerical correlation values, but also to visually assess their distribution using a color scale. Using color interpolation to color-code table cells makes data analysis clearer and more intuitive, which is especially important when working with large amounts of data.

A correlation heat map can be useful in various tasks related to financial market analysis, such as identifying relationships between instruments, finding highly correlated assets, or, conversely, finding weakly correlated instruments for portfolio diversification.

The implemented table, featuring vertical and horizontal headers and dynamically colored cells, demonstrates how the MVC paradigm can be used effectively to build complex interface elements.
We can easily extend the table’s functionality by adding new features without making significant changes to the existing code.

The indicator created today is not only a useful tool for analyzing correlations, but also an example of how MQL5's programming capabilities can be used to build complex and functional interface elements.


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 for controls
3 Controls.mqh Class library Control classes
4 iCorrelationTable.mq5 Test indicator An indicator for displaying symbol correlation in a table
5 MQL5.zip Archive An archive of the files listed above, to be extracted into the MQL5 directory of the client terminal

All created files are attached to the article for self-study. You can extract the archive file into the terminal folder, and all the files will be placed in the correct folder: \MQL5\Indicators\Tables\.

The complete source code for the project, including all the files described in the article, is available in the repository.


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

Attached files |
Tables.mqh (135.72 KB)
Base.mqh (151.08 KB)
Controls.mqh (460.02 KB)
MQL5.zip (105.22 KB)
Neural Networks in Trading: Disentangling Structured Components (Encoder) Neural Networks in Trading: Disentangling Structured Components (Encoder)
We invite you to explore the next stage in implementing the SCNN framework, which combines flexibility and interpretability, allowing structural components of a time series to be identified precisely. The article provides a detailed explanation of the mechanisms of adaptive normalization and attention, which ensure the model's resilience to changing market conditions.
Machine Learning Under Constraint (Part 2): Calibrating Position Size to the Remaining Drawdown Budget Machine Learning Under Constraint (Part 2): Calibrating Position Size to the Remaining Drawdown Budget
We present a rule-set-aware calibration chain that turns the remaining risk budget into a calibrated sigmoid scale for position sizing. It computes a ceiling from stop loss pct and safety factor, back-solves w at a reference divergence, and flattens size progressively as the budget shrinks. The paper also clarifies where leverage caps must be applied in production: at the lots conversion, since risk-based sizing alone does not enforce max leverage.
Ecological Cycle Optimizer (ECO) Ecological Cycle Optimizer (ECO)
The ECO (Ecological Cycle Optimizer) algorithm offers an interesting metaphor for applying the concept of the ecological cycle to the field of metaheuristic optimization. The idea of dividing a population into trophic levels — producers, herbivores, carnivores, omnivores, and decomposers — creates a hierarchical search structure, in which each group contributes to the overall optimization process.
Self-Optimizing Expert Advisors in MQL5 (Part 19): Parameter Optimization For Time-Lagged Independent Components Analysis (2) Self-Optimizing Expert Advisors in MQL5 (Part 19): Parameter Optimization For Time-Lagged Independent Components Analysis (2)
The article shows how to tune ICA hyperparameters with a supervised evaluation pipeline and apply spectral clustering to time-lagged indicators. Cross-validation identifies the optimal number of clusters, which are translated into expected return and risk measures. These signals drive dynamic position sizing and stop-loss control, with surrogate models converted to ONNX and integrated into an MQL5 Expert Advisor.