Tables in the MVC Paradigm in MQL5: Symbol Correlation Table
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:
- CColumnCaptionView — column header,
- 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:
- 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.
- CTableHeaderView — a horizontal table header that contains a list of CColumnCaptionView column header objects,
- 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
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Neural Networks in Trading: Disentangling Structured Components (Encoder)
Machine Learning Under Constraint (Part 2): Calibrating Position Size to the Remaining Drawdown Budget
Ecological Cycle Optimizer (ECO)
Self-Optimizing Expert Advisors in MQL5 (Part 19): Parameter Optimization For Time-Lagged Independent Components Analysis (2)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use