Tables in the MVC Paradigm in MQL5: Customizable and sortable table columns
Contents
- Introduction
- Refining Library Classes
- Class For Simplified Table Creation
- Testing the Result
- Conclusion
Introduction
In the previous article focused on creating tables on MQL5 in the MVC paradigm, we linked tabular data (Model) with their graphical representation (View) in a single control (TableView), and created a simple static table based on the resulting object. Tables are a convenient tool for classifying and displaying various data in a user—friendly view. Accordingly, the table should provide more features for the user to control the display of data.
Today, we will add to the tables a feature to adjust the column widths, indicate the types of data displayed, and sort the table data by its columns. To do this, we just need to refine the previously created classes of controls. But in the end, we will add a new class that simplifies table creation. The class will allow for creating tables in several rows from previously prepared data.
In the MVC (Model — View — Controller) concept, the interaction between the three components is organized in such a way that when the external component (View) is changed using the Controller, the Model is modified. And then the modified model is re-displayed by the visual component (View). Here, we will organize interaction between the three components in the same way — clicking on table’s column header (the Controller component operation) will result in a change in data location in the table model (reorganizing the Model component), which will result in a change in the table view — the display of the result by the View component.
Refining Library Classes
All files of the library being developed are located at \MQL5\Indicators\Tables\. The Table Model class file (Tables.mqh), along with the test indicator file (iTestTable.mq5), is located at \MQL5\Indicators\Tables\.
Graphic library files (Base.mqh and Controls.mqh) are located at subfolder \MQL5\Indicators\Tables\Controls\. All the necessary files can be downloaded in one archive from the previous article.
Refine table model classes in the file \MQL5\Indicators\Tables\Tables.mqh.
Rows in the table model are sorted by row ID (index) by default. The very first row has an index of 0. And the cells in the row also start with a zero index. To sort by cell indexes, we need to designate a certain number that we will add to the cell index, and by which we will determine that sorting by cell index is set. And we also should indicate the sorting direction. So we need two numbers: the first one will determine that the cell index needs to be sorted in ascending order, the second one will determine whether the cell index needs to be sorted in descending order.
Define macro substitutions for these numbers:
//+------------------------------------------------------------------+ //| Macros | //+------------------------------------------------------------------+ #define __TABLES__ // File ID #define MARKER_START_DATA -1 // Data start marker in a file #define MAX_STRING_LENGTH 128 // Maximum length of a string in a cell #define CELL_WIDTH_IN_CHARS 19 // Table cell width in characters #define ASC_IDX_CORRECTION 10000 // Column index offset for ascending sorting #define DESC_IDX_CORRECTION 20000 // Column index offset for descending sorting
Having defined such macro substitutions, we indicated that there can be no more than 10,000 rows in the table and cells in one row. It seems that this is more than enough to work with tables (100 million table cells).
To the sorting method we will pass a number from zero to 10,000 as the mode parameter. This will be sorting by indexes of table rows. Numbers from 10,000 inclusive to 19,999 will indicate the sorting by the index of the table column in ascending order. Numbers from 20,000 — by column index in descending order:
/*
Sort(0) - by row index
Sort(ASC_IDX_CORRECTION) - ascending by column 0
Sort(1+ASC_IDX_CORRECTION) - ascending by column 1
Sort(2+ASC_IDX_CORRECTION) - ascending by column 2
etc.
Sort(DESC_IDX_CORRECTION) - descending by column 0
Sort(1+DESC_IDX_CORRECTION) - descending by column 1
Sort(2+DESC_IDX_CORRECTION) - descending by column 2
etc.
*/
For the Sort() list sorting method to work correctly, it is necessary to redefine the virtual method for comparing two objects Compare() defined in the base object of the Standard Library. By default, this method returns 0, which means that the objects being compared are equal.
We already have an implemented comparison method in the table row class CTableRow. Refine it so that it is possible to sort rows by column indexes in the table, given the sorting direction:
//+------------------------------------------------------------------+ //| Compare two objects | //+------------------------------------------------------------------+ int CTableRow::Compare(const CObject *node,const int mode=0) const { if(node==NULL) return -1; //--- Sort by row index if(mode==0) { const CTableRow *obj=node; return(this.Index()>obj.Index() ? 1 : this.Index()<obj.Index() ? -1 : 0); } //--- Sort by cell index in ascending/descending order //--- Sorting direction flag and cell index for sorting bool asc=(mode>=ASC_IDX_CORRECTION && mode<DESC_IDX_CORRECTION); int col= mode%(asc ? ASC_IDX_CORRECTION : DESC_IDX_CORRECTION); //--- Remove the 'node' constancy CTableRow *nonconst_this=(CTableRow*)&this; CTableRow *nonconst_node=(CTableRow*)node; //--- Get the current and compared cells by 'mode' index CTableCell *cell_current =nonconst_this.GetCell(col); CTableCell *cell_compared=nonconst_node.GetCell(col); if(cell_current==NULL || cell_compared==NULL) return -1; //--- Compare depending on the cell type int cmp=0; switch(cell_current.Datatype()) { case TYPE_DOUBLE : cmp=(cell_current.ValueD()>cell_compared.ValueD() ? 1 : cell_current.ValueD()<cell_compared.ValueD() ? -1 : 0); break; case TYPE_LONG : case TYPE_DATETIME: case TYPE_COLOR : cmp=(cell_current.ValueL()>cell_compared.ValueL() ? 1 : cell_current.ValueL()<cell_compared.ValueL() ? -1 : 0); break; case TYPE_STRING : cmp=::StringCompare(cell_current.ValueS(),cell_compared.ValueS()); break; default : break; } //--- Return the result of comparing cells taking into account the sorting direction return(asc ? cmp : -cmp); }
The highlighted in color code block compares two table cells in ascending (mode >= 10000 && <20000) and descending (mode>=20000). Since, for comparison we should get necessary cell objects from the string object, and they are not constant (while the string for comparison is passed to the method as a constant pointer), we first need to remove constancy for *node by declaring non-constant objects for comparison. And from them get cell objects for comparison.
These are dangerous transformations, as the constancy of pointers is violated, and objects can be accidentally altered. But here we definitely know that this method only compares values, but not their change. Therefore, here we can take a little controlled liberty to get the result we want.
We will add three new methods to the CTableModel table model class to simplify working with table columns and sorting by them:
public: //--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position CTableRow *RowAddNew(void); CTableRow *RowInsertNewTo(const uint index_to); //--- (1) Remove or (2) relocate the row, (3) clear the row data bool RowDelete(const uint index); bool RowMoveTo(const uint row_index, const uint index_to); void RowClearData(const uint index); //--- (1) Return and (2) display the row description in the journal string RowDescription(const uint index); void RowPrint(const uint index,const bool detail); //--- (1) Add, (2) remove, (3) relocate a column, (4) clear data, set the column data (5) type, //--- (6) data accuracy, (7) time, (8) column color names display flags bool ColumnAddNew(const int index=-1); bool ColumnDelete(const uint index); bool ColumnMoveTo(const uint col_index, const uint index_to); void ColumnClearData(const uint index); void ColumnSetDatatype(const uint index,const ENUM_DATATYPE type); void ColumnSetDigits(const uint index,const int digits); void ColumnSetTimeFlags(const uint index, const uint flags); void ColumnSetColorNamesFlag(const uint index, const bool flag); //--- Sort the table by the specified column and direction void SortByColumn(const uint column, const bool descending); //--- (1) Return and (2) display the table description in the journal virtual string Description(void); void Print(const bool detail); void PrintTable(const int cell_width=CELL_WIDTH_IN_CHARS);
Outside of the class body, write their implementation.
A Method That Sets Column Time Display Flags:
//+------------------------------------------------------------------+ //| Set the column time display flags | //+------------------------------------------------------------------+ void CTableModel::ColumnSetTimeFlags(const uint index,const uint flags) { //--- In a loop through all table rows for(uint i=0;i<this.RowsTotal();i++) { //--- get the cell with the column index from each row and set the time display flags CTableCell *cell=this.GetCell(i, index); if(cell!=NULL) cell.SetDatetimeFlags(flags); } }
A Method That Sets Display Flag of Column Color Name:
//+------------------------------------------------------------------+ //| Sets the flag for displaying column color names | //+------------------------------------------------------------------+ void CTableModel::ColumnSetColorNamesFlag(const uint index,const bool flag) { //--- In a loop through all table rows for(uint i=0;i<this.RowsTotal();i++) { //--- get the cell with the column index from each row and set the flag for displaying color names CTableCell *cell=this.GetCell(i, index); if(cell!=NULL) cell.SetColorNameFlag(flag); } }
Both methods, in a simple loop through table rows get the desired cell from each subsequent row and set the specified flag for it.
A Method That Sorts the Table By the Specified Column and Direction:
//+------------------------------------------------------------------+ //| Sort the table by the specified column and direction | //+------------------------------------------------------------------+ void CTableModel::SortByColumn(const uint column,const bool descending) { if(this.m_list_rows.Total()==0) return; int mode=(int)column+(descending ? DESC_IDX_CORRECTION : ASC_IDX_CORRECTION); this.m_list_rows.Sort(mode); this.CellsPositionUpdate(); }
The index of the table column is passed to the method, by the values of which the table should be sorted, as well as the sorting direction flag. Leave the method if the list of lines is empty. Next, define the sorting mode (mode). If sorting is descending (descending == true), then add 20,000 to the column index, if sorting is ascending, then add 10000 to the column index. Next, call the sorting method with the mode indication and update all the cells in the table in each row.
Now add new methods to the CTable table class. These are methods with the same name for the ones just added to the table model class:
public: //--- (1) Return and (2) display the cell description and (3) the object assigned to the cell string CellDescription(const uint row, const uint col); void CellPrint(const uint row, const uint col); //--- Return (1) the object assigned to the cell and (2) the type of the object assigned to the cell CObject *CellGetObject(const uint row, const uint col); ENUM_OBJECT_TYPE CellGetObjType(const uint row, const uint col); //--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position CTableRow *RowAddNew(void); CTableRow *RowInsertNewTo(const uint index_to); //--- (1) Remove or (2) relocate the row, (3) clear the row data bool RowDelete(const uint index); bool RowMoveTo(const uint row_index, const uint index_to); void RowClearData(const uint index); //--- (1) Return and (2) display the row description in the journal string RowDescription(const uint index); void RowPrint(const uint index,const bool detail); //--- (1) Add new, (2) remove, (3) relocate the column and (4) clear the column data bool ColumnAddNew(const string caption,const int index=-1); bool ColumnDelete(const uint index); bool ColumnMoveTo(const uint index, const uint index_to); void ColumnClearData(const uint index); //--- Set (1) the value of the specified header and (2) data accuracy, //--- (3) time and (4) color names for the specified column display flags void ColumnCaptionSetValue(const uint index,const string value); void ColumnSetDigits(const uint index,const int digits); void ColumnSetTimeFlags(const uint index,const uint flags); void ColumnSetColorNamesFlag(const uint col, const bool flag); //--- (1) Set and (2) return the data type for the specified column void ColumnSetDatatype(const uint index,const ENUM_DATATYPE type); ENUM_DATATYPE ColumnDatatype(const uint index); //--- (1) Return and (2) display the object description in the journal virtual string Description(void); void Print(const int column_width=CELL_WIDTH_IN_CHARS); //--- Sort the table by the specified column and direction void SortByColumn(const uint column, const bool descending) { if(this.m_table_model!=NULL) this.m_table_model.SortByColumn(column,descending); } //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(OBJECT_TYPE_TABLE); }
...
//+------------------------------------------------------------------+ //| Set the time display flags for the specified column | //+------------------------------------------------------------------+ void CTable::ColumnSetTimeFlags(const uint index,const uint flags) { if(this.m_table_model!=NULL) this.m_table_model.ColumnSetTimeFlags(index,flags); } //+------------------------------------------------------------------+ //| Set the color name display flags for the specified column | //+------------------------------------------------------------------+ void CTable::ColumnSetColorNamesFlag(const uint index,const bool flag) { if(this.m_table_model!=NULL) this.m_table_model.ColumnSetColorNamesFlag(index,flag); }
The methods check the validity of the table model object and invoke its corresponding methods of the same name, which were discussed above.
Refine classes in file \MQL5\Indicators\Tables\Controls\Base.mqh.
Add a forward declaration of the classes that were made last time, but were not added to the list, and a new class that we will implement today:
//+------------------------------------------------------------------+ //| Include libraries | //+------------------------------------------------------------------+ #include <Canvas\Canvas.mqh> // CCanvas class #include <Arrays\List.mqh> // CList class #include "..\Tables.mqh" //--- Forward declaration of control element classes class CBoundedObj; // Base class storing object dimensions class CCanvasBase; // Base class of graphical elements canvas class CCounter; // Delay counter class class CAutoRepeat; // Event auto-repeat class class CImagePainter; // Image drawing class class CVisualHint; // Hint class class CLabel; // Text label class class CButton; // Simple button class class CButtonTriggered; // Two-position button class class CButtonArrowUp; // Up arrow button class class CButtonArrowDown; // Down arrow button class class CButtonArrowLeft; // Left arrow button class class CButtonArrowRight; // Right arrow button class class CCheckBox; // CheckBox control class class CRadioButton; // RadioButton control class class CScrollBarThumbH; // Horizontal scrollbar slider class class CScrollBarThumbV; // Vertical scrollbar slider class class CScrollBarH; // Horizontal scrollbar class class CScrollBarV; // Vertical scrollbar class class CTableCellView; // Class for visual representation of a table cell class CTableRowView; // Class for visual representation of a table row class CColumnCaptionView; // Class for visual representation of table column header class CTableHeaderView; // Class for visual representation of a table header class CTableView; // Class for visual representation of a table class CTableControl; // Table management class class CPanel; // Panel control class class CGroupBox; // GroupBox control class class CContainer; // Container control class
Add a new type to the enumeration of UI element types:
//+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_TYPE // Enumeration of graphical element types { ELEMENT_TYPE_BASE = 0x10000, // Basic object of graphical elements ELEMENT_TYPE_COLOR, // Color object ELEMENT_TYPE_COLORS_ELEMENT, // Color object of the graphical object element ELEMENT_TYPE_RECTANGLE_AREA, // Rectangular area of the element ELEMENT_TYPE_IMAGE_PAINTER, // Object for drawing images ELEMENT_TYPE_COUNTER, // Counter object ELEMENT_TYPE_AUTOREPEAT_CONTROL, // Event auto-repeat object ELEMENT_TYPE_BOUNDED_BASE, // Basic object of graphical element sizes ELEMENT_TYPE_CANVAS_BASE, // Basic canvas object for graphical elements ELEMENT_TYPE_ELEMENT_BASE, // Basic object of graphical elements ELEMENT_TYPE_HINT, // Hint ELEMENT_TYPE_LABEL, // Text label ELEMENT_TYPE_BUTTON, // Simple button ELEMENT_TYPE_BUTTON_TRIGGERED, // Two-position button ELEMENT_TYPE_BUTTON_ARROW_UP, // Up arrow button ELEMENT_TYPE_BUTTON_ARROW_DOWN, // Down arrow button ELEMENT_TYPE_BUTTON_ARROW_LEFT, // Left arrow button ELEMENT_TYPE_BUTTON_ARROW_RIGHT, // Right arrow button ELEMENT_TYPE_CHECKBOX, // CheckBox control ELEMENT_TYPE_RADIOBUTTON, // RadioButton control ELEMENT_TYPE_SCROLLBAR_THUMB_H, // Horizontal scroll bar slider ELEMENT_TYPE_SCROLLBAR_THUMB_V, // Vertical scroll bar slider ELEMENT_TYPE_SCROLLBAR_H, // ScrollBarHorisontal control ELEMENT_TYPE_SCROLLBAR_V, // ScrollBarVertical control ELEMENT_TYPE_TABLE_CELL_VIEW, // Table cell (View) ELEMENT_TYPE_TABLE_ROW_VIEW, // Table row (View) ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW,// Table column header (View) ELEMENT_TYPE_TABLE_HEADER_VIEW, // Table header (View) ELEMENT_TYPE_TABLE_VIEW, // Table (View) ELEMENT_TYPE_TABLE_CONTROL_VIEW, // Table control (View) ELEMENT_TYPE_PANEL, // Panel control ELEMENT_TYPE_GROUPBOX, // GroupBox control ELEMENT_TYPE_CONTAINER, // Container control };
Add a new object type to the function that returns the element’s short name by type:
//+------------------------------------------------------------------+ //| Return the short name of the element by type | //+------------------------------------------------------------------+ string ElementShortName(const ENUM_ELEMENT_TYPE type) { switch(type) { case ELEMENT_TYPE_ELEMENT_BASE : return "BASE"; // Basic object of graphical elements case ELEMENT_TYPE_HINT : return "HNT"; // Hint case ELEMENT_TYPE_LABEL : return "LBL"; // Text label case ELEMENT_TYPE_BUTTON : return "SBTN"; // Simple button case ELEMENT_TYPE_BUTTON_TRIGGERED : return "TBTN"; // Toggle button case ELEMENT_TYPE_BUTTON_ARROW_UP : return "BTARU"; // Up arrow button case ELEMENT_TYPE_BUTTON_ARROW_DOWN : return "BTARD"; // Down arrow button case ELEMENT_TYPE_BUTTON_ARROW_LEFT : return "BTARL"; // Left arrow button case ELEMENT_TYPE_BUTTON_ARROW_RIGHT : return "BTARR"; // Right arrow button case ELEMENT_TYPE_CHECKBOX : return "CHKB"; // CheckBox control case ELEMENT_TYPE_RADIOBUTTON : return "RBTN"; // RadioButton control case ELEMENT_TYPE_SCROLLBAR_THUMB_H : return "THMBH"; // Horizontal scroll bar slider case ELEMENT_TYPE_SCROLLBAR_THUMB_V : return "THMBV"; // Vertical scroll bar slider case ELEMENT_TYPE_SCROLLBAR_H : return "SCBH"; // ScrollBarHorisontal control case ELEMENT_TYPE_SCROLLBAR_V : return "SCBV"; // ScrollBarVertical control case ELEMENT_TYPE_TABLE_CELL_VIEW : return "TCELL"; // Table cell (View) case ELEMENT_TYPE_TABLE_ROW_VIEW : return "TROW"; // Table row (View) case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW : return "TCAPT"; // Table column header (View) case ELEMENT_TYPE_TABLE_HEADER_VIEW : return "THDR"; // Table header (View) case ELEMENT_TYPE_TABLE_VIEW : return "TABLE"; // Table (View) case ELEMENT_TYPE_TABLE_CONTROL_VIEW : return "TBLCTRL"; // Table control (View) case ELEMENT_TYPE_PANEL : return "PNL"; // Panel Table control case ELEMENT_TYPE_GROUPBOX : return "GRBX"; // GroupBox control case ELEMENT_TYPE_CONTAINER : return "CNTR"; // Container control default : return "Unknown"; // Unknown } }
Add methods that return cursor coordinates to the base class of graphical elements:
//+------------------------------------------------------------------+ //| Base class of graphical elements | //+------------------------------------------------------------------+ class CBaseObj : public CObject { protected: int m_id; // ID ushort m_name[]; // Name public: //--- Set (1) name and (2) ID void SetName(const string name) { ::StringToShortArray(name,this.m_name); } virtual void SetID(const int id) { this.m_id=id; } //--- Return (1) name and (2) ID string Name(void) const { return ::ShortArrayToString(this.m_name); } int ID(void) const { return this.m_id; } //--- Return the cursor coordinates int CursorX(void) const { return CCommonManager::GetInstance().CursorX(); } int CursorY(void) const { return CCommonManager::GetInstance().CursorY(); } //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_BASE); } //--- (1) Return and (2) display the object description in the journal virtual string Description(void); virtual void Print(void); //--- Constructor/destructor CBaseObj (void) : m_id(-1) { this.SetName(""); } ~CBaseObj (void) {} };
This will allow each graphical element to have access to cursor coordinates at any time. CCommonManager singleton class constantly monitors the cursor coordinates, and accessing it from any graphical element gives the element access to these coordinates.
In the rectangular area class, add a method that removes assignment of an object to the area:
//+------------------------------------------------------------------+ //| Rectangular region class | //+------------------------------------------------------------------+ class CBound : public CBaseObj { protected: CBaseObj *m_assigned_obj; // Object assigned to the region CRect m_bound; // Rectangular area structure public: //--- Change the bounding rectangular (1) width, (2) height and (3) size void ResizeW(const int size) { this.m_bound.Width(size); } void ResizeH(const int size) { this.m_bound.Height(size); } void Resize(const int w,const int h) { this.m_bound.Width(w); this.m_bound.Height(h); } //--- Set (1) X, (2) Y and (3) both coordinates of the bounding rectangle void SetX(const int x) { this.m_bound.left=x; } void SetY(const int y) { this.m_bound.top=y; } void SetXY(const int x,const int y) { this.m_bound.LeftTop(x,y); } //--- (1) Set and (2) shift the bounding rectangle by the specified coordinates/offset size void Move(const int x,const int y) { this.m_bound.Move(x,y); } void Shift(const int dx,const int dy) { this.m_bound.Shift(dx,dy); } //--- Returns the object coordinates, dimensions, and boundaries int X(void) const { return this.m_bound.left; } int Y(void) const { return this.m_bound.top; } int Width(void) const { return this.m_bound.Width(); } int Height(void) const { return this.m_bound.Height(); } int Right(void) const { return this.m_bound.right-(this.m_bound.Width() >0 ? 1 : 0);} int Bottom(void) const { return this.m_bound.bottom-(this.m_bound.Height()>0 ? 1 : 0);} //--- Returns the flag indicating whether the cursor is inside the area bool Contains(const int x,const int y) const { return this.m_bound.Contains(x,y); } //--- (1) Assign, (2) unassign and (3) return the pointer to the assigned element void AssignObject(CBaseObj *obj) { this.m_assigned_obj=obj; } void UnassignObject(void) { this.m_assigned_obj=NULL; } CBaseObj *GetAssignedObj(void) { return this.m_assigned_obj; } //--- Return the object description virtual string Description(void); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_RECTANGLE_AREA); } //--- Constructors/destructor CBound(void) { ::ZeroMemory(this.m_bound); } CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h); } ~CBound(void) { ::ZeroMemory(this.m_bound); } };
If a UI element is programmatically deleted, then being assigned to an area in another UI element, its pointer will remain registered for that area. Accessing an element using this pointer will lead to a critical program termination. Therefore, when deleting an object, it is necessary to unattach it from the area if it was attached to it. Writing a NULL value to a pointer enables to control pointer validity to an already deleted UI element.
Classes of UI elements in the library under development are arranged so that if an object is attached to a container, when the element leaves its container, it gets cropped along boundaries of its container. If an element is completely outside the container, it is simply hidden. But if there is a command for the container to move it to the foreground, the container automatically brings all the elements attached to it to the foreground in a loop. Accordingly, hidden elements are visible, since shifting an object to the foreground means sequential execution of two hide-display commands. In order not to move hidden elements to the foreground, add a flag to UI element properties that it is hidden because it is located outside its container. And in the method of shifting an object to the foreground, check this flag.
In the base class of CCanvasBase UI element canvas, declare the following flag in the protected section:
protected: CCanvas *m_background; // Background canvas CCanvas *m_foreground; // Foreground canvas CCanvasBase *m_container; // Parent container object CColorElement m_color_background; // Background color control object CColorElement m_color_foreground; // Foreground color control object CColorElement m_color_border; // Border color control object CColorElement m_color_background_act; // Activated element background color control object CColorElement m_color_foreground_act; // Activated element foreground color control object CColorElement m_color_border_act; // Activated element frame color control object CAutoRepeat m_autorepeat; // Event auto-repeat control object ENUM_ELEMENT_STATE m_state; // Control state (e.g. buttons (on/off)) long m_chart_id; // Chart ID int m_wnd; // Chart subwindow index int m_wnd_y; // Cursor Y coordinate offset in the subwindow int m_obj_x; // Graphical object X coordinate int m_obj_y; // Graphical object Y coordinate uchar m_alpha_bg; // Background transparency uchar m_alpha_fg; // Foreground transparency uint m_border_width_lt; // Left frame width uint m_border_width_rt; // Right frame width uint m_border_width_up; // Top frame width uint m_border_width_dn; // Bottom frame width string m_program_name; // Program name bool m_hidden; // Hidden object flag bool m_blocked; // Blocked element flag bool m_movable; // Moved element flag bool m_resizable; // Resizing flag bool m_focused; // Element flag in focus bool m_main; // Main object flag bool m_autorepeat_flag; // Event sending auto-repeat flag bool m_scroll_flag; // Flag for scrolling content using scrollbars bool m_trim_flag; // Flag for clipping the element to the container borders bool m_cropped; // Flag indicating that the object is hidden outside the container borders int m_cursor_delta_x; // Distance from the cursor to the left edge of the element int m_cursor_delta_y; // Distance from the cursor to the top edge of the element int m_z_order; // Graphical object Z-order
In the public section, implement a method that returns this flag:
public: //--- (1) Set and (2) return the state void SetState(ENUM_ELEMENT_STATE state) { this.m_state=state; this.ColorsToDefault(); } ENUM_ELEMENT_STATE State(void) const { return this.m_state; } //--- (1) Set and (2) return z-order bool ObjectSetZOrder(const int value); int ObjectZOrder(void) const { return this.m_z_order; } //--- Return (1) the object's belonging to the program, the flag (2) of a hidden element, (3) a blocked element, //--- (4) moved, (5) resized, (6) main element, (7) in focus, (8, 9) graphical object name (background, text) bool IsBelongsToThis(const string name) const { return(::ObjectGetString(this.m_chart_id,name,OBJPROP_TEXT)==this.m_program_name);} bool IsHidden(void) const { return this.m_hidden; } bool IsBlocked(void) const { return this.m_blocked; } bool IsMovable(void) const { return this.m_movable; } bool IsResizable(void) const { return this.m_resizable; } bool IsMain(void) const { return this.m_main; } bool IsFocused(void) const { return this.m_focused; } bool IsAutorepeat(void) const { return this.m_autorepeat_flag; } bool IsScrollable(void) const { return this.m_scroll_flag; } bool IsTrimmed(void) const { return this.m_trim_flag; } bool IsCropped(void) const { return this.m_cropped; } string NameBG(void) const { return this.m_background.ChartObjectName(); } string NameFG(void) const { return this.m_foreground.ChartObjectName(); }
a method that sets a flag, and a virtual method that returns a flag indicating that the element is located completely outside its container:
//--- Return the object boundaries considering the frame int LimitLeft(void) const { return this.ObjectX()+(int)this.m_border_width_lt; } int LimitRight(void) const { return this.ObjectRight()-(int)this.m_border_width_rt; } int LimitTop(void) const { return this.ObjectY()+(int)this.m_border_width_up; } int LimitBottom(void) const { return this.ObjectBottom()-(int)this.m_border_width_dn; } //--- Set (1) movability, (2) main object flag for the object and (3) resizability, //--- (4) auto-repeat events, (5) scrolling within the container and (6) clipping by the container borders void SetMovable(const bool flag) { this.m_movable=flag; } void SetAsMain(void) { this.m_main=true; } virtual void SetResizable(const bool flag) { this.m_resizable=flag; } void SetAutorepeat(const bool flag) { this.m_autorepeat_flag=flag; } void SetScrollable(const bool flag) { this.m_scroll_flag=flag; } virtual void SetTrimmered(const bool flag) { this.m_trim_flag=flag; } void SetCropped(const bool flag) { this.m_cropped=flag; } //--- Return the flag that the object is located outside its container virtual bool IsOutOfContainer(void); //--- Limit the graphical object by the container dimensions virtual bool ObjectTrim(void);
Implementing a method that returns a flag indicating that the object is located outside its container:
//+------------------------------------------------------------------+ //| CCanvasBase::Return a flag that the object is | //| located outside of its container | //+------------------------------------------------------------------+ bool CCanvasBase::IsOutOfContainer(void) { //--- Return the result of checking that the object is completely outside the container return(this.Right() <= this.ContainerLimitLeft() || this.X() >= this.ContainerLimitRight() || this.Bottom()<= this.ContainerLimitTop() || this.Y() >= this.ContainerLimitBottom()); }
The method checks coordinates of object's boundaries relative to container boundaries and returns a flag indicating that the element is completely outside its container. For some graphic elements, this method may be calculated differently. Therefore, the method is declared virtual.
In a method that trims a UI object along the container contour, manage the new flag:
//+-----------------------------------------------------------------------+ //| CCanvasBase::Crop a graphical object to the outline of its container | //+-----------------------------------------------------------------------+ bool CCanvasBase::ObjectTrim() { //--- Check the element cropping permission flag and //--- if the element should not be clipped by the container borders, return 'false' if(!this.m_trim_flag) return false; //--- Get the container boundaries int container_left = this.ContainerLimitLeft(); int container_right = this.ContainerLimitRight(); int container_top = this.ContainerLimitTop(); int container_bottom = this.ContainerLimitBottom(); //--- Get the current object boundaries int object_left = this.X(); int object_right = this.Right(); int object_top = this.Y(); int object_bottom = this.Bottom(); //--- Check if the object is completely outside the container and hide it if it is if(this.IsOutOfContainer()) { //--- Set the flag that the object is outside the container this.m_cropped=true; //--- Hide the object and restore its dimensions this.Hide(false); if(this.ObjectResize(this.Width(),this.Height())) this.BoundResize(this.Width(),this.Height()); return true; } //--- The object is fully or partially located within the visible area of the container else { //--- Remove the flag indicating that the object is located outside the container this.m_cropped=false; //--- If the element is completely inside the container if(object_right<=container_right && object_left>=container_left && object_bottom<=container_bottom && object_top>=container_top) { //--- If the width or height of the graphical object does not match the width or height of the element, //--- modify the graphical object according to the element dimensions and return 'true' if(this.ObjectWidth()!=this.Width() || this.ObjectHeight()!=this.Height()) { if(this.ObjectResize(this.Width(),this.Height())) return true; } } //--- If the element is partially within the container visible area else { //--- If the element is vertically within the container visible area if(object_bottom<=container_bottom && object_top>=container_top) { //--- If the height of the graphic object does not match the height of the element, //--- modify the graphical object by the element height if(this.ObjectHeight()!=this.Height()) this.ObjectResizeH(this.Height()); } else { //--- If the element is horizontally within the container visible area if(object_right<=container_right && object_left>=container_left) { //--- If the width of the graphic object does not match the width of the element, //--- modify the graphical object by the element width if(this.ObjectWidth()!=this.Width()) this.ObjectResizeW(this.Width()); } } } } //--- Check whether the object extends horizontally and vertically beyond the container boundaries bool modified_horizontal=false; // Horizontal change flag bool modified_vertical =false; // Vertical change flag //--- Horizontal cropping int new_left = object_left; int new_width = this.Width(); //--- If the object extends beyond the container left border if(object_left<=container_left) { int crop_left=container_left-object_left; new_left=container_left; new_width-=crop_left; modified_horizontal=true; } //--- If the object extends beyond the container right border if(object_right>=container_right) { int crop_right=object_right-container_right; new_width-=crop_right; modified_horizontal=true; } //--- If there were changes horizontally if(modified_horizontal) { this.ObjectSetX(new_left); this.ObjectResizeW(new_width); } //--- Vertical cropping int new_top=object_top; int new_height=this.Height(); //--- If the object extends beyond the top edge of the container if(object_top<=container_top) { int crop_top=container_top-object_top; new_top=container_top; new_height-=crop_top; modified_vertical=true; } //--- If the object extends beyond the bottom border of the container if(object_bottom>=container_bottom) { int crop_bottom=object_bottom-container_bottom; new_height-=crop_bottom; modified_vertical=true; } //--- If there were vertical changes if(modified_vertical) { this.ObjectSetY(new_top); this.ObjectResizeH(new_height); } //--- After calculations, the object may be hidden, but is now in the container area - display it this.Show(false); //--- If the object has been changed, redraw it if(modified_horizontal || modified_vertical) { this.Update(false); this.Draw(false); return true; } return false; }
After such an addition, objects that are completely outside the container will not be moved to the foreground, becoming visible if a command is sent to move this object to the foreground, or the entire container with all its contents at once.
In the method that places the object in the foreground, check the flag being set in the above method:
//+------------------------------------------------------------------+ //| CCanvasBase::Bring an object to the foreground | //+------------------------------------------------------------------+ void CCanvasBase::BringToTop(const bool chart_redraw) { if(this.m_cropped) return; this.Hide(false); this.Show(chart_redraw); }
If the flag for the object is set, there is no need to move the element to the foreground — leave the method.
Each UI element has a general mouse wheel scroll handler. This general handler calls a virtual method for handling wheel scrolling. Whereas, the value from the sparam of the general handler is passed to sparam. This is a mistake, since none of the controls can detect that the mouse wheel is scrolling over it. The solution is as follows: in the general handler, we know the name of the active element — the one above which the cursor is located, which means that when calling the scroll wheel handler, the name of the active element should be passed to sparam. And in the handler itself, check the element name and the sparam value. If they are equal, then this is exactly the object over which the mouse wheel scrolls. Implement this in the general event handler:
//--- Mouse wheel scroll event if(id==CHARTEVENT_MOUSE_WHEEL) { //--- If this is an active element, call its scroll wheel event handler if(this.IsCurrentActiveElement()) this.OnWheelEvent(id,lparam,dparam,this.ActiveElementName()); // pass the name of the active element to sparam }
We will check for equality of the object name and value in sparam in the handler. The handler is located in another file along with other improvements.
Open \MQL5\Indicators\Tables\Controls\Controls.mqh — now improvements will be entered to this file.
Add new definitions to the macro substitutions section:
//+------------------------------------------------------------------+ //| Macro substitutions | //+------------------------------------------------------------------+ #define DEF_LABEL_W 50 // Text label default width #define DEF_LABEL_H 16 // Text label default height #define DEF_BUTTON_W 60 // Default button width #define DEF_BUTTON_H 16 // Default button height #define DEF_TABLE_ROW_H 16 // Default table row height #define DEF_TABLE_HEADER_H 20 // Default table header height #define DEF_TABLE_COLUMN_MIN_W 12 // Table column minimal width #define DEF_PANEL_W 80 // Default panel width #define DEF_PANEL_H 80 // Default panel height #define DEF_PANEL_MIN_W 60 // Minimum panel width #define DEF_PANEL_MIN_H 60 // Minimum panel height #define DEF_SCROLLBAR_TH 13 // Default scrollbar width #define DEF_THUMB_MIN_SIZE 8 // Minimum width of the scrollbar slider #define DEF_AUTOREPEAT_DELAY 500 // Delay before launching auto-repeat #define DEF_AUTOREPEAT_INTERVAL 100 // Auto-repeat frequency #define DEF_HINT_NAME_TOOLTIP "HintTooltip" // Tooltip name #define DEF_HINT_NAME_HORZ "HintHORZ" // "Double horizontal arrow" hint name #define DEF_HINT_NAME_VERT "HintVERT" // "Double vertical arrow" hint name #define DEF_HINT_NAME_NWSE "HintNWSE" // "Double arrow top-left" (NorthWest-SouthEast) hint name #define DEF_HINT_NAME_NESW "HintNESW" // "Double arrow bottom-left" (NorthEast-SouthWest) hint name #define DEF_HINT_NAME_SHIFT_HORZ "HintShiftHORZ" // "Horizontal offset arrow" hint name #define DEF_HINT_NAME_SHIFT_VERT "HintShiftVERT" // "Vertical offset arrow" hint name
The width of the table column must not be less than 12 pixels, so that when the size is reduced, the columns are not too narrow. It is more convenient to set names of tooltips using the compiler directive and substitute them as the name, since if we need to change the tooltip name, we only change the directive, and there is no need to search and change all occurrences of this name in different places of the code. We now have two names for two new tooltip. These will be tooltips that appear when you hover the cursor over an object's edge, which you can "pull" to resize the object.
Add a new enumeration of column sorting modes and new enumeration constants:
//+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_SORT_BY // Compared properties { ELEMENT_SORT_BY_ID = BASE_SORT_BY_ID, // Comparison by element ID ELEMENT_SORT_BY_NAME = BASE_SORT_BY_NAME, // Comparison by element name ELEMENT_SORT_BY_X = BASE_SORT_BY_X, // Comparison by element X coordinate ELEMENT_SORT_BY_Y = BASE_SORT_BY_Y, // Comparison by element Y coordinate ELEMENT_SORT_BY_WIDTH= BASE_SORT_BY_WIDTH, // Comparison by element width ELEMENT_SORT_BY_HEIGHT= BASE_SORT_BY_HEIGHT, // Comparison by element height ELEMENT_SORT_BY_ZORDER= BASE_SORT_BY_ZORDER, // Comparison by element Z-order ELEMENT_SORT_BY_TEXT, // Comparison by element text ELEMENT_SORT_BY_COLOR_BG, // Comparison by element background color ELEMENT_SORT_BY_ALPHA_BG, // Comparison by element background transparency ELEMENT_SORT_BY_COLOR_FG, // Comparison by element foreground color ELEMENT_SORT_BY_ALPHA_FG, // Comparison by element foreground transparency color ELEMENT_SORT_BY_STATE, // Comparison by element state ELEMENT_SORT_BY_GROUP, // Comparison by element group }; enum ENUM_TABLE_SORT_MODE // Table column sorting modes { TABLE_SORT_MODE_NONE, // No sorting TABLE_SORT_MODE_ASC, // Sort in ascending order TABLE_SORT_MODE_DESC, // Sort in descending order }; enum ENUM_HINT_TYPE // Hint types { HINT_TYPE_TOOLTIP, // Tooltip HINT_TYPE_ARROW_HORZ, // Double horizontal arrow HINT_TYPE_ARROW_VERT, // Double vertical arrow HINT_TYPE_ARROW_NWSE, // Double arrow top-left --- bottom-right (NorthWest-SouthEast) HINT_TYPE_ARROW_NESW, // Double arrow bottom-left --- top-right (NorthEast-SouthWest) HINT_TYPE_ARROW_SHIFT_HORZ, // Horizontal offset arrow HINT_TYPE_ARROW_SHIFT_VERT, // Vertical offset arrow };
In the CImagePainter image drawing class, declare two new methods that draw horizontal and vertical offset arrows:
//--- Clear the area bool Clear(const int x,const int y,const int w,const int h,const bool update=true); //--- Draw a filled (1) up, (2) down, (3) left and (4) right arrow bool ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Draw (1) horizontal 17х7 and (2) vertical 7х17 double arrow bool ArrowHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Draw a diagonal (1) top-left --- bottom-right and (2) bottom-left --- up-right 17x17 double arrow bool ArrowNWSE(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowNESW(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Draw an 18x18 offset arrow (1) horizontally and (2) vertically bool ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Draw (1) checked and (2) unchecked CheckBox bool CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); //--- Draw (1) checked and (2) unchecked RadioButton bool CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); bool UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
Outside of the class body, implement the declared methods.
A Method That Draws an 18x18 Horizontal Offset Arrow:
//+------------------------------------------------------------------+ //| CImagePainter::Draw an 18x18 arrow with horizontal offset | //+------------------------------------------------------------------+ bool CImagePainter::ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true) { //--- If the image area is not valid, return 'false' if(!this.CheckBound(__FUNCTION__)) return false; //--- Shape coordinates int arrx[25]={0, 3, 4, 4, 7, 7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10, 7, 7, 4, 4, 3, 0, 0}; int arry[25]={8, 5, 5, 7, 7, 0, 0, 7, 7, 5, 5, 8, 9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12, 9, 8}; //--- Draw the white background this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha)); //--- Draw the line of arrows this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha)); //--- Draw a dividing line this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha)); //--- Draw the left triangle this.m_canvas.Line(2,7, 2,10,::ColorToARGB(clr,alpha)); this.m_canvas.Line(3,6, 3,11,::ColorToARGB(clr,alpha)); //--- Draw the right triangle this.m_canvas.Line(14,6, 14,11,::ColorToARGB(clr,alpha)); this.m_canvas.Line(15,7, 15,10,::ColorToARGB(clr,alpha)); if(update) this.m_canvas.Update(false); return true; }
A Method That Draws an 18x18 Vertical Offset Arrow:
//+------------------------------------------------------------------+ //| CImagePainter::Draw an 18x18 arrow with vertical offset | //+------------------------------------------------------------------+ bool CImagePainter::ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true) { //--- If the image area is not valid, return 'false' if(!this.CheckBound(__FUNCTION__)) return false; //--- Shape coordinates int arrx[25]={0, 7, 7, 5, 5, 8, 9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12, 9, 8, 5, 5, 7, 7, 0, 0}; int arry[25]={7, 7, 4, 4, 3, 0, 0, 3, 4, 4, 7, 7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10, 7}; //--- Draw the white background this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha)); //--- Draw a dividing line this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha)); //--- Draw the line of arrows this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha)); //--- Draw the top triangle this.m_canvas.Line(7,2, 10,2,::ColorToARGB(clr,alpha)); this.m_canvas.Line(6,3, 11,3,::ColorToARGB(clr,alpha)); //--- Draw the bottom triangle this.m_canvas.Line(6,14, 11,14,::ColorToARGB(clr,alpha)); this.m_canvas.Line(7,15, 10,15,::ColorToARGB(clr,alpha)); if(update) this.m_canvas.Update(false); return true; }
Both methods draw arrow tooltips for horizontal (
) and vertical (
) offset of the element edge to change its dimensions.
In the base class of the CElementBase graphical element, make two methods AddHintsArrowed and ShowCursorHint virtual:
//--- Add an existing hint object to the list CVisualHint *AddHint(CVisualHint *obj, const int dx, const int dy); //--- (1) Add to the list and (2) remove tooltip objects with arrows from the list virtual bool AddHintsArrowed(void); bool DeleteHintsArrowed(void); //--- Displays the resize cursor virtual bool ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y); //--- Handler for dragging element edges and corners virtual void ResizeActionDragHandler(const int x, const int y);
In the class destructor, clear the list of tooltips:
//--- Constructors/destructor CElementBase(void) { this.m_painter.CanvasAssign(this.GetForeground()); this.m_visible_in_container=true; } CElementBase(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CElementBase(void) { this.m_list_hints.Clear(); }
When adding objects to the lists, first the exact same object is searched for in the list. Whereas, for proper search, the list is sorted by the property being compared. But initially the list can be sorted by another property. In order not to disrupt the initial sorting of the lists, we will memorize it, then set the sorting necessary for the search. And at the end, return the previously memorized one.
In the method of adding the specified tooltip to the list, make the following refinement:
//+------------------------------------------------------------------+ //| CElementBase::Add the specified hint object to the list | //+------------------------------------------------------------------+ bool CElementBase::AddHintToList(CVisualHint *obj) { //--- If an empty pointer is passed, report this and return 'false' if(obj==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return false; } //--- Save the list sorting method int sort_mode=this.m_list_hints.SortMode(); //--- Set the sorting flag for the list by ID this.m_list_hints.Sort(ELEMENT_SORT_BY_ID); //--- If there is no such element in the list, if(this.m_list_hints.Search(obj)==NULL) { //--- return the list to its original sorting and get back the result of adding it to the list this.m_list_hints.Sort(sort_mode); return(this.m_list_hints.Add(obj)>-1); } //--- Return the list to its original sorting this.m_list_hints.Sort(sort_mode); //--- An element with this ID is already in the list - return 'false' return false; }
In the methods that work with the names of tooltip objects, we will now use names of the objects by the previously specified directives:
//+------------------------------------------------------------------+ //| CElementBase::Add hint objects with arrows to the list | //+------------------------------------------------------------------+ bool CElementBase::AddHintsArrowed(void) { //--- Arrays of names and hint types string array[4]={DEF_HINT_NAME_HORZ,DEF_HINT_NAME_VERT,DEF_HINT_NAME_NWSE,DEF_HINT_NAME_NESW}; ENUM_HINT_TYPE type[4]={HINT_TYPE_ARROW_HORZ,HINT_TYPE_ARROW_VERT,HINT_TYPE_ARROW_NWSE,HINT_TYPE_ARROW_NESW}; //--- In the loop, create four hints with arrows bool res=true; for(int i=0;i<(int)array.Size();i++) res &=(this.CreateAndAddNewHint(type[i],array[i],0,0)!=NULL); //--- If there were errors during creation, return 'false' if(!res) return false; //--- In the loop through the array of names of hint objects for(int i=0;i<(int)array.Size();i++) { //--- get the next object by name, CVisualHint *obj=this.GetHint(array[i]); if(obj==NULL) continue; //--- hide the object and draw the appearance (arrows according to the object type) obj.Hide(false); obj.Draw(false); } //--- All is successful return true; }
...
//+------------------------------------------------------------------+ //| CElementBase::Displays the resize cursor | //+------------------------------------------------------------------+ bool CElementBase::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y) { CVisualHint *hint=NULL; // Pointer to the hint int hint_shift_x=0; // Hint offset by X int hint_shift_y=0; // Hint offset by Y //--- Depending on the location of the cursor on the element borders //--- specify the tooltip offsets relative to the cursor coordinates, //--- display the required hint on the chart and get the pointer to this object switch(edge) { //--- Cursor on the right or left border - horizontal double arrow case CURSOR_REGION_RIGHT : case CURSOR_REGION_LEFT : hint_shift_x=1; hint_shift_y=18; this.ShowHintArrowed(HINT_TYPE_ARROW_HORZ,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_HORZ); break; //--- Cursor at the top or bottom border - vertical double arrow case CURSOR_REGION_TOP : case CURSOR_REGION_BOTTOM : hint_shift_x=12; hint_shift_y=4; this.ShowHintArrowed(HINT_TYPE_ARROW_VERT,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_VERT); break; //--- Cursor in the upper left or lower right corner - a diagonal double arrow from top left to bottom right case CURSOR_REGION_LEFT_TOP : case CURSOR_REGION_RIGHT_BOTTOM : hint_shift_x=10; hint_shift_y=2; this.ShowHintArrowed(HINT_TYPE_ARROW_NWSE,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_NWSE); break; //--- Cursor in the lower left or upper right corner - a diagonal double arrow from bottom left to top right case CURSOR_REGION_LEFT_BOTTOM : case CURSOR_REGION_RIGHT_TOP : hint_shift_x=5; hint_shift_y=12; this.ShowHintArrowed(HINT_TYPE_ARROW_NESW,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_NESW); break; //--- By default, do nothing default: break; } //--- Return the result of adjusting the position of the tooltip relative to the cursor return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false); }
etc.
In the tooltip object class, declare two new methods that draw two new tooltips:
//+------------------------------------------------------------------+ //| Hint class | //+------------------------------------------------------------------+ class CVisualHint : public CButton { protected: ENUM_HINT_TYPE m_hint_type; // Hint type //--- Draw (1) a tooltip, (2) a horizontal, (3) a vertical arrow, //--- arrows (4) top-left --- bottom-right, (5) bottom-left --- top-right, //--- (6) horizontal and (7) vertical offset arrows void DrawTooltip(void); void DrawArrHorz(void); void DrawArrVert(void); void DrawArrNWSE(void); void DrawArrNESW(void); void DrawArrShiftHorz(void); void DrawArrShiftVert(void); //--- Initialize colors for the hint type (1) Tooltip, (2) arrows void InitColorsTooltip(void); void InitColorsArrowed(void); public: //--- (1) Set and (2) return the hint type void SetHintType(const ENUM_HINT_TYPE type); ENUM_HINT_TYPE HintType(void) const { return this.m_hint_type; } //--- Draw the appearance virtual void Draw(const bool chart_redraw); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle) { return CButton::Save(file_handle); } virtual bool Load(const int file_handle) { return CButton::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_HINT); } //--- Initialize (1) the class object and (2) default object colors void Init(const string text); virtual void InitColors(void); //--- Constructors/destructor CVisualHint(void); CVisualHint(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CVisualHint (void) {} };
Outside of the class body, write their implementation:
//+------------------------------------------------------------------+ //| CVisualHint::Draw horizontal offset arrows | //+------------------------------------------------------------------+ void CVisualHint::DrawArrShiftHorz(void) { //--- Clear the drawing area this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Draw horizontal offset arrows this.m_painter.ArrowShiftHorz(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true); } //+------------------------------------------------------------------+ //| CVisualHint::Draw vertical offset arrows | //+------------------------------------------------------------------+ void CVisualHint::DrawArrShiftVert(void) { //--- Clear the drawing area this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Draw horizontal offset arrows this.m_painter.ArrowShiftVert(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true); }
Add the processing of new methods for drawing tooltips to the methods for drawing and setting the tooltip type:
//+------------------------------------------------------------------+ //| CVisualHint::Set the hint type | //+------------------------------------------------------------------+ void CVisualHint::SetHintType(const ENUM_HINT_TYPE type) { //--- If the passed type matches the set one, leave if(this.m_hint_type==type) return; //--- Set a new hint type this.m_hint_type=type; //--- Depending on the hint type, set the object dimensions switch(this.m_hint_type) { case HINT_TYPE_ARROW_HORZ : this.Resize(17,7); break; case HINT_TYPE_ARROW_VERT : this.Resize(7,17); break; case HINT_TYPE_ARROW_NESW : case HINT_TYPE_ARROW_NWSE : this.Resize(13,13); break; case HINT_TYPE_ARROW_SHIFT_HORZ : case HINT_TYPE_ARROW_SHIFT_VERT : this.Resize(18,18); break; default : break; } //--- Set the offset and dimensions of the image area, //--- initialize colors based on the hint type this.SetImageBound(0,0,this.Width(),this.Height()); this.InitColors(); } //+------------------------------------------------------------------+ //| CVisualHint::Draw the appearance | //+------------------------------------------------------------------+ void CVisualHint::Draw(const bool chart_redraw) { //--- Depending on the type of hint, call the corresponding drawing method switch(this.m_hint_type) { case HINT_TYPE_ARROW_HORZ : this.DrawArrHorz(); break; case HINT_TYPE_ARROW_VERT : this.DrawArrVert(); break; case HINT_TYPE_ARROW_NESW : this.DrawArrNESW(); break; case HINT_TYPE_ARROW_NWSE : this.DrawArrNWSE(); break; case HINT_TYPE_ARROW_SHIFT_HORZ : this.DrawArrShiftHorz(); break; case HINT_TYPE_ARROW_SHIFT_VERT : this.DrawArrShiftVert(); break; default : this.DrawTooltip(); break; } //--- If specified, update the chart if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Refine the CPanel panel class.
Add auxiliary methods for working with lists of attached elements:
//+------------------------------------------------------------------+ //| Panel class | //+------------------------------------------------------------------+ class CPanel : public CLabel { private: CElementBase m_temp_elm; // Temporary object for element searching CBound m_temp_bound; // Temporary object for area searching protected: CListElm m_list_elm; // List of attached elements CListElm m_list_bounds; // List of areas //--- Add a new element to the list bool AddNewElement(CElementBase *element); public: //--- Return the pointer to the list of (1) attached elements and (2) areas CListElm *GetListAttachedElements(void) { return &this.m_list_elm; } CListElm *GetListBounds(void) { return &this.m_list_bounds; } //--- Return the attached element by (1) index in the list, (2) ID and (3) specified object name CElementBase *GetAttachedElementAt(const uint index) { return this.m_list_elm.GetNodeAtIndex(index); } CElementBase *GetAttachedElementByID(const int id); CElementBase *GetAttachedElementByName(const string name); //--- Returns the number of added elements int AttachedElementsTotal(void) const { return this.m_list_elm.Total(); } //--- Return the area by (1) index in the list, (2) ID and (3) specified area name CBound *GetBoundAt(const uint index) { return this.m_list_bounds.GetNodeAtIndex(index); } CBound *GetBoundByID(const int id); CBound *GetBoundByName(const string name); //--- Create and add (1) a new and (2) a previously created element to the list virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h); virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy); //--- Remove the specified element bool DeleteElement(const int index) { return this.m_list_elm.Delete(index); } //--- (1) Create and add a new area to the list and (2) remove the specified region CBound *InsertNewBound(const string name,const int dx,const int dy,const int w,const int h); bool DeleteBound(const int index) { return this.m_list_bounds.Delete(index); } //--- (1) Assign an object to the specified area and (2) unassign an object from the specified area bool AssignObjectToBound(const int bound, CBaseObj *object); bool UnassignObjectFromBound(const int bound); //--- Resize the object virtual bool ResizeW(const int w); virtual bool ResizeH(const int h); virtual bool Resize(const int w,const int h); //--- Draw the appearance virtual void Draw(const bool chart_redraw); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_PANEL); } //--- Initialize (1) the class object and (2) default object colors void Init(void); virtual void InitColors(void); //--- Set new XY object coordinates virtual bool Move(const int x,const int y); //--- Shift the object by XY axes by the specified offset virtual bool Shift(const int dx,const int dy); //--- Set both the element coordinates and dimensions virtual bool MoveXYWidthResize(const int x,const int y,const int w,const int h); //--- (1) Hide and (2) display the object on all chart periods, //--- (3) bring the object to the front, (4) block, (5) unblock the element, virtual void Hide(const bool chart_redraw); virtual void Show(const bool chart_redraw); virtual void BringToTop(const bool chart_redraw); virtual void Block(const bool chart_redraw); virtual void Unblock(const bool chart_redraw); //--- Display the object description in the journal virtual void Print(void); //--- Print a list of (1) attached objects and (2) areas void PrintAttached(const uint tab=3); void PrintBounds(void); //--- Event handler virtual void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Timer event handler virtual void TimerEventHandler(void); //--- Constructors/destructor CPanel(void); CPanel(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CPanel (void) { this.m_list_elm.Clear(); this.m_list_bounds.Clear(); } };
The method of element width change ResizeW() has a potential error:
//--- Get the pointer to the base element and, if it exists, its type is container, //--- check the ratio of the current element dimensions to the container dimensions //--- to display scrollbars in the container if necessary CContainer *base=this.GetContainer(); if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER) base.CheckElementSizes(&this);
If the container received by the GetContainer() method is not the CContainer class, the program will terminate due to a critical error due to inability to convert object types.
Fix it:
//+------------------------------------------------------------------+ //| CPanel::Change the object width | //+------------------------------------------------------------------+ bool CPanel::ResizeW(const int w) { if(!this.ObjectResizeW(w)) return false; this.BoundResizeW(w); this.SetImageSize(w,this.Height()); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } //--- Get the pointer to the base element and, if it exists, its type is container, //--- check the ratio of the current element dimensions to the container dimensions //--- to display scrollbars in the container if necessary CContainer *container=NULL; CCanvasBase *base=this.GetContainer(); if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER) { container=base; container.CheckElementSizes(&this); } //--- In the loop through the attached elements, trim each element along the container boundaries int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.ObjectTrim(); } //--- All is successful return true; }
Now we can accurately assign a pointer to the correct type of object.
In the method of adding a new element to the list, remember the initial sorting of the list, and return it after adding the object to the list:
//+------------------------------------------------------------------+ //| CPanel::Add a new element to the list | //+------------------------------------------------------------------+ bool CPanel::AddNewElement(CElementBase *element) { //--- If an empty pointer is passed, report this and return 'false' if(element==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return false; } //--- Save the list sorting method int sort_mode=this.m_list_elm.SortMode(); //--- Set the sorting flag for the list by ID this.m_list_elm.Sort(ELEMENT_SORT_BY_ID); //--- If there is no such element in the list, if(this.m_list_elm.Search(element)==NULL) { //--- return the list to its original sorting and get back the result of adding it to the list this.m_list_elm.Sort(sort_mode); return(this.m_list_elm.Add(element)>-1); } //--- Return the list to its original sorting this.m_list_elm.Sort(sort_mode); //--- An element with this ID is already in the list - return 'false' return false; }
Refine a method that creates and adds a new area to the list:
//+------------------------------------------------------------------+ //| Create and add a new area to the list | //+------------------------------------------------------------------+ CBound *CPanel::InsertNewBound(const string name,const int dx,const int dy,const int w,const int h) { //--- Check whether the list contains a region with the specified name and, if it does, report this and return NULL this.m_temp_bound.SetName(name); //--- Save the list sorting method int sort_mode=this.m_list_bounds.SortMode(); //--- Set the sorting by name flag to the list this.m_list_bounds.Sort(ELEMENT_SORT_BY_NAME); if(this.m_list_bounds.Search(&this.m_temp_bound)!=NULL) { //--- Return the list to its original sorting, report that such an object already exists and return NULL this.m_list_bounds.Sort(sort_mode); ::PrintFormat("%s: Error. An area named \"%s\" is already in the list",__FUNCTION__,name); return NULL; } //--- Return the list to its original sorting this.m_list_bounds.Sort(sort_mode); //--- Create a new area object; if unsuccessful, report it and return NULL CBound *bound=new CBound(dx,dy,w,h); if(bound==NULL) { ::PrintFormat("%s: Error. Failed to create CBound object",__FUNCTION__); return NULL; } //--- Set the area name and ID, and return the pointer to the object bound.SetName(name); bound.SetID(this.m_list_bounds.Total()); //--- If failed to add the new object to the list, report this, remove the object and return NULL if(this.m_list_bounds.Add(bound)==-1) { ::PrintFormat("%s: Error. Failed to add CBound object to list",__FUNCTION__); delete bound; return NULL; } return bound; }
There are two methods in the class that were declared but not implemented. Fix it.
A Method That Returns an Area by ID:
//+------------------------------------------------------------------+ //| CPanel::Return area by ID | //+------------------------------------------------------------------+ CBound *CPanel::GetBoundByID(const int id) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { CBound *bound=this.GetBoundAt(i); if(bound!=NULL && bound.ID()==id) return bound; } return NULL; }
In a simple loop through objects of element's areas, we search for the area with the indicated identifier and return a pointer to the found object.
A Method That Returns an Area by the Assigned Name of the Area:
//+------------------------------------------------------------------+ //| CPanel::Return the area by the assigned area name | //+------------------------------------------------------------------+ CBound *CPanel::GetBoundByName(const string name) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { CBound *bound=this.GetBoundAt(i); if(bound!=NULL && bound.Name()==name) return bound; } return NULL; }
In a simple loop through objects of element's areas, we search for the area with the indicated name and return a pointer to the found object.
Write implementation of the two declared methods.
A Method That Assigns an Object to an Indicated Area:
//+------------------------------------------------------------------+ //| CPanel::Assign an object to the specified area | //+------------------------------------------------------------------+ bool CPanel::AssignObjectToBound(const int bound,CBaseObj *object) { CBound *bound_obj=this.GetBoundAt(bound); if(bound_obj==NULL) { ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound); return false; } bound_obj.AssignObject(object); return true; }
We get the area by its identifier and call the area object method, which assigns the object to the area.
A Method That Cancels Assignment Of an Object From the Indicated Area:
//+------------------------------------------------------------------+ //| CPanel::Unassign an object from the specified area | //+------------------------------------------------------------------+ bool CPanel::UnassignObjectFromBound(const int bound) { CBound *bound_obj=this.GetBoundAt(bound); if(bound_obj==NULL) { ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound); return false; } bound_obj.UnassignObject(); return true; }
We get the area by its identifier and call the area object method, that cancels a previously assigned object to the area.
There is a flaw in the horizontal and vertical scrollbar classes. This results in a fact that when scrolling the thumb, several scrollbars can shift at once if there is more than one container on the chart. To correct this behavior, the name of the active element should be read from the sparam parameter and it should be compared with the thumb name. If there is a substring with the name of the active element within the thumb name, then it is this thumb that scrolls. Make edits to the mouse wheel scroll handlers in both classes of scrollbar thumbs:
//+------------------------------------------------------------------+ //| CScrollBarThumbH::Wheel scroll handler | //+------------------------------------------------------------------+ void CScrollBarThumbH::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Get the pointer to the base object (the "horizontal scroll bar" control) CCanvasBase *base_obj=this.GetContainer(); //--- Get the name of the main object in the hierarchy by the value in sparam string array_names[]; string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : ""); //--- If the main object in the hierarchy is not ours, leave if(::StringFind(this.NameFG(),name_main)!=0) return; //--- If the slider's movability flag is not set, or the pointer to the base object is not received, leave if(!this.IsMovable() || base_obj==NULL) return; //--- Get the width of the base object and calculate the boundaries of the space for the slider int base_w=base_obj.Width(); int base_left=base_obj.X()+base_obj.Height(); int base_right=base_obj.Right()-base_obj.Height()+1; //--- Set the offset direction depending on the mouse wheel rotation direction int dx=(dparam<0 ? 2 : dparam>0 ? -2 : 0); if(dx==0) dx=(int)lparam; //--- If the slider goes beyond the left edge of its area when moving, set it to the left edge if(dx<0 && this.X()+dx<=base_left) this.MoveX(base_left); //--- otherwise, if the slider moves beyond the right edge of its area, position it along the right edge else if(dx>0 && this.Right()+dx>=base_right) this.MoveX(base_right-this.Width()); //--- Otherwise, if the slider is within its area, move it by the offset value else { this.ShiftX(dx); } //--- Calculate the slider position int thumb_pos=this.X()-base_left; //--- Get cursor coordinates int x=CCommonManager::GetInstance().CursorX(); int y=CCommonManager::GetInstance().CursorY(); //--- If the cursor lands on the slider, change the color to "In focus", if(this.Contains(x,y)) this.OnFocusEvent(id,lparam,dparam,sparam); //--- otherwise, return the color to "Default" else this.OnReleaseEvent(id,lparam,dparam,sparam); //--- Send a custom event to the chart with the slider position in lparam and the object name in sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG()); //--- Redraw the chart if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); } //+------------------------------------------------------------------+ //| CScrollBarThumbV::Wheel scroll handler | //+------------------------------------------------------------------+ void CScrollBarThumbV::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- Get the pointer to the base object (the "vertical scroll bar" control) CCanvasBase *base_obj=this.GetContainer(); //--- Get the name of the main object in the hierarchy by the value in sparam string array_names[]; string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : ""); //--- If the main object in the hierarchy is not ours, leave if(::StringFind(this.NameFG(),name_main)!=0) return; //--- If the slider's movability flag is not set, or the pointer to the base object is not received, leave if(!this.IsMovable() || base_obj==NULL) return; //--- Get the height of the base object and calculate the boundaries of the space for the slider int base_h=base_obj.Height(); int base_top=base_obj.Y()+base_obj.Width(); int base_bottom=base_obj.Bottom()-base_obj.Width()+1; //--- Set the offset direction depending on the mouse wheel rotation direction int dy=(dparam<0 ? 2 : dparam>0 ? -2 : 0); if(dy==0) dy=(int)lparam; //--- If the slider goes beyond the top edge of its area when moving, set it to the top edge if(dy<0 && this.Y()+dy<=base_top) this.MoveY(base_top); //--- otherwise, if the slider moves beyond the bottom edge of its area, position it along the bottom edge else if(dy>0 && this.Bottom()+dy>=base_bottom) this.MoveY(base_bottom-this.Height()); //--- Otherwise, if the slider is within its area, move it by the offset value else { this.ShiftY(dy); } //--- Calculate the slider position int thumb_pos=this.Y()-base_top; //--- Get cursor coordinates int x=CCommonManager::GetInstance().CursorX(); int y=CCommonManager::GetInstance().CursorY(); //--- If the cursor lands on the slider, change the color to "In focus", if(this.Contains(x,y)) this.OnFocusEvent(id,lparam,dparam,sparam); //--- otherwise, return the color to "Default" else this.OnReleaseEvent(id,lparam,dparam,sparam); //--- Send a custom event to the chart with the slider position in lparam and the object name in sparam ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG()); //--- Redraw the chart if(this.m_chart_redraw) ::ChartRedraw(this.m_chart_id); }
In the CContainer container class, in the method of shifting the container contents horizontally, it is important to take into account that for a table, when scrolling horizontally, it is also necessary to scroll the table header. Implement it:
//+-------------------------------------------------------------------+ //|CContainer::Shift the content horizontally by the specified value | //+-------------------------------------------------------------------+ bool CContainer::ContentShiftHorz(const int value) { //--- Get the pointer to the container contents CElementBase *elm=this.GetAttachedElement(); if(elm==NULL) return false; //--- Get the table header for the CTableView element CElementBase *elm_container=elm.GetContainer(); CTableHeaderView *table_header=NULL; if(elm_container!=NULL && ::StringFind(elm.Name(),"Table")==0) { CElementBase *obj=elm_container.GetContainer(); if(obj!=NULL && obj.Type()==ELEMENT_TYPE_TABLE_VIEW) { CTableView *table_view=obj; table_header=table_view.GetHeader(); } } //--- Calculate the offset value based on the slider position int content_offset=this.CalculateContentOffsetHorz(value); //--- Shift the header bool res=true; if(table_header!=NULL) { res &=table_header.MoveX(this.X()-content_offset); } //--- Return the result of shifting the content by the calculated value res &=elm.MoveX(this.X()-content_offset); return res; }
In the method that returns the type of the element that sent the event, the base element was incorrectly searched in the container object class:
//--- Get the names of all elements in the hierarchy (if an error occurs, return -1) string names[]={}; int total = GetElementNames(name,"_",names); if(total==WRONG_VALUE) return WRONG_VALUE; //--- If the name of the base element in the hierarchy does not match the name of the container, then this is not our event - leave string base_name=names[0]; if(base_name!=this.NameFG()) return WRONG_VALUE;
The base object is not always the very first one in the hierarchy of the container's graphical elements. There may be situations where one element is nested into another multiple times, and then for the last elements in the hierarchy, their base object will not be the first one in the list of names of all nested container elements. Search for the base object correctly:
//+------------------------------------------------------------------+ //| Return the type of the element that sent the event | //+------------------------------------------------------------------+ ENUM_ELEMENT_TYPE CContainer::GetEventElementType(const string name) { //--- Get the names of all elements in the hierarchy (if an error occurs, return -1) string names[]={}; int total = GetElementNames(name,"_",names); if(total==WRONG_VALUE) return WRONG_VALUE; //--- Find the container name in the array that is closest to the name of the element with the event int cntr_index=-1; // Index of the container name in the array of names in the element hierarchy string cntr_name=""; // The name of the container in the array of names in the element hierarchy //--- Search in the loop for the very first occurrence of the CNTR substring from the end for(int i=total-1;i>=0;i--) { if(::StringFind(names[i],"CNTR")==0) { cntr_name=names[i]; cntr_index=i; break; } } //--- If the container name is not found in the array (index is -1), return -1 if(cntr_index==WRONG_VALUE) return WRONG_VALUE; //--- If the element name does not contain a substring with the name of the base element, then this is not our event - leave string base_name=names[cntr_index]; if(::StringFind(this.NameFG(),base_name)==WRONG_VALUE) return WRONG_VALUE; //--- Events that do not arrive from scrollbars are skipped string check_name=::StringSubstr(names[cntr_index+1],0,4); if(check_name!="SCBH" && check_name!="SCBV") return WRONG_VALUE; //--- Get the name of the element the event came from and initialize the element type string elm_name=names[names.Size()-1]; ENUM_ELEMENT_TYPE type=WRONG_VALUE; //--- Check and write the element type //--- Up arrow button if(::StringFind(elm_name,"BTARU")==0) type=ELEMENT_TYPE_BUTTON_ARROW_UP; //--- Down arrow button else if(::StringFind(elm_name,"BTARD")==0) type=ELEMENT_TYPE_BUTTON_ARROW_DOWN; //--- Left arrow button else if(::StringFind(elm_name,"BTARL")==0) type=ELEMENT_TYPE_BUTTON_ARROW_LEFT; //--- Right arrow button else if(::StringFind(elm_name,"BTARR")==0) type=ELEMENT_TYPE_BUTTON_ARROW_RIGHT; //--- Horizontal scroll bar slider else if(::StringFind(elm_name,"THMBH")==0) type=ELEMENT_TYPE_SCROLLBAR_THUMB_H; //--- Vertical scroll bar slider else if(::StringFind(elm_name,"THMBV")==0) type=ELEMENT_TYPE_SCROLLBAR_THUMB_V; //--- ScrollBarHorisontal control else if(::StringFind(elm_name,"SCBH")==0) type=ELEMENT_TYPE_SCROLLBAR_H; //--- ScrollBarVertical control else if(::StringFind(elm_name,"SCBV")==0) type=ELEMENT_TYPE_SCROLLBAR_V; //--- Return the element type return type; }
Now, refine the visual representation class of the CTableCellView table cell. The refinements will affect limitations of cell rendering — you do not need to draw a cell that is outside its container, so as not to waste resources on drawing outside the graphical object. Also make it possible to change the color of the text displayed in the cell and correct the text output to the cell for different types of text anchor points.
Declare new variables and methods:
//+------------------------------------------------------------------+ //| Table cell visual representation class | //+------------------------------------------------------------------+ class CTableCellView : public CBoundedObj { protected: CTableCell *m_table_cell_model; // Pointer to the cell model CImagePainter *m_painter; // Pointer to the drawing object CTableRowView *m_element_base; // Pointer to the base element (table row) CCanvas *m_background; // Pointer to the background canvas CCanvas *m_foreground; // Pointer to the foreground canvas int m_index; // Index in the cell list ENUM_ANCHOR_POINT m_text_anchor; // Text anchor point (alignment in cell) int m_text_x; // Text X coordinate (offset relative to the left border of the object area) int m_text_y; // Y text coordinate (offset relative to the top border of the object area) ushort m_text[]; // Text color m_fore_color; // Foreground color //--- Return the offsets of the initial drawing coordinates on the canvas relative to the canvas and the coordinates of the base element int CanvasOffsetX(void) const { return(this.m_element_base.ObjectX()-this.m_element_base.X()); } int CanvasOffsetY(void) const { return(this.m_element_base.ObjectY()-this.m_element_base.Y()); } //--- Return the adjusted coordinate of a point on the canvas, taking into account the offset of the canvas relative to the base element int AdjX(const int x) const { return(x-this.CanvasOffsetX()); } int AdjY(const int y) const { return(y-this.CanvasOffsetY()); } //--- Return the X and Y coordinates of the text based on the anchor point bool GetTextCoordsByAnchor(int &x, int &y, int &dir_x, int dir_y); //--- Return the pointer to the table row panel container CContainer *GetRowsPanelContainer(void); public: //--- Return the pointer to the specified (1) background and (2) foreground canvas CCanvas *GetBackground(void) { return this.m_background; } CCanvas *GetForeground(void) { return this.m_foreground; } //--- Get the boundaries of the parent container object int ContainerLimitLeft(void) const { return(this.m_element_base==NULL ? this.X() : this.m_element_base.LimitLeft()); } int ContainerLimitRight(void) const { return(this.m_element_base==NULL ? this.Right() : this.m_element_base.LimitRight()); } int ContainerLimitTop(void) const { return(this.m_element_base==NULL ? this.Y() : this.m_element_base.LimitTop()); } int ContainerLimitBottom(void) const { return(this.m_element_base==NULL ? this.Bottom() : this.m_element_base.LimitBottom()); } //--- Return the flag that the object is located outside its container virtual bool IsOutOfContainer(void); //--- (1) Set and (2) return the cell text void SetText(const string text) { ::StringToShortArray(text,this.m_text); } string Text(void) const { return ::ShortArrayToString(this.m_text); } //--- (1) Set and (2) return the cell text color void SetForeColor(const color clr) { this.m_fore_color=clr; } color ForeColor(void) const { return this.m_fore_color; } //--- Set the ID virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Set and (2) return the cell index void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Set and (2) return the text offset along the X axis void SetTextShiftX(const int shift) { this.m_text_x=shift; } int TextShiftX(void) const { return this.m_text_x; } //--- (1) Set and (2) return the text offset along the Y axis void SetTextShiftY(const int shift) { this.m_text_y=shift; } int TextShiftY(void) const { return this.m_text_y; } //--- (1) Set and (2) return the text anchor point void SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw); int TextAnchor(void) const { return this.m_text_anchor; } //--- Sets the anchor point and text offsets void SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw); //--- Assign the base element (table row) void RowAssign(CTableRowView *base_element); //--- (1) Assign and (2) return the cell model bool TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h); CTableCell *GetTableCellModel(void) { return this.m_table_cell_model; } //--- Print the assigned cell model in the journal void TableCellModelPrint(void); //--- (1) Fill the object with the background color, (2) update the object to reflect the changes and (3) draw the appearance virtual void Clear(const bool chart_redraw); virtual void Update(const bool chart_redraw); virtual void Draw(const bool chart_redraw); //--- Display the text virtual void DrawText(const int dx, const int dy, const string text, const bool chart_redraw); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0)const { return CBaseObj::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_CELL_VIEW); } //--- Initialize a class object void Init(const string text); //--- Return the object description virtual string Description(void); //--- Constructors/destructor CTableCellView(void); CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h); ~CTableCellView (void){} };
In the method that assigns a row, background and foreground canvases to a cell, set the text color to be equal to the foreground color:
//+---------------------------------------------------------------------+ //| CTableCellView::Assigns the row, background and foreground canvases | //+---------------------------------------------------------------------+ void CTableCellView::RowAssign(CTableRowView *base_element) { if(base_element==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return; } this.m_element_base=base_element; this.m_background=this.m_element_base.GetBackground(); this.m_foreground=this.m_element_base.GetForeground(); this.m_painter=this.m_element_base.Painter(); this.m_fore_color=this.m_element_base.ForeColor(); }
In the method that returns the X and Y coordinates of the text depending on the anchor point, add variables that will record the signs of the shift direction (+1 / -1) along the X and Y axes:
//+------------------------------------------------------------------+ //| CTableCellView::Return the X and Y coordinates of the text | //| depending on the anchor point | //+------------------------------------------------------------------+ bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y, int &dir_x,int dir_y) { //--- Get the text size in the cell int text_w=0, text_h=0; this.m_foreground.TextSize(this.Text(),text_w,text_h); if(text_w==0 || text_h==0) return false; //--- Depending on the text anchor point in the cell, //--- calculate its initial coordinates (upper left corner) switch(this.m_text_anchor) { //--- Anchor point left centered case ANCHOR_LEFT : x=0; y=(this.Height()-text_h)/2; dir_x=1; dir_y=1; break; //--- Anchor point in the lower left corner case ANCHOR_LEFT_LOWER : x=0; y=this.Height()-text_h; dir_x= 1; dir_y=-1; break; //--- Anchor point at the bottom center case ANCHOR_LOWER : x=(this.Width()-text_w)/2; y=this.Height()-text_h; dir_x= 1; dir_y=-1; break; //--- Anchor point in the lower right corner case ANCHOR_RIGHT_LOWER : x=this.Width()-text_w; y=this.Height()-text_h; dir_x=-1; dir_y=-1; break; //--- Anchor point is right centered case ANCHOR_RIGHT : x=this.Width()-text_w; y=(this.Height()-text_h)/2; dir_x=-1; dir_y= 1; break; //--- Anchor point in the upper right corner case ANCHOR_RIGHT_UPPER : x=this.Width()-text_w; y=0; dir_x=-1; dir_y= 1; break; //--- Anchor point at top center case ANCHOR_UPPER : x=(this.Width()-text_w)/2; y=0; dir_x=1; dir_y=1; break; //--- Anchor point is strictly in the center of the object case ANCHOR_CENTER : x=(this.Width()-text_w)/2; y=(this.Height()-text_h)/2; dir_x=1; dir_y=1; break; //--- Anchor point in the upper left corner //---ANCHOR_LEFT_UPPER default: x=0; y=0; dir_x=1; dir_y=1; break; } return true; }
In the method that draws the view, we now use the values obtained as a multiplier to shift the text relative to X and Y axes, and also do not draw the cell if it is outside its container:
//+------------------------------------------------------------------+ //| CTableCellView::Draw the appearance | //+------------------------------------------------------------------+ void CTableCellView::Draw(const bool chart_redraw) { //--- If the cell is outside the table row container, leave if(this.IsOutOfContainer()) return; //--- Get the text coordinates and the offset direction depending on the anchor point int text_x=0, text_y=0; int dir_horz=0, dir_vert=0; if(!this.GetTextCoordsByAnchor(text_x,text_y,dir_horz,dir_vert)) return; //--- Correct the text coordinates int x=this.AdjX(this.X()+text_x); int y=this.AdjY(this.Y()+text_y); //--- Set the coordinates of the dividing line int x1=this.AdjX(this.X()); int x2=this.AdjX(this.X()); int y1=this.AdjY(this.Y()); int y2=this.AdjY(this.Bottom()); //--- Display the text on the foreground canvas taking into account the offset direction without updating the chart this.DrawText(x+this.m_text_x*dir_horz,y+this.m_text_y*dir_vert,this.Text(),false); //--- If this is not the rightmost cell, draw a vertical dividing line near the cell on the right if(this.m_element_base!=NULL && this.Index()<this.m_element_base.CellsTotal()-1) { int line_x=this.AdjX(this.Right()); this.m_background.Line(line_x,y1,line_x,y2,::ColorToARGB(this.m_element_base.BorderColor(),this.m_element_base.AlphaBG())); } //--- Update the background canvas with the specified chart redraw flag this.m_background.Update(chart_redraw); }
Now the text in the cell will be positioned correctly, regardless of the anchor point of the text.
A Method That Returns a Pointer to the Table Row Panel Container:
//+------------------------------------------------------------------+ //| CTableCellView::Return the pointer | //| to the table row panel container | //+------------------------------------------------------------------+ CContainer *CTableCellView::GetRowsPanelContainer(void) { //--- Check the row if(this.m_element_base==NULL) return NULL; //--- Get the panel for placing rows CPanel *rows_area=this.m_element_base.GetContainer(); if(rows_area==NULL) return NULL; //--- Return the panel container with rows return rows_area.GetContainer(); }
The cell is located inside the row. The row, along with other rows, is located on the panel. The panel, in turn, is attached to a container, inside of which scrollbars can scroll it. The method returns a pointer to a container with scrollbars.
A Method That Returns a Flag Indicating That the Object Is Located Outside Its Container:
//+------------------------------------------------------------------+ //| CTableCellView::Return the flag that the object is | //| located outside of its container | //+------------------------------------------------------------------+ bool CTableCellView::IsOutOfContainer(void) { //--- Check the row if(this.m_element_base==NULL) return false; //--- Get the panel container with rows CContainer *container=this.GetRowsPanelContainer(); if(container==NULL) return false; //--- Get the cell boundaries by all sides int cell_l=this.m_element_base.X()+this.X(); int cell_r=this.m_element_base.X()+this.Right(); int cell_t=this.m_element_base.Y()+this.Y(); int cell_b=this.m_element_base.Y()+this.Bottom(); //--- Return the result of checking that the object is completely outside the container return(cell_r <= container.X() || cell_l >= container.Right() || cell_b <= container.Y() || cell_t >= container.Bottom()); }
The cell is located inside its own area located in the row. The method calculates cell coordinates relative to the row and returns a flag indicating that the calculated cell coordinates go beyond the container.
Now, refine the visual representation class of the CTableRowView table row.
Declare new methods and in the destructor clear the cell list:
//+------------------------------------------------------------------+ //| Table row visual representation class | //+------------------------------------------------------------------+ class CTableRowView : public CPanel { protected: CTableCellView m_temp_cell; // Temporary cell object for searching CTableRow *m_table_row_model; // Pointer to the row model CListElm m_list_cells; // Cell list int m_index; // Index in the list of rows //--- Create and add a new cell representation object to the list CTableCellView *InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h); //--- Delete the specified row region and the cell with the corresponding index bool BoundCellDelete(const int index); public: //--- Return (1) the list, (2) the number of cells and (3) the cell CListElm *GetListCells(void) { return &this.m_list_cells; } int CellsTotal(void) const { return this.m_list_cells.Total(); } CTableCellView *GetCellView(const uint index) { return this.m_list_cells.GetNodeAtIndex(index); } //--- Set the ID virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Set and (2) return the row index void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Set and (2) return the row model bool TableRowModelAssign(CTableRow *row_model); CTableRow *GetTableRowModel(void) { return this.m_table_row_model; } //--- Update the row with the updated model bool TableRowModelUpdate(CTableRow *row_model); //--- Recalculate cell areas bool RecalculateBounds(CListElm *list_bounds); //--- Print the assigned row model in the journal void TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); //--- Draw the appearance virtual void Draw(const bool chart_redraw); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0)const { return CLabel::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_ROW_VIEW); } //--- Initialize (1) the class object and (2) default object colors void Init(void); virtual void InitColors(void); //--- Constructors/destructor CTableRowView(void); CTableRowView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableRowView (void){ this.m_list_cells.Clear(); } };
In the method of creating and adding to the list a new cell representation object, remember the sorting mode of the list, and after adding, return the list to its original sorting:
//+------------------------------------------------------------------+ //| CTableRowView::Create and add a new | //| cell representation object to the list | //+------------------------------------------------------------------+ CTableCellView *CTableRowView::InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h) { //--- Check whether the list contains an object with the specified ID and, if it does, report this and return NULL this.m_temp_cell.SetIndex(index); //--- Save the list sorting method int sort_mode=this.m_list_cells.SortMode(); //--- Set the sorting flag for the list by ID this.m_list_cells.Sort(ELEMENT_SORT_BY_ID); if(this.m_list_cells.Search(&this.m_temp_cell)!=NULL) { //--- Return the list to its original sorting, report that such an object already exists and return NULL this.m_list_cells.Sort(sort_mode); ::PrintFormat("%s: Error. The TableCellView object with index %d is already in the list",__FUNCTION__,index); return NULL; } //--- Return the list to its original sorting this.m_list_cells.Sort(sort_mode); //--- Create a cell object name string name="TableCellView"+(string)this.Index()+"x"+(string)index; //--- Create a new TableCellView object; in case of a failure, report it and return NULL CTableCellView *cell_view=new CTableCellView(index,name,text,dx,dy,w,h); if(cell_view==NULL) { ::PrintFormat("%s: Error. Failed to create CTableCellView object",__FUNCTION__); return NULL; } //--- If failed to add the new object to the list, report this, remove the object and return NULL if(this.m_list_cells.Add(cell_view)==-1) { ::PrintFormat("%s: Error. Failed to add CTableCellView object to list",__FUNCTION__); delete cell_view; return NULL; } //--- Assign the base element (row) and return the pointer to the object cell_view.RowAssign(&this); return cell_view; }
In the row model installation method, the column width will be calculated based on the width of the row panel, not the row itself, since the row is slightly narrower than the panel. The table header width is also equal to that of the row panel, so we use the panel width to match the widths of cells and columns:
//+------------------------------------------------------------------+ //| CTableRowView::Set the row model | //+------------------------------------------------------------------+ bool CTableRowView::TableRowModelAssign(CTableRow *row_model) { //--- If an empty object is passed, report this and return 'false' if(row_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- If the passed row model does not contain a single cell, report this and return 'false' int total=(int)row_model.CellsTotal(); if(total==0) { ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__); return false; } //--- Save the pointer to the passed row model this.m_table_row_model=row_model; //--- calculate the cell width based on the row panel width CCanvasBase *base=this.GetContainer(); int w=(base!=NULL ? base.Width() : this.Width()); int cell_w=(int)::fmax(::round((double)w/(double)total),DEF_TABLE_COLUMN_MIN_W); //--- In the loop by the number of cells in the row model for(int i=0;i<total;i++) { //--- get the model of the next cell, CTableCell *cell_model=this.m_table_row_model.GetCell(i); if(cell_model==NULL) return false; //--- calculate the coordinate and create a name for the cell area int x=cell_w*i; string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i; //--- Create a new cell area CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height()); if(cell_bound==NULL) return false; //--- Create a new cell visual representation object CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height()); if(cell_view==NULL) return false; //--- Assign the corresponding visual representation object of the cell to the current cell area cell_bound.AssignObject(cell_view); } //--- All is successful return true; }
Cells in a table row can update their view, for example, when adding or deleting a table column. Such changes made to the table model must also be displayed in the visual representation of the table.
Create a special method for this.
A Method That Updates a Row With an Updated Model:
//+------------------------------------------------------------------+ //| CTableRowView::Update the row with the updated model | //+------------------------------------------------------------------+ bool CTableRowView::TableRowModelUpdate(CTableRow *row_model) { //--- If an empty object is passed, report this and return 'false' if(row_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- If the passed row model does not contain a single cell, report this and return 'false' int total_model=(int)row_model.CellsTotal(); // Number of cells in the row model if(total_model==0) { ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__); return false; } //--- Save the pointer to the passed row model this.m_table_row_model=row_model; //--- Calculate the cell width based on the row panel width CCanvasBase *base=this.GetContainer(); int w=(base!=NULL ? base.Width() : this.Width()); int cell_w=(int)::fmax(::round((double)w/(double)total_model),DEF_TABLE_COLUMN_MIN_W); CBound *cell_bound=NULL; int total_bounds=this.m_list_bounds.Total(); // Number of areas int diff=total_model-total_bounds; // Difference between the number of areas in a row and the number of cells in a row model //--- If the model has more cells than areas in the list, create the missing areas and cells at the end of the lists if(diff>0) { //--- In a loop by the number of missing areas for(int i=total_bounds;i<total_bounds+diff;i++) { //--- create and add the number of cell areas of the row to the diff list. //--- Get the model of the next cell, CTableCell *cell_model=this.m_table_row_model.GetCell(i); if(cell_model==NULL) return false; //--- calculate the coordinate and create a name for the cell area int x=cell_w*i; string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i; //--- Create a new cell area CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height()); if(cell_bound==NULL) return false; //--- Create a new cell visual representation object CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height()); if(cell_view==NULL) return false; } } //--- If there are more areas in the list than cells in the model, remove the extra areas at the end of the list if(diff<0) { int start=total_bounds-1; int end=start-diff; bool res=true; for(int i=start;i>end;i--) { if(!this.BoundCellDelete(i)) return false; } } //--- In the loop by the number of cells in the row model for(int i=0;i<total_model;i++) { //--- get the model of the next cell, CTableCell *cell_model=this.m_table_row_model.GetCell(i); if(cell_model==NULL) return false; //--- calculate the cell coordinate int x=cell_w*i; //--- Get the next cell area CBound *cell_bound=this.GetBoundAt(i); if(cell_bound==NULL) return false; //--- Get the cell visual representation object from the list CTableCellView *cell_view=this.m_list_cells.GetNodeAtIndex(i); if(cell_view==NULL) return false; //--- Assign the corresponding visual representation object of the cell and its text to the current cell area cell_bound.AssignObject(cell_view); cell_view.SetText(cell_model.Value()); } //--- All is successful return true; }
The method's logic is explained in the comments to the code. If columns have been added or deleted in the table model, then the required number of cells is added or deleted in the visual representation of the row. Then, in a loop based on the number of row cells in the table model, assign the corresponding visual representation object of the cell to the cell area.
A Method That Deletes the Specified Row Area and a Cell With the Corresponding Index:
//+------------------------------------------------------------------+ //| CTableRowView::Delete the specified row region | //| and the cell with the corresponding index | //+------------------------------------------------------------------+ bool CTableRowView::BoundCellDelete(const int index) { if(!this.m_list_cells.Delete(index)) return false; return this.m_list_bounds.Delete(index); }
If the cell object is successfully deleted from the list, also delete the corresponding area from the list of areas.
In the method that draws the row view, check whether the row goes beyond the container:
//+------------------------------------------------------------------+ //| CTableRowView::Draw the appearance | //+------------------------------------------------------------------+ void CTableRowView::Draw(const bool chart_redraw) { //--- If the row is outside the container, leave if(this.IsOutOfContainer()) return; //--- Fill the object with the background color, draw the row line and update the background canvas this.Fill(this.BackColor(),false); this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG())); //--- Draw the row cells int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Get the area of the next cell CBound *cell_bound=this.GetBoundAt(i); if(cell_bound==NULL) continue; //--- Get the attached cell object from the cell area CTableCellView *cell_view=cell_bound.GetAssignedObj(); //--- Draw a visual representation of the cell if(cell_view!=NULL) cell_view.Draw(false); } //--- Update the background and foreground canvases with the specified chart redraw flag this.Update(chart_redraw); }
A row that is not in the container’s visible area should not be drawn.
When changing the column width, you should change the width of all the cells corresponding to the column and shift the adjacent cells to new coordinates. To do this, implement a new method.
A Method That Recalculates Cell Areas:
//+------------------------------------------------------------------+ //| CTableRowView::Recalculate cell areas | //+------------------------------------------------------------------+ bool CTableRowView::RecalculateBounds(CListElm *list_bounds) { //--- Check the list if(list_bounds==NULL) return false; //--- In the loop by the number of areas in the list for(int i=0;i<list_bounds.Total();i++) { //--- get the next header area and the corresponding cell area CBound *capt_bound=list_bounds.GetNodeAtIndex(i); CBound *cell_bound=this.GetBoundAt(i); if(capt_bound==NULL || cell_bound==NULL) return false; //--- Set the coordinate and size of the header area in the cell area cell_bound.SetX(capt_bound.X()); cell_bound.ResizeW(capt_bound.Width()); //--- Get the attached cell object from the cell area CTableCellView *cell_view=cell_bound.GetAssignedObj(); if(cell_view==NULL) return false; //--- Set the coordinate and size of the cell area in the cell's visual representation object cell_view.BoundSetX(cell_bound.X()); cell_view.BoundResizeW(cell_bound.Width()); } //--- All is successful return true; }
A list of column header areas is passed to the method. Regarding properties of column areas from the passed list, areas of table cells change — their sizes and coordinates. Both the new dimensions and coordinates of the areas are set to their corresponding cell objects.
In the MVC paradigm, the table header is not just a table part, but is a separate control using which you can effect and control the view of table columns. The column header is also a control element in this context. In the context of this development, the column header allows changing the column size, setting the same properties for all column cells, etc.
Thus, the column header class and the table header class have accumulated the main improvements that make it possible to "revive" the table.
Refine the visual representation class of the table column header CColumnCaptionView.
The column header, as a control, will control the size (width) of the column and the sorting direction, or absence thereof. When clicking on the header, an arrow will appear on it indicating the sorting direction. If there is no sorting (if you click on another header), the arrow does not appear. If you hover the cursor over the header’s right bound, the cursor will have an tooltip arrow indicating the shift direction. Whereas, if you hold down the mouse button and drag the edge, the column width will change, and the position of columns on the right will be adjusted — they will shift, following the right edge of the column being resized.
Declare new variables and methods:
//+------------------------------------------------------------------+ //| Class for visual representation of table column header | //+------------------------------------------------------------------+ class CColumnCaptionView : public CButton { protected: CColumnCaption *m_column_caption_model; // Pointer to the column header model CBound *m_bound_node; // Pointer to the header area int m_index; // Index in the column list ENUM_TABLE_SORT_MODE m_sort_mode; // Table column sorting mode //--- Add hint objects with arrows to the list virtual bool AddHintsArrowed(void); //--- Displays the resize cursor virtual bool ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y); public: //--- Set the ID virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Set and (2) return the cell index void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Assign and (2) return the area of the header the object is assigned to void AssignBoundNode(CBound *bound) { this.m_bound_node=bound; } CBound *GetBoundNode(void) { return this.m_bound_node; } //--- (1) Assign and (2) return the column header model bool ColumnCaptionModelAssign(CColumnCaption *caption_model); CColumnCaption *ColumnCaptionModel(void) { return this.m_column_caption_model; } //--- Print the assigned column header model in the journal void ColumnCaptionModelPrint(void); //--- (1) Set and (2) return the sorting mode void SetSortMode(const ENUM_TABLE_SORT_MODE mode) { this.m_sort_mode=mode; } ENUM_TABLE_SORT_MODE SortMode(void) const { return this.m_sort_mode; } //--- Set the reverse sorting direction void SetSortModeReverse(void); //--- Draw (1) the appearance and (2) sorting direction arrow virtual void Draw(const bool chart_redraw); protected: void DrawSortModeArrow(void); public: //--- Handler for resizing an element by the right side virtual bool ResizeZoneRightHandler(const int x, const int y); //--- Handlers for resizing the element by sides and corners virtual bool ResizeZoneLeftHandler(const int x, const int y) { return false; } virtual bool ResizeZoneTopHandler(const int x, const int y) { return false; } virtual bool ResizeZoneBottomHandler(const int x, const int y) { return false; } virtual bool ResizeZoneLeftTopHandler(const int x, const int y) { return false; } virtual bool ResizeZoneRightTopHandler(const int x, const int y) { return false; } virtual bool ResizeZoneLeftBottomHandler(const int x, const int y) { return false; } virtual bool ResizeZoneRightBottomHandler(const int x, const int y){ return false; } //--- Change the object width virtual bool ResizeW(const int w); //--- Mouse button click event handler (Press) virtual void OnPressEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0)const { return CButton::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW);} //--- Initialize (1) the class object and (2) default object colors void Init(const string text); virtual void InitColors(void); //--- Return the object description virtual string Description(void); //--- Constructors/destructor CColumnCaptionView(void); CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CColumnCaptionView (void){} };
Discuss improvements to already written class methods and implementation of new ones.
In the constructor, set the default sorting as missing:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Default constructor. Builds an object | //| in the main window of the current chart at coordinates 0,0 | //| with default dimensions | //+------------------------------------------------------------------+ CColumnCaptionView::CColumnCaptionView(void) : CButton("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE) { //--- Initialization this.Init("Caption"); this.SetID(0); this.SetName("ColumnCaption"); } //+------------------------------------------------------------------+ //| CColumnCaptionView::Parametric constructor. | //| Plot an object in the specified window of the specified plot with| //| the specified text, coordinates and dimensions | //+------------------------------------------------------------------+ CColumnCaptionView::CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) : CButton(object_name,text,chart_id,wnd,x,y,w,h), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE) { //--- Initialization this.Init(text); this.SetID(0); }
In the initialization method, set a feature of resizing, impossibility of moving an object, and set a display area for the sorting direction arrow:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Initialization | //+------------------------------------------------------------------+ void CColumnCaptionView::Init(const string text) { //--- Default text offsets this.m_text_x=4; this.m_text_y=2; //--- Set the colors of different states this.InitColors(); //--- It is possible to resize this.SetResizable(true); this.SetMovable(false); this.SetImageBound(this.ObjectWidth()-14,4,8,11); }
In the method for drawing the view, check that the object is not outside the container and call the method for drawing the sorting arrow:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Draw the appearance | //+------------------------------------------------------------------+ void CColumnCaptionView::Draw(const bool chart_redraw) { //--- If the object is outside its container, leave if(this.IsOutOfContainer()) return; //--- Fill the object with the background color, draw the light vertical line on the left and the dark one on the right this.Fill(this.BackColor(),false); color clr_dark =this.BorderColor(); // "Dark color" color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100); // "Light color" this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG())); // Line on the left this.m_background.Line(this.AdjX(this.Width()-1),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(clr_dark,this.AlphaBG())); // Line on the right //--- Update the background canvas this.m_background.Update(false); //--- Display the header text CLabel::Draw(false); //--- Draw sorting direction arrows this.DrawSortModeArrow(); //--- If specified, update the chart if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
A Method That Draws a Sorting Direction Arrow:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Draw the sorting direction arrow | //+------------------------------------------------------------------+ void CColumnCaptionView::DrawSortModeArrow(void) { //--- Set the arrow color for the normal and disabled object states color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor()); switch(this.m_sort_mode) { //--- Sort ascending case TABLE_SORT_MODE_ASC : //--- Clear the drawing area and draw the down arrow this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); this.m_painter.ArrowDown(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true); break; //--- Sort descending case TABLE_SORT_MODE_DESC : //--- Clear the drawing area and draw the up arrow this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); this.m_painter.ArrowUp(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true); break; //--- No sorting default : //--- Clear the drawing area this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); break; } }
Depending on the sorting direction, draw the corresponding arrow:
- in ascending order — down arrow,
- in descending order — up arrow,
- no sorting — we just clear the arrow drawing.
A Method That Reverses the Sorting Direction:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Reverse the sorting direction | //+------------------------------------------------------------------+ void CColumnCaptionView::SetSortModeReverse(void) { switch(this.m_sort_mode) { case TABLE_SORT_MODE_ASC : this.m_sort_mode=TABLE_SORT_MODE_DESC; break; case TABLE_SORT_MODE_DESC : this.m_sort_mode=TABLE_SORT_MODE_ASC; break; default : break; } }
If the current sorting is set in ascending order, then set it in descending order, and vice versa. Set the missing sorting in another method, since this method is intended only for switching the sorting when clicking on the column header.
A Method That Adds Arrow Hint Objects To the List:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Add hint objects | //| with arrows to the list | //+------------------------------------------------------------------+ bool CColumnCaptionView::AddHintsArrowed(void) { //--- Create a hint for the horizontal offset arrow CVisualHint *hint=this.CreateAndAddNewHint(HINT_TYPE_ARROW_SHIFT_HORZ,DEF_HINT_NAME_SHIFT_HORZ,18,18); if(hint==NULL) return false; //--- Set the size of the hint image area hint.SetImageBound(0,0,hint.Width(),hint.Height()); //--- hide the hint and draw the appearance hint.Hide(false); hint.Draw(false); //--- All is successful return true; }
A new tooltip is created and added to the list. Then you can get it from the list by name.
A Method That Displays the Resizing Cursor:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Display the resize cursor | //+------------------------------------------------------------------+ bool CColumnCaptionView::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y) { CVisualHint *hint=NULL; // Pointer to the hint int hint_shift_x=0; // Hint offset by X int hint_shift_y=0; // Hint offset by Y //--- Depending on the location of the cursor on the element borders //--- specify the tooltip offsets relative to the cursor coordinates, //--- display the required hint on the chart and get the pointer to this object if(edge!=CURSOR_REGION_RIGHT) return false; hint_shift_x=-8; hint_shift_y=-12; this.ShowHintArrowed(HINT_TYPE_ARROW_SHIFT_HORZ,x+hint_shift_x,y+hint_shift_y); hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ); //--- Return the result of adjusting the position of the tooltip relative to the cursor return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false); }
This method only works when the cursor is over the right edge of the element. It displays the tooltip and moves it to the cursor position with the set shift.
A Handler For Resizing With the Right Edge:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Right edge resize handler | //+------------------------------------------------------------------+ bool CColumnCaptionView::ResizeZoneRightHandler(const int x,const int y) { //--- Calculate and set the new element width int width=::fmax(x-this.X()+1,DEF_TABLE_COLUMN_MIN_W); if(!this.ResizeW(width)) return false; //--- Get the pointer to the hint CVisualHint *hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ); if(hint==NULL) return false; //--- Shift the hint by the specified values relative to the cursor int shift_x=-8; int shift_y=-12; CTableHeaderView *header=this.m_container; if(header==NULL) return false; bool res=header.RecalculateBounds(this.GetBoundNode(),this.Width()); res &=hint.Move(x+shift_x,y+shift_y); if(res) ::ChartRedraw(this.m_chart_id); return res; }
When dragging the right edge of an element, it should change its width depending on cursor shift direction. First, the width of the element changes, then a tooltip is displayed, and the column header area recalculate method RecalculateBounds() of the table header is called. This method sets a new size for the area of the modified header and shifts all adjacent column header areas to new coordinates.
A Virtual Method That Changes the Object Width:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Change the object width | //+------------------------------------------------------------------+ bool CColumnCaptionView::ResizeW(const int w) { if(!CCanvasBase::ResizeW(w)) return false; //--- Clear the drawing area in the previous location this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Set a new drawing area this.SetImageBound(this.Width()-14,4,8,11); return true; }
When resizing the column header, it is necessary to shift the drawing area (the arrows of the sorting direction) so that the arrow is drawn in the right place near the right edge of the element.
Mouse Button Click Event Handler:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Mouse button click event handler | //+------------------------------------------------------------------+ void CColumnCaptionView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam) { //--- If the mouse button is released in the drag area of the right edge of the element, leave if(this.ResizeRegion()==CURSOR_REGION_RIGHT) return; //--- Change the sort direction arrow to the opposite and call the mouse click handler this.SetSortModeReverse(); CCanvasBase::OnPressEvent(id,lparam,dparam,sparam); }
If a click event is caused by releasing the mouse button in the dragging area, it means that the action to resize the header has just been completed — we are leaving. Next, we change the arrow of sorting direction to a reverse one and call the event handler for mouse buttons of the base object.
In the methods of working with files, save and load the value of sorting direction:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Save to file | //+------------------------------------------------------------------+ bool CColumnCaptionView::Save(const int file_handle) { //--- Save the parent object data if(!CButton::Save(file_handle)) return false; //--- Save the header index if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE) return false; //--- Save the sorting direction if(::FileWriteInteger(file_handle,this.m_sort_mode,INT_VALUE)!=INT_VALUE) return false; //--- All is successful return true; } //+------------------------------------------------------------------+ //| CColumnCaptionView::Load from file | //+------------------------------------------------------------------+ bool CColumnCaptionView::Load(const int file_handle) { //--- Load parent object data if(!CButton::Load(file_handle)) return false; //--- Load the header index this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE); //--- Load the sorting direction this.m_id=this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE); //--- All is successful return true; }
Now, finalize the visual representation class of the table header CTableHeaderView.
An object of this class contains a list of column headers that this object manages, giving the user control over the view of table columns and sorting by any of them.
Declare new class methods:
//+------------------------------------------------------------------+ //| Class for visual representation of table header | //+------------------------------------------------------------------+ class CTableHeaderView : public CPanel { protected: CColumnCaptionView m_temp_caption; // Temporary column header object for searching CTableHeader *m_table_header_model; // Pointer to the table header model //--- Create and add a new column header representation object to the list CColumnCaptionView *InsertNewColumnCaptionView(const string text, const int x, const int y, const int w, const int h); public: //--- (1) Set and (2) return the table header model bool TableHeaderModelAssign(CTableHeader *header_model); CTableHeader *GetTableHeaderModel(void) { return this.m_table_header_model; } //--- Recalculate the header areas bool RecalculateBounds(CBound *bound,int new_width); //--- Print the assigned table header model in the journal void TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); //--- Draw the appearance virtual void Draw(const bool chart_redraw); //--- Set the sorting flag for the column header void SetSortedColumnCaption(const uint index); //--- Get the column header (1) by index and (2) with sorting flag CColumnCaptionView *GetColumnCaption(const uint index); CColumnCaptionView *GetSortedColumnCaption(void); //--- Return the column header index with the sorting flag int IndexSortedColumnCaption(void); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode); } virtual bool Save(const int file_handle) { return CPanel::Save(file_handle); } virtual bool Load(const int file_handle) { return CPanel::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_HEADER_VIEW); } //--- Handler for the element user event when clicking on the object area virtual void MousePressHandler(const int id, const long lparam, const double dparam, const string sparam); //--- Initialize (1) the class object and (2) default object colors void Init(void); virtual void InitColors(void); //--- Constructors/destructor CTableHeaderView(void); CTableHeaderView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableHeaderView (void){} };
Consider improvements to the existing methods and implementation of declared ones.
In the method that sets the header model, we will calculate the width of headers subject to the minimum header width, set the header identifier correctly, set the area corresponding to it in the header, and set the ascending sorting flag for the very first header:
//+------------------------------------------------------------------+ //| CTableHeaderView::Set the header model | //+------------------------------------------------------------------+ bool CTableHeaderView::TableHeaderModelAssign(CTableHeader *header_model) { //--- If an empty object is passed, report this and return 'false' if(header_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- If the passed header model does not contain any column headers, report this and return 'false' int total=(int)header_model.ColumnsTotal(); if(total==0) { ::PrintFormat("%s: Error. Header model does not contain any columns",__FUNCTION__); return false; } //--- Store the pointer to the passed table header model and calculate the width of each column header this.m_table_header_model=header_model; int caption_w=(int)::fmax(::round((double)this.Width()/(double)total),DEF_TABLE_COLUMN_MIN_W); //--- In the loop by the number of column headers in the table header model for(int i=0;i<total;i++) { //--- get the model of the next column header, CColumnCaption *caption_model=this.m_table_header_model.GetColumnCaption(i); if(caption_model==NULL) return false; //--- calculate the coordinate and create a name for the column header area int x=caption_w*i; string name="CaptionBound"+(string)i; //--- Create a new column header area CBound *caption_bound=this.InsertNewBound(name,x,0,caption_w,this.Height()); if(caption_bound==NULL) return false; caption_bound.SetID(i); //--- Create a new column header visual representation object CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height()); if(caption_view==NULL) return false; //--- Assign the corresponding visual representation object of the column header to the current column header area caption_bound.AssignObject(caption_view); caption_view.AssignBoundNode(caption_bound); //--- For the very first header, set the sorting flag in ascending order if(i==0) caption_view.SetSortMode(TABLE_SORT_MODE_ASC); } //--- All is successful return true; }
A Method That Recalculates Header Areas:
//+------------------------------------------------------------------+ //| CTableHeaderView::Recalculate the header areas | //+------------------------------------------------------------------+ bool CTableHeaderView::RecalculateBounds(CBound *bound,int new_width) { //--- If an empty area object is passed or its width has not changed, return 'false' if(bound==NULL || bound.Width()==new_width) return false; //--- Get the area index in the list int index=this.m_list_bounds.IndexOf(bound); if(index==WRONG_VALUE) return false; //--- Calculate the offset and, if it is absent, return 'false' int delta=new_width-bound.Width(); if(delta==0) return false; //--- Change the width of the current area and the object assigned to it bound.ResizeW(new_width); CElementBase *assigned_obj=bound.GetAssignedObj(); if(assigned_obj!=NULL) assigned_obj.ResizeW(new_width); //--- Get the next area after the current one CBound *next_bound=this.m_list_bounds.GetNextNode(); //--- Recalculate the X coordinates for all subsequent areas while(!::IsStopped() && next_bound!=NULL) { //--- Shift the region by delta int new_x = next_bound.X()+delta; int prev_width=next_bound.Width(); next_bound.SetX(new_x); next_bound.Resize(prev_width,next_bound.Height()); //--- If there is an assigned object in the area, update its position CElementBase *assigned_obj=next_bound.GetAssignedObj(); if(assigned_obj!=NULL) { assigned_obj.Move(assigned_obj.X()+delta,assigned_obj.Y()); //--- This block of code is part of the effort to find and fix artifacts when dragging headers CCanvasBase *base_obj=assigned_obj.GetContainer(); if(base_obj!=NULL) { if(assigned_obj.X()>base_obj.ContainerLimitRight()) assigned_obj.Hide(false); else assigned_obj.Show(false); } } //--- Move on to the next area next_bound=this.m_list_bounds.GetNextNode(); } //--- Calculate the new width of the table header based on the width of the column headers int header_width=0; for(int i=0;i<this.m_list_bounds.Total();i++) { CBound *bound=this.GetBoundAt(i); if(bound!=NULL) header_width+=bound.Width(); } //--- If the calculated width of the table header differs from the current one, change the width if(header_width!=this.Width()) { if(!this.ResizeW(header_width)) return false; } //--- Get the pointer to the table object (View) CTableView *table_view=this.GetContainer(); if(table_view==NULL) return false; //--- Get a pointer to the panel with table rows from the table object CPanel *table_area=table_view.GetTableArea(); if(table_area==NULL) return false; //--- Resize the table row panel to fit the overall size of the column headers if(!table_area.ResizeW(header_width)) return false; //--- Get a list of table rows and loop through all the rows CListElm *list=table_area.GetListAttachedElements(); int total=list.Total(); for(int i=0;i<total;i++) { //--- Get the next table row CTableRowView *row=table_area.GetAttachedElementAt(i); if(row!=NULL) { //--- Change the row size to fit the panel size and recalculate the cell areas row.ResizeW(table_area.Width()); row.RecalculateBounds(&this.m_list_bounds); } } //--- Redraw all table rows table_area.Draw(false); return true; }
The method's logic is explained in the comments. A pointer to the changed header area and its new width is passed to the method. The size of the area and the header assigned to it change, and starting from the next area in the list, each subsequent area is shifted to a new coordinate along with the headers assigned to them. Upon completion of the shift of all areas, a new size of the table header is calculated based on the widths of all the column header areas included in it. Subsequently, this new size affects the width of the table’s horizontal scrollbar. According to the new size of the table header, the width of the table row panel is set, all rows are resized to the new panel size, and all cell areas are recalculated for them according to the list of table header areas. As a result, the table is redrawn (the part of it visible in the container).
A Method That Sets a Sort Flag To the Column Header:
//+------------------------------------------------------------------+ //| CTableHeaderView::Set the sorting flag for the column header | //+------------------------------------------------------------------+ void CTableHeaderView::SetSortedColumnCaption(const uint index) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Get the area of the next column header and //--- get the attached column header object from it CColumnCaptionView *caption_view=this.GetColumnCaption(i); if(caption_view==NULL) continue; //--- If the loop index is equal to the required index, set the ascending sort flag if(i==index) { caption_view.SetSortMode(TABLE_SORT_MODE_ASC); caption_view.Draw(false); } //--- Otherwise, reset the sorting flag else { caption_view.SetSortMode(TABLE_SORT_MODE_NONE); caption_view.Draw(false); } } this.Draw(true); }
In the loop, we get the header object from the list of column header areas. If this is a header you are looking for by index, set the ascending sort flag for it, otherwise, uncheck the sort flag. Thus, the sort flag will be unchecked for all column headers, and ascending sorting will be set for the one indicated by index. In other words, when clicking on a column header, it will be sorted in ascending order. When clicking on the same header again, it will be set to sort in descending order, but yet in the handler of click event on the element.
A Method That Returns a Column Header By Index:
//+------------------------------------------------------------------+ //| CTableHeaderView::Get the column header by index | //+------------------------------------------------------------------+ CColumnCaptionView *CTableHeaderView::GetColumnCaption(const uint index) { //--- Get the column header area by index CBound *capt_bound=this.GetBoundAt(index); if(capt_bound==NULL) return NULL; //--- Return a pointer to the attached column header object from the column header area return capt_bound.GetAssignedObj(); }
A Method That Returns a Column Header With the Sort Flag:
//+------------------------------------------------------------------+ //| CTableHeaderView::Get the column header with the sorting flag | //+------------------------------------------------------------------+ CColumnCaptionView *CTableHeaderView::GetSortedColumnCaption(void) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Get the area of the next column header and //--- get the attached column header object from it CColumnCaptionView *caption_view=this.GetColumnCaption(i); //--- If the object is received and it has the sorting flag set, return the pointer to it if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE) return caption_view; } return NULL; }
A Method That Returns Index Of a Sorted Column:
//+------------------------------------------------------------------+ //| CTableHeaderView::Return the index of the sorted column | //+------------------------------------------------------------------+ int CTableHeaderView::IndexSortedColumnCaption(void) { int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Get the area of the next column header and //--- get the attached column header object from it CColumnCaptionView *caption_view=this.GetColumnCaption(i); //--- If the object is received and it has the sorting flag set, return the area index if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE) return i; } return WRONG_VALUE; }
The three above methods, in a simple loop search for a column header with the desired flag, or by the desired index, and return the data which correspond to the method.
The handler of a custom element event when clicking in the object area:
//+------------------------------------------------------------------+ //| CTableHeaderView::Element custom event handler | //| when clicking on an object area | //+------------------------------------------------------------------+ void CTableHeaderView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam) { //--- Get the name of the table header object from sparam int len=::StringLen(this.NameFG()); string header_str=::StringSubstr(sparam,0,len); //--- If the retrieved name does not match the name of this object, it is not our event, we leave if(header_str!=this.NameFG()) return; //--- Find the column header index in sparam string capt_str=::StringSubstr(sparam,len+1); string index_str=::StringSubstr(capt_str,5,capt_str.Length()-7); //--- Failed to find the index in the row - leave if(index_str=="") return; //--- Set the column header index int index=(int)::StringToInteger(index_str); //--- Get the column header by index CColumnCaptionView *caption=this.GetColumnCaption(index); if(caption==NULL) return; //--- If the header does not have a sorting flag, set the ascending sorting flag if(caption.SortMode()==TABLE_SORT_MODE_NONE) { this.SetSortedColumnCaption(index); } //--- Send a custom event to the chart with the header index in lparam, the sorting mode in dparam, and the object name in sparam //--- Since the standard OBJECT_CLICK event passes cursor coordinates in lparam and dparam, we will pass negative values here ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, -(1000+index), -(1000+caption.SortMode()), this.NameFG()); ::ChartRedraw(this.m_chart_id); }
The method sends a custom event to enable sorting by column. Since cursor coordinates are passed to lparam and dparam in the event of clicking on an object, in order to understand that this is an event for activating sorting, we will pass a negative value to lparam and dparam :
- To lparam (-1000) + column index,
- To dparam (-1000) + sorting type.
Finalize the table visual representation class CTableView.
The class is a full-fledged "Table" control. Improvements will include enabling functionality to change column width and sort by selected column in the table.
Declare new class methods:
//+------------------------------------------------------------------+ //| Table visual representation class | //+------------------------------------------------------------------+ class CTableView : public CPanel { protected: //--- Obtained table data CTable *m_table_obj; // Pointer to table object (includes table and header models) CTableModel *m_table_model; // Pointer to the table model (obtained from CTable) CTableHeader *m_header_model; // Pointer to the table header model (obtained from CTable) //--- View component data CTableHeaderView *m_header_view; // Pointer to the table header (View) CPanel *m_table_area; // Panel for placing table rows CContainer *m_table_area_container; // Container for placing a panel with table rows //--- (1) Set and (2) return the table model bool TableModelAssign(CTableModel *table_model); CTableModel *GetTableModel(void) { return this.m_table_model; } //--- (1) Set and (2) return the table header model bool HeaderModelAssign(CTableHeader *header_model); CTableHeader *GetHeaderModel(void) { return this.m_header_model; } //--- Create a (1) header, (2) a table object and update the modified table from the model bool CreateHeader(void); bool CreateTable(void); bool UpdateTable(void); public: //--- (1) Set and (2) return the table object bool TableObjectAssign(CTable *table_obj); CTable *GetTableObj(void) { return this.m_table_obj; } //--- Return (1) the header, (2) the table layout area and (3) the table area container CTableHeaderView *GetHeader(void) { return this.m_header_view; } CPanel *GetTableArea(void) { return this.m_table_area; } CContainer *GetTableAreaContainer(void) { return this.m_table_area_container; } //--- Print the assigned (1) table, (2) table header and (3) table object model in the journal void TableModelPrint(const bool detail); void HeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); void TablePrint(const int column_width=CELL_WIDTH_IN_CHARS); //--- Get the column header (1) by index and (2) with sorting flag CColumnCaptionView *GetColumnCaption(const uint index) { return(this.GetHeader()!=NULL ? this.GetHeader().GetColumnCaption(index) : NULL); } CColumnCaptionView *GetSortedColumnCaption(void) { return(this.GetHeader()!=NULL ? this.GetHeader().GetSortedColumnCaption(): NULL); } //--- Returns the visual representation object of the specified (1) row and (2) cell CTableRowView *GetRowView(const uint index) { return(this.GetTableArea()!=NULL ? this.GetTableArea().GetAttachedElementAt(index) : NULL); } CTableCellView *GetCellView(const uint row,const uint col) { return(this.GetRowView(row)!=NULL ? this.GetRowView(row).GetCellView(col) : NULL); } //--- Return the number of table rows int RowsTotal(void) { return(this.GetTableArea()!=NULL ? this.GetTableArea().AttachedElementsTotal() : 0); } //--- Draw the appearance virtual void Draw(const bool chart_redraw); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_VIEW); } //--- Handler for the element user event when clicking on the object area virtual void MousePressHandler(const int id, const long lparam, const double dparam, const string sparam); //--- Sort the table by column value and direction bool Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode); //--- Initialize (1) the class object and (2) default object colors void Init(void); //--- Constructors/destructor CTableView(void); CTableView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableView (void){} };
Consider the declared methods.
A Method That Updates the Modified Table:
//+------------------------------------------------------------------+ //| CTableView::Updates the modified table | //+------------------------------------------------------------------+ bool CTableView::UpdateTable(void) { if(this.m_table_area==NULL) return false; int total_model=(int)this.m_table_model.RowsTotal(); // Number of rows in the model int total_view =this.m_table_area.AttachedElementsTotal(); // Number of rows in the visual representation int diff=total_model-total_view; // Difference in the number of rows of two components int y=1; // Vertical offset int table_height=0; // Calculated panel height CTableRowView *row=NULL; // Pointer to the visual representation of the row //--- If there are more rows in the model than in the visual representation, create the missing rows in the visual representation at the end of the list if(diff>0) { //--- Get the last row of the visual representation of the table (added rows will be placed based on its coordinates) row=this.m_table_area.GetAttachedElementAt(total_view-1); //--- In the loop by the number of missing rows for(int i=total_view;i<total_view+diff;i++) { //--- create and attach the number of objects of visual representation of the table row to the diff panel row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW_VIEW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H); if(row==NULL) return false; } } //--- If there are more rows in the visual representation than in the model, remove the extra rows in the visual representation at the end of the list if(diff<0) { CListElm *list=this.m_table_area.GetListAttachedElements(); if(list==NULL) return false; int start=total_view-1; int end=start-diff; bool res=true; for(int i=start;i>end;i--) res &=list.Delete(i); if(!res) return false; } //--- In the loop through the list of rows of the table model for(int i=0;i<total_model;i++) { //--- get the next object of visual representation of the table row from the list of the rows panel row=this.m_table_area.GetAttachedElementAt(i); if(row==NULL) return false; //--- Check the object type if(row.Type()!=ELEMENT_TYPE_TABLE_ROW_VIEW) continue; //--- Set the row ID row.SetID(i); //--- Set the row background color depending on its index (even/odd) if(row.ID()%2==0) row.InitBackColorDefault(clrWhite); else row.InitBackColorDefault(C'242,242,242'); row.BackColorToDefault(); row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4)); //--- Get the row model from the table object CTableRow *row_model=this.m_table_model.GetRow(i); if(row_model==NULL) return false; //--- Update the cells of the table row object using the row model row.TableRowModelUpdate(row_model); //--- Calculate the new panel height value table_height+=row.Height(); } //--- Return the result of changing the panel size to the value calculated in the loop return this.m_table_area.ResizeH(table_height+y); }
The logic of the method is commented in the code. The number of rows in the table model and in the visual representation is compared. If there is a difference, then the missing rows of the table are either added or deleted in the visual representation. Then, in a loop through rows of the table model, cells in the visual representation of the table are updated. After updating all the rows of the visual representation, the result of resizing the table for the new number of rows is returned.
The handler of a custom element event when clicking in the object area:
//+------------------------------------------------------------------+ //| CTableView::Element custom event handler | //| when clicking on an object area | //+------------------------------------------------------------------+ void CTableView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam) { if(id==CHARTEVENT_OBJECT_CLICK && lparam>=0 && dparam>=0) return; //--- Get the name of the table header object from sparam int len=::StringLen(this.NameFG()); string header_str=::StringSubstr(sparam,0,len); //--- If the retrieved name does not match the name of this object, it is not our event, we leave if(header_str!=this.NameFG()) return; //--- Set the column header index //--- Since the standard OBJECT_CLICK event passes cursor coordinates in lparam and dparam, //--- a negative value of the header index (the event occurred for) is passed for the handler int index=(int)::fabs(lparam+1000); //--- Get the column header by index CColumnCaptionView *caption=this.GetColumnCaption(index); if(caption==NULL) return; //--- Sort the list of rows by the sort value in the column header and update the table this.Sort(index,caption.SortMode()); if(this.UpdateTable()) this.Draw(true); }
The method does not process events if lparam and dparam are not less than zero. Next, we get the column index from lparam and sort the table according to the sorting mode specified in the resulting column header. After that the table is updated.
We have finished refining classes. Now, make a new class to create tables from preliminary prepared data quite comfortably. And table management will also be implemented in the same class. Thus, and based on the purpose, it will be a table management class. One object of this class can contain many different tables, which can be accessed by the table ID or by its user name.
Class For Simplified Table Creation
Continue writing the code in \MQL5\Indicators\Tables\Controls\Controls.mqh.
//+------------------------------------------------------------------+ //| Table management class | //+------------------------------------------------------------------+ class CTableControl : public CPanel { protected: CListObj m_list_table_model; //--- Add an object (1) of the model (CTable) and (2) table visual representation (CTableView) to the list bool TableModelAdd(CTable *table_model,const int table_id,const string source); CTableView *TableViewAdd(CTable *table_model,const string source); //--- Update the specified column of the specified table bool ColumnUpdate(const string source, CTable *table_model, const uint table, const uint col, const bool cells_redraw); public: //--- Returns (1) the model, (2) the table visual representation object and (3) the object type CTable *GetTable(const uint index) { return this.m_list_table_model.GetNodeAtIndex(index); } CTableView *GetTableView(const uint index) { return this.GetAttachedElementAt(index); } //--- Create a table based on the passed data template<typename T> CTableView *TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE); CTableView *TableCreate(const uint num_rows, const uint num_columns,const int table_id=WRONG_VALUE); CTableView *TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE); CTableView *TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE); //--- Return (1) the string value of the specified cell (Model), the specified (2) string and (3) the table cell (View) string CellValueAt(const uint table, const uint row, const uint col); CTableRowView *GetRowView(const uint table, const uint index); CTableCellView *GetCellView(const uint table, const uint row, const uint col); //--- Set the (1) value, (2) accuracy, (3) time display flags and (4) color name display flag to the specified cell (Model + View) template<typename T> void CellSetValue(const uint table, const uint row, const uint col, const T value, const bool chart_redraw); void CellSetDigits(const uint table, const uint row, const uint col, const int digits, const bool chart_redraw); void CellSetTimeFlags(const uint table, const uint row, const uint col, const uint flags, const bool chart_redraw); void CellSetColorNamesFlag(const uint table, const uint row, const uint col, const bool flag, const bool chart_redraw); //--- Set the foreground color to the specified cell (View) void CellSetForeColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw); //--- (1) Set and (2) return the text anchor point in the specified cell (View) void CellSetTextAnchor(const uint table, const uint row, const uint col, const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw); ENUM_ANCHOR_POINT CellTextAnchor(const uint table, const uint row, const uint col); //--- Set the (1) accuracy, (2) time display flags, (3) color name display flag, (4) text anchor point and (5) data type in the specified column (View) void ColumnSetDigits(const uint table, const uint col, const int digits, const bool cells_redraw, const bool chart_redraw); void ColumnSetTimeFlags(const uint table, const uint col, const uint flags, const bool cells_redraw, const bool chart_redraw); void ColumnSetColorNamesFlag(const uint table, const uint col, const bool flag, const bool cells_redraw, const bool chart_redraw); void ColumnSetTextAnchor(const uint table, const uint col, const ENUM_ANCHOR_POINT anchor, const bool cells_redraw, const bool chart_redraw); void ColumnSetDatatype(const uint table, const uint col, const ENUM_DATATYPE type, const bool cells_redraw, const bool chart_redraw); //--- Object type virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_CONTROL_VIEW); } //--- Constructors/destructor CTableControl(void) { this.m_list_table_model.Clear(); } CTableControl(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableControl(void) {} };
The class is a Panel on which tables (one or more) are placed, and consists of two lists:
- a list of table models,
- a list of visual representations of tables created based on the corresponding models.
The class methods allow getting the desired table and manage its view, rows, columns, and cells.
Consider the declared class methods.
The list of table models is cleared in the class constructor and the default name is set:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CTableControl::CTableControl(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,"",chart_id,wnd,x,y,w,h) { this.m_list_table_model.Clear(); this.SetName("Table Control"); }
After creating an object, and if more than one object of this class is planned, then you should set your own unique name so that there are no two or more elements with the same name in the list of objects attached to the panel.
A method that adds a table model object to the list:
//+------------------------------------------------------------------+ //| Add the table model (CTable) object to the list | //+------------------------------------------------------------------+ bool CTableControl::TableModelAdd(CTable *table_model,const int table_id,const string source) { //--- Check the table model object if(table_model==NULL) { ::PrintFormat("%s::%s: Error. Failed to create Table Model object",source,__FUNCTION__); return false; } //--- Set the ID to the table model - either by the size of the list or the specified one table_model.SetID(table_id<0 ? this.m_list_table_model.Total() : table_id); //--- If a table model with the specified ID is in the list, report this, delete the object, and return 'false' this.m_list_table_model.Sort(0); if(this.m_list_table_model.Search(table_model)!=NULL) { ::PrintFormat("%s::%s: Error: Table Model object with ID %d already exists in the list",source,__FUNCTION__,table_id); delete table_model; return false; } //--- If the table model is not added to the list, report this, delete the object, and return 'false' if(this.m_list_table_model.Add(table_model)<0) { ::PrintFormat("%s::%s: Error. Failed to add Table Model object to list",source,__FUNCTION__); delete table_model; return false; } //--- All is successful return true; }
A Method That Creates a New Object Of Table Visual Representation And Adds It To the list:
//+------------------------------------------------------------------+ //| Create a new object and add it to the list | //| of the table visual representation (CTableView) | //+------------------------------------------------------------------+ CTableView *CTableControl::TableViewAdd(CTable *table_model,const string source) { //--- Check the table model object if(table_model==NULL) { ::PrintFormat("%s::%s: Error. An invalid Table Model object was passed",source,__FUNCTION__); return NULL; } //--- Create a new element - a visual representation of the table attached to the panel CTableView *table_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_VIEW,"","TableView"+(string)table_model.ID(),1,1,this.Width()-2,this.Height()-2); if(table_view==NULL) { ::PrintFormat("%s::%s: Error. Failed to create Table View object",source,__FUNCTION__); return NULL; } //--- Assign the table object (Model) and its ID to the Table graphical element (Model) table_view.TableObjectAssign(table_model); table_view.SetID(table_model.ID()); return table_view; }
Both of the above methods are used in table creation methods.
A Method That Creates a Table With Specification Of a Tables Array And a Headers Array:
//+-------------------------------------------------------------------+ //| Create a table while specifying a table array and a header array. | //| Defines the index and names of columns according to column_names | //| The number of rows is determined by the size of the row_data array| //| also used to fill the table | //+-------------------------------------------------------------------+ template<typename T> CTableView *CTableControl::TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE) { //--- Create a table object using the specified parameters CTable *table_model=new CTable(row_data,column_names); //--- If there are errors when creating or adding a table to the list, return NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Create and return the table return this.TableViewAdd(table_model,__FUNCTION__); }
A Method That Creates a Table With Definition Of a Number Of Columns And Rows:
//+------------------------------------------------------------------+ //| Create a table with a specified number of columns and rows. | //| The columns will have Excel names "A", "B", "C", etc. | //+------------------------------------------------------------------+ CTableView *CTableControl::TableCreate(const uint num_rows,const uint num_columns,const int table_id=WRONG_VALUE) { CTable *table_model=new CTable(num_rows,num_columns); //--- If there are errors when creating or adding a table to the list, return NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Create and return the table return this.TableViewAdd(table_model,__FUNCTION__); }
A Method That Creates a Table From the Matrix:
//+-------------------------------------------------------------------------------------+ //| Create a table with column initialization according to column_names | //| The number of rows is determined by the row_data parameter, with the 'matrix' type | //+-------------------------------------------------------------------------------------+ CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE) { CTable *table_model=new CTable(row_data,column_names); //--- If there are errors when creating or adding a table to the list, return NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Create and return the table return this.TableViewAdd(table_model,__FUNCTION__); }
A Method That Creates a Table Based On a List Of User Parameters And an Array Of Column Headers:
//+------------------------------------------------------------------+ //| Create a table with the specified table array based on the | //| row_data list containing objects with structure field data. | //| Define the index and names of columns according to | //| column names in column_names | //+------------------------------------------------------------------+ CTableView *CTableControl::TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE) { CTableByParam *table_model=new CTableByParam(row_data,column_names); //--- If there are errors when creating or adding a table to the list, return NULL if(!this.TableModelAdd(table_model,table_id,__FUNCTION__)) return NULL; //--- Create and return the table return this.TableViewAdd(table_model,__FUNCTION__); }
The logic of all the presented methods for creating a table is the same: first, based on method’s input parameters, a table model object is created and added to the list, and then a visual representation. These are the basic methods for creating tables from a wide range of data. The table created by the considered methods is located on the panel, which is the substrate for placing the tables being created.
A Method That Sets Values To the Specified Cell:
//+------------------------------------------------------------------+ //| Set the value to the specified cell (Model + View) | //+------------------------------------------------------------------+ template<typename T> void CTableControl::CellSetValue(const uint table,const uint row,const uint col,const T value,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Get the cell model from the table model CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL) return; //--- Get the cell visual representation object CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- Compare the value set in the cell with the passed one bool equal=false; ENUM_DATATYPE datatype=cell_model.Datatype(); switch(datatype) { case TYPE_LONG : case TYPE_DATETIME: case TYPE_COLOR : equal=(cell_model.ValueL()==value); break; case TYPE_DOUBLE : equal=(::NormalizeDouble(cell_model.ValueD()-value,cell_model.Digits())==0); break; //---TYPE_STRING default : equal=(::StringCompare(cell_model.ValueS(),(string)value)==0); break; } //--- If the values are equal, leave if(equal) return; //--- Set a new value in the cell model; //--- enter the value from the cell model into the cell visual representation object //--- Redraw the cell with the chart update flag table_model.CellSetValue(row,col,value); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
The method's logic is explained in the comments to the code. If a cell has the same value as that passed to the method for entering to the cell, leave the method. A new value is first written to the table model cell, then to a cell of table visual representation, and this cell is redrawn.
A Method That Sets Accuracy Of Displaying Fractional Numbers In the Indicated Cell:
//+------------------------------------------------------------------+ //| Set the accuracy to the specified cell (Model + View) | //+------------------------------------------------------------------+ void CTableControl::CellSetDigits(const uint table,const uint row,const uint col,const int digits,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Get the cell model from the table model CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL || cell_model.Digits()==digits) return; //--- Get the cell visual representation object CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- Set a new accuracy value in the cell model; //--- enter the value from the cell model into the cell visual representation object //--- Redraw the cell with the chart update flag table_model.CellSetDigits(row,col,digits); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
The method is similar to the above. If the specified precision of fractional numbers is equal to the actual one, leave the method. Next, the accuracy is recorded in the cell model, the text from the cell model is recorded in the visual representation (for fractional numbers, the accuracy has already been changed), and the cell is redrawn.
A Method That Sets Time Display Flags To the Specified Cell:
//+------------------------------------------------------------------+ //| Set the time display flags | //| to the specified cell (Model + View) | //+------------------------------------------------------------------+ void CTableControl::CellSetTimeFlags(const uint table,const uint row,const uint col,const uint flags,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Get the cell model from the table model CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL || cell_model.DatetimeFlags()==flags) return; //--- Get the cell visual representation object CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- Set a new value for the time display flags in the cell model; //--- enter the value from the cell model into the cell visual representation object //--- Redraw the cell with the chart update flag table_model.CellSetTimeFlags(row,col,flags); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
The method’s logic is identical to the method that sets accuracy to a cell.
A Method That Sets Color Name Display Flag To the Specified Cell:
//+------------------------------------------------------------------+ //| Set the flag for displaying color names | //| to the specified cell (Model + View) | //+------------------------------------------------------------------+ void CTableControl::CellSetColorNamesFlag(const uint table,const uint row,const uint col,const bool flag,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) return; //--- Get the cell model from the table model CTableCell *cell_model=table_model.GetCell(row,col); if(cell_model==NULL || cell_model.ColorNameFlag()==flag) return; //--- Get the cell visual representation object CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- Set a new value for the color name display flag in the cell model; //--- enter the value from the cell model into the cell visual representation object //--- Redraw the cell with the chart update flag table_model.CellSetColorNamesFlag(row,col,flag); cell_view.SetText(cell_model.Value()); cell_view.Draw(chart_redraw); }
Exactly the same method as the ones above.
A Method That Sets the Foreground Color To a Specified Cell:
//+------------------------------------------------------------------+ //| Set the foreground color to the specified cell (View) | //+------------------------------------------------------------------+ void CTableControl::CellSetForeColor(const uint table,const uint row,const uint col,const color clr,const bool chart_redraw) { //--- Get the cell visual representation object CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- Set the cell background color in the cell visual representation object //--- Redraw the cell with the chart update flag cell_view.SetForeColor(clr); cell_view.Draw(chart_redraw); }
Get the visual representation object of the cell, set a new foreground color for it, and redraw the cell with a new text color.
A Method That Sets the Anchor Point of the Text To the Specified Cell:
//+------------------------------------------------------------------+ //| Set the text anchor point to the specified cell (View) | //+------------------------------------------------------------------+ void CTableControl::CellSetTextAnchor(const uint table,const uint row,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw) { //--- Get the cell visual representation object CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return; //--- Set the text anchor point in the cell visual representation object //--- Redraw the cell with the chart update flag cell_view.SetTextAnchor(anchor,cell_redraw,chart_redraw); }
We get the visual representation object of the cell, set a new anchor point for it, and redraw the cell with a new text layout in it.
A Method That Returns the Anchor Point of the Text In the Specified Cell:
//+------------------------------------------------------------------+ //| Return the text anchor point in the specified cell (View) | //+------------------------------------------------------------------+ ENUM_ANCHOR_POINT CTableControl::CellTextAnchor(const uint table,const uint row,const uint col) { //--- Get the cell visual representation object CTableCellView *cell_view=this.GetCellView(table,row,col); if(cell_view==NULL) return ANCHOR_LEFT_UPPER; //--- Return the text anchor point return((ENUM_ANCHOR_POINT)cell_view.TextAnchor()); }
We get the visual representation object of the cell and return the text anchor point set for it.
A Method That Updates the Specified Column Of the Specified Table:
//+------------------------------------------------------------------+ //| Update the specified column of the specified table | //+------------------------------------------------------------------+ bool CTableControl::ColumnUpdate(const string source,CTable *table_model,const uint table,const uint col,const bool cells_redraw) { //--- Check the table model if(::CheckPointer(table_model)==POINTER_INVALID) { ::PrintFormat("%s::%s: Error. Invalid table model pointer passed",source,__FUNCTION__); return false; } //--- Get the table visual representation CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s::%s: Error. Failed to get CTableView object",source,__FUNCTION__); return false; } //--- In the loop by the rows of the table visual representation int total=table_view.RowsTotal(); for(int i=0;i<total;i++) { //--- get the cell visual representation object in the specified column from the next table row CTableCellView *cell_view=this.GetCellView(table,i,col); if(cell_view==NULL) { ::PrintFormat("%s::%s: Error. Failed to get CTableCellView object (row %d, col %u)",source,__FUNCTION__,i,col); return false; } //--- Get the model of the corresponding cell from the row model CTableCell *cell_model=table_model.GetCell(i,col); if(cell_model==NULL) { ::PrintFormat("%s::%s: Error. Failed to get CTableCell object (row %d, col %u)",source,__FUNCTION__,i,col); return false; } //--- Set the value from the cell model to the cell visual representation object cell_view.SetText(cell_model.Value()); //--- If specified, redraw the cell visual representation if(cells_redraw) cell_view.Draw(false); } return true; }
The method's logic is explained in the comments. After the table model is updated, pass it to this method. And here, in a loop through all the rows of the visual representation of the table, get each new row in turn, and from the corresponding row of the model get the specified cell. We write data from the cell model to the visual representation of the cell, and the visual representation of the cell is redrawn. This way, the entire table column is redrawn.
A Method That Sets the Accuracy In the Specified Column:
//+------------------------------------------------------------------+ //| Set the accuracy in the specified column (Model + View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetDigits(const uint table,const uint col,const int digits,const bool cells_redraw,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Set Digits for the specified column in the table model table_model.ColumnSetDigits(col,digits); //--- Update the display of the column data and, if specified, redraw the chart if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
In the table model, set the specified accuracy for the column and call the table column update method discussed above.
A Method That Sets Time Display Flags To the Specified Column:
//+------------------------------------------------------------------+ //| Set the time display flags | //| in the specified column (Model + View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetTimeFlags(const uint table,const uint col,const uint flags,const bool cells_redraw,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Set the time display flags for the specified column in the table model table_model.ColumnSetTimeFlags(col,flags); //--- Update the display of the column data and, if specified, redraw the chart if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
In the table model, set the specified time display flags for the column and call the update method for the table column.
A Method That Sets Color Name Display Flag To the Specified Column:
//+------------------------------------------------------------------+ //| Set the flag for displaying color names | //| in the specified column (Model + View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetColorNamesFlag(const uint table,const uint col,const bool flag,const bool cells_redraw,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Set the time display flags for the specified column in the table model table_model.ColumnSetColorNamesFlag(col,flag); //--- Update the display of the column data and, if specified, redraw the chart if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
In the table model, set the specified color name display flag for the column and call the update method for the table column.
A Method That Sets a Data Type In the Specified Column:
//+------------------------------------------------------------------+ //| Set the data type of the specified column ( (Model + View)) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetDatatype(const uint table,const uint col,const ENUM_DATATYPE type,const bool cells_redraw,const bool chart_redraw) { //--- Get the table model CTable *table_model=this.GetTable(table); if(table_model==NULL) { ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__); return; } //--- Set the data type for the specified column in the table model table_model.ColumnSetDatatype(col,type); //--- Update the display of the column data and, if specified, redraw the chart if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw) ::ChartRedraw(this.m_chart_id); }
In the table model, set the specified data type for the column and call the update method for the table column.
A Method That Sets an Anchor Point of the Text In the Specified Column:
//+------------------------------------------------------------------+ //| Set the text anchor point in the specified column (View) | //+------------------------------------------------------------------+ void CTableControl::ColumnSetTextAnchor(const uint table,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cells_redraw,const bool chart_redraw) { //--- Get the table visual representation CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__); return; } //--- In a loop through all table rows int total=table_view.RowsTotal(); for(int i=0;i<total;i++) { //--- get the next object of the cell visual representation //--- and set a new anchor point into the object CTableCellView *cell_view=this.GetCellView(table,i,col); if(cell_view!=NULL && cell_view.TextAnchor()!=anchor) cell_view.SetTextAnchor(anchor,cells_redraw,false); } //--- If specified, update the chart if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
In a loop through all the rows of the visual representation of the table, get the specified cell from the next row and enter a new anchor point into it. At the end of the loop update the chart.
A Method That Returns the String Value Of the Specified Cell:
//+------------------------------------------------------------------+ //| Return the string value of the specified cell (Model) | //+------------------------------------------------------------------+ string CTableControl::CellValueAt(const uint table,const uint row,const uint col) { CTable *table_model=this.GetTable(table); return(table_model!=NULL ? table_model.CellValueAt(row,col) : ::StringFormat("%s: Error. Failed to get table model",__FUNCTION__)); }
We get the required cell from the table model and return its value as a row. If an error occurs, the error text is returned.
A Method That Returns the Specified Table Row:
//+------------------------------------------------------------------+ //| Return the specified table row (View) | //+------------------------------------------------------------------+ CTableRowView *CTableControl::GetRowView(const uint table,const uint index) { CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__); return NULL; } return table_view.GetRowView(index); }
We get a visual representation of the table by index and return a pointer to the required row from it.
A Method That Returns the Specified Table Cell:
//+------------------------------------------------------------------+ //| Return the specified table cell (View) | //+------------------------------------------------------------------+ CTableCellView *CTableControl::GetCellView(const uint table,const uint row,const uint col) { CTableView *table_view=this.GetTableView(table); if(table_view==NULL) { ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__); return NULL; } return table_view.GetCellView(row,col); }
We get a visual representation of the table by index and return from it a pointer to the required cell of the specified row.
As you can see, this class is a simple auxiliary class for building tables and working with them. All of its methods represent service functionality for handling, modifying, setting, and retrieving tabular data using methods from previously written model classes and the visual representation of the table.
Let’s check what we have.
Testing the Result
Open test indicator file \MQL5\Indicators\Tables\iTestTable.mq5 and rewrite it as follows (earlier prepared data and creation of the table are highlighted in the appropriate colors):
//+------------------------------------------------------------------+ //| iTestTable.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 0 #property indicator_plots 0 //+------------------------------------------------------------------+ //| Include libraries | //+------------------------------------------------------------------+ #include "Controls\Controls.mqh" // Controls library //--- Pointer to the CTableControl object CTableControl *table_ctrl; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Search for the chart subwindow int wnd=ChartWindowFind(); //--- Create table data //--- Declare and fill the array of column headers with the dimension of 4 string captions[]={"Column 0","Column 1","Column 2","Column 3"}; //--- Declare and fill the 15x4 data array //--- Acceptable array types: double, long, datetime, color, string long array[15][4]={{ 1, 2, 3, 4}, { 5, 6, 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24}, {25, 26, 27, 28}, {29, 30, 31, 32}, {33, 34, 35, 36}, {37, 38, 39, 40}, {41, 42, 43, 44}, {45, 46, 47, 48}, {49, 50, 51, 52}, {53, 54, 55, 56}, {57, 58, 59, 60}}; //--- Create the table control graphical element table_ctrl=new CTableControl("TableControl0",0,wnd,30,30,460,184); if(table_ctrl==NULL) return INIT_FAILED; //--- The chart should have one main element table_ctrl.SetAsMain(); //--- It is possible to set the table control parameters table_ctrl.SetID(0); // ID table_ctrl.SetName("Table Control 0"); // Name //--- Create the table 0 object (Model + View component) from the above-created 15x4 long array and the string array of column headers if(table_ctrl.TableCreate(array,captions)==NULL) return INIT_FAILED; //--- Additionally, set the text output to be centered in the cell for columns 1, 2, 3, and to be left-aligned for column 0 table_ctrl.ColumnSetTextAnchor(0,0,ANCHOR_LEFT,true,false); table_ctrl.ColumnSetTextAnchor(0,1,ANCHOR_CENTER,true,false); table_ctrl.ColumnSetTextAnchor(0,2,ANCHOR_CENTER,true,false); table_ctrl.ColumnSetTextAnchor(0,3,ANCHOR_CENTER,true,false); //--- Draw the table table_ctrl.Draw(true); //--- Get the table with index 0 and print it in the journal CTable *table=table_ctrl.GetTable(0); table.Print(); //--- Successful return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom deindicator initialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Remove the table control and destroy the library's shared resource manager delete table_ctrl; CCommonManager::DestroyInstance(); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- //--- return value of prev_calculated for the next call return(rates_total); } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Call the OnChartEvent handler of the table control table_ctrl.OnChartEvent(id,lparam,dparam,sparam); //--- If the event is mouse cursor movement if(id==CHARTEVENT_MOUSE_MOVE) { //--- get the cursor coordinates int x=table_ctrl.CursorX(); int y=table_ctrl.CursorY(); //--- set the X coordinate value to cell 0 of row 1 table_ctrl.CellSetValue(0,1,0,x,false); //--- set the Y coordinate value to cell 1 of row 1 //--- the text color in the cell depends on the sign of the Y coordinate (if the value is negative, the text is red table_ctrl.CellSetForeColor(0,1,1,(y<0 ? clrRed : table_ctrl.ForeColor()),false); table_ctrl.CellSetValue(0,1,1,y,true); } } //+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void OnTimer(void) { //--- Call the OnTimer handler of the table control table_ctrl.OnTimer(); } //+------------------------------------------------------------------+
In the event handler, check how some data can be written to cells in real time. We will read the cursor coordinates and enter them into the first two cells of the second row from the top. Whereas, the negative value of the cursor’s Y coordinate will be displayed in red.
Compile the indicator and run it on the chart:

As you can see, the stated table - user interaction is working.
Of course, there are some nuances when displaying the visual representation of the table during interaction with the cursor, but all this will gradually be fixed and refined.
Conclusion
At the moment, we have created a convenient tool for displaying various tabular data, which allows us to customize the display of the default table, to some extent.
There are several data options for creating tables based on them:
- a two-dimensional array of the table and an array of column headers,
- a number of columns and rows,
- matrix: the header is automatically created in Excel-style,
- a list of custom parameters CList and an array of column headers.
In the future, it is possible that table classes will be improved to conveniently add and remove rows, columns, and individual cells. But for now, stop on this format of the object for creating and displaying tables.
Programs used in the article:
| # | Name | Type | Description |
|---|---|---|---|
| 1 | Tables.mqh | Class Library | Classes for creating a table model |
| 2 | Base.mqh | Class Library | Classes for creating a base object of controls |
| 3 | Controls.mqh | Class Library | Control classes |
| 4 | iTestTable.mq5 | Test indicator | Indicator for testing manipulations with the TableView control |
| 5 | MQL5.zip | Archive | An archive of the files above for unpacking into the MQL5 directory of the client terminal |
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/19979
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.
Larry Williams Market Secrets (Part 2): Automating a Market Structure Trading System
Reimagining Classic Strategies (Part 20): Modern Stochastic Oscillators
Billiards Optimization Algorithm (BOA)
From Novice to Expert: Navigating Market Irregularities
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
clicking on a table column header (Controller component operation) will cause a change in the data arrangement in the table model (reorganisation of the Model component),