Русский 中文 Español Deutsch 日本語 Português
preview
Making a dashboard to display data in indicators and EAs

Making a dashboard to display data in indicators and EAs

MetaTrader 5Examples | 10 January 2024, 15:24
6 437 6
Artyom Trishkin
Artyom Trishkin

Contents


Introduction

In this article, I will create a dashboard that can display data specified by the developer. Such a panel will be convenient for visually displaying data on a chart and for visual debugging, since looking at the necessary values on the panel is more convenient than tracking them in the debugger. I mean cases when the strategy is being debugged depending on some data values.

I will make the panel in the form of a prototype of the terminal data window and fill it with the same data:


Fig. 1. Data window and dashboard

Our custom panel will allow us to add any required amount of the necessary data to it, sign it, as well as display and update readings from the program code.

It should be possible to move the panel around the chart with the mouse, dock it in the desired chart position, as well as collapse/expand it. For the convenience of placing the data on the panel, it will be possible to display a table with the specified number of rows and columns. The data from this table can be displayed in the journal (X and Y coordinates of each table cell) and obtained programmatically in order to indicate the index of the row and column where this data should be located, or simply print the coordinates and enter the required ones into the code, right when displaying text and data. The first method is more convenient due to its complete automation. The panel will also have an active close button, but we delegate its handling to the control program, since only the program developer should decide how to react to pressing the close button. When a button is clicked, a custom event will be sent to the program event handler. The developer can process it at their own discretion.


Classes for obtaining tabular data

Since it is convenient to arrange the data on the panel according to some visually or virtually predetermined coordinates, we will first create classes for arranging tabular data. The table can be represented as a simple grid, whose line intersections will be the coordinates of the table cells. It will be possible to place any visual data at such coordinates. The table has a certain number of rows (horizontal lines) and each row has a certain number of cells (vertical lines). In a simple grid table, all rows have the same number of cells.

Based on this, we need three classes:

  1. Table cell class,
  2. Table row class,
  3. Table class.

The table cell class includes the row index and column index in the table and the coordinates of the visual location of the table cell in the panel - the X and Y coordinates relative to the table origin in the upper-left corner of the panel.

The table row class includes the table cell class. We can create the required number of cells in one row.

The table class includes a list of table rows. Rows in the table can be created and added in the required quantity.

Let's take a brief look at all three classes.


Table cell class

//+------------------------------------------------------------------+
//| Table cell class                                                 |
//+------------------------------------------------------------------+
class CTableCell : public CObject
  {
private:
   int               m_row;                     // Row
   int               m_col;                     // Column
   int               m_x;                       // X coordinate
   int               m_y;                       // Y coordinate
public:
//--- Methods of setting values
   void              SetRow(const uint row)     { this.m_row=(int)row;  }
   void              SetColumn(const uint col)  { this.m_col=(int)col;  }
   void              SetX(const uint x)         { this.m_x=(int)x;      }
   void              SetY(const uint y)         { this.m_y=(int)y;      }
   void              SetXY(const uint x,const uint y)
                       {
                        this.m_x=(int)x;
                        this.m_y=(int)y;
                       }
//--- Methods of obtaining values
   int               Row(void)            const { return this.m_row;    }
   int               Column(void)         const { return this.m_col;    }
   int               X(void)              const { return this.m_x;      }
   int               Y(void)              const { return this.m_y;      }
//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableCell *compared=node;
                        return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0);
                       }
//--- Constructor/destructor
                     CTableCell(const int row,const int column) : m_row(row),m_col(column){}
                    ~CTableCell(void){}
  };

The class is inherited from the base class for building the MQL5 standard library, since it will fit into CArrayObj lists of the MQL5 Standard Library. The lists can only contain CObject objects or objects inherited from the base CObject.

The function of all variables and methods is quite transparent and easy to understand. The variables are used to store the values of a table row (Row) and column (Column), while the coordinates are the relative coordinates of the upper left corner of the table cell in the panel. Using these coordinates, you can draw something or place it on the panel.

The Compare virtual method is needed to find and compare two table cell objects. The method is declared in the base CObject class:

   //--- method of comparing the objects
   virtual int       Compare(const CObject *node,const int mode=0) const { return(0);      }

returns null and should be overridden in inherited classes.

Since table cells are added to the table row, i.e. visually horizontally, then the search and comparison should be carried out by horizontal cell numbers - by the Column value. This is exactly what the overridden virtual Compare method does here:

//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableCell *compared=node;
                        return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0);
                       }

If the column value of the current object is greater than that of the object being compared (the pointer to which is passed to the method), then 1 is returned. If the column value is less than that of the one being compared, then -1 is returned. Otherwise, return zero. Thus, the zero value returned by the method indicates that the values of the objects being compared are equal.


Table row class

Cell objects will be added to the table row. If the cells in a row are located next to each other horizontally, then the rows in the table are located one below the other vertically.
Here we only need to know the line index and its Y coordinate on the panel:

//+------------------------------------------------------------------+
//| Table row class                                                  |
//+------------------------------------------------------------------+
class CTableRow : public CObject
  {
private:
  CArrayObj          m_list_cell;               // Cell list
  int                m_row;                     // Row index
  int                m_y;                       // Y coordinate
public:
//--- Return the list of table cells in a row
   CArrayObj        *GetListCell(void)       { return &this.m_list_cell;         }
//--- Return (1) the number of table cells in a row (2) the row index in the table
   int               CellsTotal(void)  const { return this.m_list_cell.Total();  }
   int               Row(void)         const { return this.m_row;                }
//--- (1) Set and (2) return the Y row coordinate
   void              SetY(const int y)       { this.m_y=y;                       }
   int               Y(void)           const { return this.m_y;                  }
//--- Add a new table cell to the row
   bool              AddCell(CTableCell *cell)
                       {
                        this.m_list_cell.Sort();
                        if(this.m_list_cell.Search(cell)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        if(!this.m_list_cell.InsertSort(cell))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        return true;
                       }
//--- Return the pointer to the specified cell in the row
   CTableCell       *GetCell(const int column)
                       {
                        const CTableCell *obj=new CTableCell(this.m_row,column);
                        int index=this.m_list_cell.Search(obj);
                        delete obj;
                        return this.m_list_cell.At(index);
                       }
//--- Virtual method for comparing two objects
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CTableRow *compared=node;
                        return(this.Row()>compared.Row() ? 1 : this.Row()<compared.Row() ? -1 : 0);
                       }
//--- Constructor/destructor
                     CTableRow(const int row) : m_row(row)  { this.m_list_cell.Clear();   }
                    ~CTableRow(void)                        { this.m_list_cell.Clear();   }
  };

The CArrayObj list to contain newly added cell objects is declared in the class.

In the Compare virtual method, we compare the objects by the row index value (Row), since when adding a new row we will need to search just by the row index. If no row with such an index is found, the search method (Search) returns -1, otherwise the search returns the index of the found object position in the list. The Search method is declared and implemented in the CArrayObj class:

//+------------------------------------------------------------------+
//| Search of position of element in a sorted array                  |
//+------------------------------------------------------------------+
int CArrayObj::Search(const CObject *element) const
  {
   int pos;
//--- check
   if(m_data_total==0 || !CheckPointer(element) || m_sort_mode==-1)
      return(-1);
//--- search
   pos=QuickSearch(element);
   if(m_data[pos].Compare(element,m_sort_mode)==0)
      return(pos);
//--- not found
   return(-1);
  }

As we can see, it uses the virtual Compare method of comparing two objects to determine the equality of objects.


The method adding a new cell to the list:

//--- Add a new table cell to the row
   bool              AddCell(CTableCell *cell)
                       {
                        this.m_list_cell.Sort();
                        if(this.m_list_cell.Search(cell)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        if(!this.m_list_cell.InsertSort(cell))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column());
                           return false;
                          }
                        return true;
                       }

Since the cells are located in the list strictly one after another according to column numbers (Column) and we add them in sorting order, the list should have the sorted list flag set first. If the search does not return -1, such am object is already present in the list. The appropriate message is sent to the log and false is returned. If failed to add the object pointer to the list, inform of that as well and return false. If all is OK, return true.


The method that returns the pointer to the specified cell in the row:

//--- Return the pointer to the specified cell in the row
   CTableCell       *GetCell(const int column)
                       {
                        const CTableCell *obj=new CTableCell(this.m_row,column);
                        int index=this.m_list_cell.Search(obj);
                        delete obj;
                        return this.m_list_cell.At(index);
                       }

The Search method of the CArrayObj class of the Standard Library searches the list for equality based on the instance of the object whose pointer is passed to the method. Therefore, here we create a new temporary object specifying the column number passed to the method in its constructor and get the object index in the list or -1 if no object with such parameters is found in the list. Also, make sure to delete a temporary object and return the pointer to the found object in the list.
If the object is not found and the index is equal to -1, the At method of the CArrayObj class returns NULL.


Table class

The table consists of a list of rows, which in turn consist of cell lists. In other words, the tabular data class contains only the CArrayObj object the rows to be created, as well as the methods for adding and receiving table rows and cells, are placed into:

//+------------------------------------------------------------------+
//| Table data class                                                 |
//+------------------------------------------------------------------+
class CTableData : public CObject
  {
private:
   CArrayObj         m_list_rows;               // List of rows
public:
//--- Return the list of table rows
   CArrayObj        *GetListRows(void)       { return &this.m_list_rows;   }
//--- Add a new row to the table
   bool              AddRow(CTableRow *row)
                       {
                        //--- Set the sorted list flag
                        this.m_list_rows.Sort();
                        //--- If such an object is already in the list (the search returns the object index, not -1),
                        //--- inform of that in the journal and return 'false'
                        if(this.m_list_rows.Search(row)!=WRONG_VALUE)
                          {
                           ::PrintFormat("%s: Table row with index %lu is already in the list",__FUNCTION__,row.Row());
                           return false;
                          }
                        //--- If failed to add the pointer to the sorted list, inform of that and return 'false'
                        if(!this.m_list_rows.InsertSort(row))
                          {
                           ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,row.Row());
                           return false;
                          }
                        //--- Successful - return 'true'
                        return true;
                       }
//--- Return the pointer to the (1) specified row and (2) specified cell in the specified table row
   CTableRow        *GetRow(const int index) { return this.m_list_rows.At(index);   }
   CTableCell       *GetCell(const int row,const int column)
                       {
                        //--- Get a pointer to a string object in a list of strings
                        CTableRow *row_obj=this.GetRow(row);
                        //--- If failed to get the object, return NULL
                        if(row_obj==NULL)
                           .return NULL;
                        //--- Get the pointer to the cell object in the row by a column number and
                        CTableCell *cell=row_obj.GetCell(column);
                        //--- return the result (object pointer or NULL)
                        return cell;
                       }
//--- Write the X and Y coordinates of the specified table cell into the variables passed to the method
   void              CellXY(const uint row,const uint column, int &x, int &y)
                       {
                        x=WRONG_VALUE;
                        y=WRONG_VALUE;
                        CTableCell *cell=this.GetCell(row,column);
                        if(cell==NULL)
                           return;
                        x=cell.X();
                        y=cell.Y();
                       }
//--- Return the X coordinate of the specified table cell
   int               CellX(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.X() : WRONG_VALUE);
                       }
//--- Return the Y coordinate of the specified table cell
   int               CellY(const uint row,const uint column)
                       {
                        CTableCell *cell=this.GetCell(row,column);
                        return(cell!=NULL ? cell.Y() : WRONG_VALUE);
                       }
//--- Return the number of table (1) rows and (2) columns
   int               RowsTotal(void)            { return this.m_list_rows.Total();  }
   int               ColumnsTotal(void)
                       {
                        //--- If there is no row in the list, return 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Get a pointer to the first row and return the number of cells in it
                        CTableRow *row=this.GetRow(0);
                        return(row!=NULL ? row.CellsTotal() : 0);
                       }
//--- Return the total number of cells in the table
   int               CellsTotal(void){ return this.RowsTotal()*this.ColumnsTotal(); }
//--- Clear lists of rows and table cells
   void              Clear(void)
                       {
                        //--- In the loop by the number of rows in the list of table rows, 
                        for(int i=0;i<this.m_list_rows.Total();i++)
                          {
                           //--- get the pointer to the next row
                           CTableRow *row=this.m_list_rows.At(i);
                           if(row==NULL)
                              continue;
                           //--- get cell list from the obtained row object,
                           CArrayObj *list_cell=row.GetListCell();
                           //--- clear cell list
                           if(list_cell!=NULL)
                              list_cell.Clear();
                          }
                        //--- Clear cell list
                        this.m_list_rows.Clear();
                       }                
//--- Print the table cell data in the journal
   void              Print(const uint indent=0)
                       {
                        //--- Print the header in the journal
                        ::PrintFormat("Table: Rows: %lu, Columns: %lu",this.RowsTotal(),this.ColumnsTotal());
                        //--- In the loop by table rows
                        for(int r=0;r<this.RowsTotal();r++)
                           //--- in the loop by the next row cells,
                           for(int c=0;c<this.ColumnsTotal();c++)
                             {
                              //--- get the pointer to the next cell and display its data in the journal
                              CTableCell *cell=this.GetCell(r,c);
                              if(cell!=NULL)
                                 ::PrintFormat("%*s%-5s %-4lu %-8s %-6lu %-8s %-6lu %-8s %-4lu",indent,"","Row",r,"Column",c,"Cell X:",cell.X(),"Cell Y:",cell.Y());
                             }
                       }
//--- Constructor/destructor
                     CTableData(void)  { this.m_list_rows.Clear();   }
                    ~CTableData(void)  { this.m_list_rows.Clear();   }
  };

Almost all methods here are commented in the code. I will only note the methods that return the number of rows and columns in the table, and the total number of table cells.

The number of rows is a row list size. We obtain the exact number of table rows:

   int               RowsTotal(void)            { return this.m_list_rows.Total();  }

Unlike the rows, the number of columns here is returned only with the assumption that their number is the same in each row. So, d only the number of cells in the very first row is returned (line with the zero index in the list):

   int               ColumnsTotal(void)
                       {
                        //--- If there is no row in the list, return 0
                        if(this.RowsTotal()==0)
                           return 0;
                        //--- Get a pointer to the first row and return the number of cells in it
                        CTableRow *row=this.GetRow(0);
                        return(row!=NULL ? row.CellsTotal() : 0);
                       }

When expanding and finalizing this class, it will be possible to add methods that return the number of cells in a specified row and, accordingly, do not return the total number of cells in the table by multiplying the (exact) number of rows in the table by the number of cells in the first row (with the assumption stated above):

   int               CellsTotal(void){ return this.RowsTotal()*this.ColumnsTotal(); }

This is sufficient for accurate calculations for this version of the tabular data class, and there is no need to complicate things yet - these are just auxiliary classes for the information panel class, where we will use tabular (lattice) markup to place data on the panel.


Dashboard class

Let's define all possible mouse states:

  • The mouse buttons (left, right) are not pressed,
  • The mouse button is pressed outside the panel window,
  • The mouse button is pressed inside the panel window,
  • The mouse button is pressed inside the panel window title,
  • The mouse button is pressed on the "close" control element,
  • The mouse button is pressed on the "collapse/expand" control element,
  • The mouse button is pressed on the "pin" control element,
  • The mouse cursor is outside the panel window,
  • The mouse cursor is inside the panel window,
  • The mouse cursor is inside the panel window title,
  • The mouse cursor is inside the "close" control element,
  • The mouse cursor is inside the "collapse/expand" control element,
  • The mouse cursor is located inside the "pin" control element.

Let's create the corresponding enumeration:

enum ENUM_MOUSE_STATE
  {
   MOUSE_STATE_NOT_PRESSED,
   MOUSE_STATE_PRESSED_OUTSIDE_WINDOW,
   MOUSE_STATE_PRESSED_INSIDE_WINDOW,
   MOUSE_STATE_PRESSED_INSIDE_HEADER,
   MOUSE_STATE_PRESSED_INSIDE_CLOSE,
   MOUSE_STATE_PRESSED_INSIDE_MINIMIZE,
   MOUSE_STATE_PRESSED_INSIDE_PIN,
   MOUSE_STATE_OUTSIDE_WINDOW,
   MOUSE_STATE_INSIDE_WINDOW,
   MOUSE_STATE_INSIDE_HEADER,
   MOUSE_STATE_INSIDE_CLOSE,
   MOUSE_STATE_INSIDE_MINIMIZE,
   MOUSE_STATE_INSIDE_PIN
  };

At the moment, tracking of holding or clicking the mouse button on the control elements of the panel has been implemented. In other words, the first pressing is a trigger for fixing the state. However, in Windows applications, such a trigger is releasing a button after it is pressed - a click. Pressing and holding is used to drag objects. But for now, a simple solution is sufficient for us - the first pressing will be considered either a click or a hold. If we develop the panel further, it will be possible to supplement the handling of mouse buttons to match the behavior described above.

The CDashboard information panel class will consist of two elements: a canvas (background), where the panel design and control elements will be drawn, and a working area - the data placed on the panel will be drawn on it. The working space will always be completely transparent, and the canvas will have separate transparency values - for the header and for everything else:

Fig. 2. Only the canvas with different transparency of the title and the field with a frame


The area under the title, outlined by a frame, serves to place a working area on it. The area is completely transparent and contains data texts. In addition, the canvas area below the title may serve for visual design. In this case, tables are drawn on it:

Fig. 3. Table of 12 rows with 4 columns

A working area with data is superimposed on top of the designed canvas. As a result, we get a full-fledged panel:

Fig. 4. The panel with the 12x2 background table and data on top of it


We will save the values of some panel parameters into global terminal variables, so that the panel remembers its states and restores them when restarted - X and Y coordinates, the minimized state and the panel movability flag. When pinning the panel on the chart in a collapsed form, this pinned position is saved and the next time we collapse the pinned panel, it appears in the saved place.

Fig. 5. The panel "remembers" its anchor location if it was pinned in a collapsed form


We can see that in order to remember the binding location of a collapsed panel, it needs to be collapsed, moved to the binding location and pinned. When the panel is pinned in a collapsed state, its location is remembered. Then it can be expanded, unpinned and moved. In order for the panel to return to the saved binding location, it should be pinned and collapsed. Without pinning, the panel is collapsed in its current location.


Class body:

//+------------------------------------------------------------------+
//| Dashboard class                                                  |
//+------------------------------------------------------------------+
class CDashboard : public CObject
  {
private:
   CCanvas           m_canvas;                  // Canvas
   CCanvas           m_workspace;               // Work space
   CTableData        m_table_data;              // Table cell array
   ENUM_PROGRAM_TYPE m_program_type;            // Program type
   ENUM_MOUSE_STATE  m_mouse_state;             // Mouse button status
   uint              m_id;                      // Object ID
   long              m_chart_id;                // ChartID
   int               m_chart_w;                 // Chart width
   int               m_chart_h;                 // Chart height
   int               m_x;                       // X coordinate
   int               m_y;                       // Y coordinate
   int               m_w;                       // Width
   int               m_h;                       // Height
   int               m_x_dock;                  // X coordinate of the pinned collapsed panel
   int               m_y_dock;                  // Y coordinate of the pinned collapsed panel
   
   bool              m_header;                  // Header presence flag
   bool              m_butt_close;              // Close button presence flag
   bool              m_butt_minimize;           // Collapse/expand button presence flag
   bool              m_butt_pin;                // Pin button presence flag
   bool              m_wider_wnd;               // Flag for exceeding the horizontal size of the window width panel
   bool              m_higher_wnd;              // Flag for exceeding the vertical size of the window height panel
   bool              m_movable;                 // Panel movability flag 
   int               m_header_h;                // Header height
   int               m_wnd;                     // Chart subwindow index
   
   uchar             m_header_alpha;            // Header transparency
   uchar             m_header_alpha_c;          // Current header transparency
   color             m_header_back_color;       // Header background color
   color             m_header_back_color_c;     // Current header background color
   color             m_header_fore_color;       // Header text color
   color             m_header_fore_color_c;     // Current header text color
   color             m_header_border_color;     // Header border color
   color             m_header_border_color_c;   // Current header border color
   
   color             m_butt_close_back_color;   // Close button background color
   color             m_butt_close_back_color_c; // Current close button background color
   color             m_butt_close_fore_color;   // Close button icon color
   color             m_butt_close_fore_color_c; // Current close button color
   
   color             m_butt_min_back_color;     // Expand/collapse button background color
   color             m_butt_min_back_color_c;   // Current expand/collapse button background color
   color             m_butt_min_fore_color;     // Expand/collapse button icon color
   color             m_butt_min_fore_color_c;   // Current expand/collapse button icon color
   
   color             m_butt_pin_back_color;     // Pin button background color
   color             m_butt_pin_back_color_c;   // Current pin button background color
   color             m_butt_pin_fore_color;     // Pin button icon color
   color             m_butt_pin_fore_color_c;   // Current pin button icon color
   
   uchar             m_alpha;                   // Panel transparency
   uchar             m_alpha_c;                 // Current panel transparency
   uchar             m_fore_alpha;              // Text transparency
   uchar             m_fore_alpha_c;            // Current text transparency
   color             m_back_color;              // Background color 

   color             m_back_color_c;            // Current background color 

   color             m_fore_color;              // Text color
   color             m_fore_color_c;            // Current text color
   color             m_border_color;            // Border color
   color             m_border_color_c;          // Current border color
   
   string            m_title;                   // Title text
   string            m_title_font;              // Title font
   int               m_title_font_size;         // Title font size
   string            m_font;                    // Font
   int               m_font_size;               // Font size
   
   bool              m_minimized;               // Collapsed panel window flag 
   string            m_program_name;            // Program name
   string            m_name_gv_x;               // Name of the global terminal variable storing the X coordinate 
   string            m_name_gv_y;               // Name of the global terminal variable storing the Y coordinate
   string            m_name_gv_m;               // Name of the global terminal variable storing the collapsed panel flag 

   string            m_name_gv_u;               // Name of the global terminal variable storing the flag of the pinned panel 

   uint              m_array_wpx[];             // Array of pixels to save/restore the workspace 
   uint              m_array_ppx[];             // Array of pixels to save/restore the panel background 

//--- Return the flag that the panel exceeds (1) the height and (2) the width of the corresponding chart size
   bool              HigherWnd(void)      const { return(this.m_h+2>this.m_chart_h);   }
   bool              WiderWnd(void)       const { return(this.m_w+2>this.m_chart_w);   }
//--- Enable/disable modes of working with the chart
   void              SetChartsTool(const bool flag);
   
//--- Save (1) the working space and (2) the panel background to the pixel array
   void              SaveWorkspace(void);
   void              SaveBackground(void);
//--- Restore (1) the working space and (2) the panel background from the pixel array
   void              RestoreWorkspace(void);
   void              RestoreBackground(void);

//--- Save the pixel array (1) of the working space and the (2) panel background to the file
   bool              FileSaveWorkspace(void);
   bool              FileSaveBackground(void);
//--- Load the pixel array of the (1) working space and (2) the panel background from the file
   bool              FileLoadWorkspace(void);
   bool              FileLoadBackground(void);

//--- Return the subwindow index
   int               GetSubWindow(void) const
                       {
                        return(this.m_program_type==PROGRAM_EXPERT || this.m_program_type==PROGRAM_SCRIPT ? 0 : ::ChartWindowFind());
                       }
   
protected:
//--- (1) Hide, (2) show and (3) bring the panel to the foreground
   void              Hide(const bool redraw=false);
   void              Show(const bool redraw=false);
   void              BringToTop(void);
//--- Return the chart ID
   long              ChartID(void)        const { return this.m_chart_id;              }
//--- Draw the header area
   void              DrawHeaderArea(const string title);
//--- Redraw the header area using a new color and text values
   void              RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- Draw the panel frame
   void              DrawFrame(void);
//--- (1) Draw and (2) redraw the panel closing button
   void              DrawButtonClose(void);
   void              RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Draw and (2) redraw the panel collapse/expand button
   void              DrawButtonMinimize(void);
   void              RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);
//--- (1) Draw and (2) redraw the panel pin button
   void              DrawButtonPin(void);
   void              RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX);

//--- Return the flag for working in the visual tester
   bool              IsVisualMode(void) const
                       { return (bool)::MQLInfoInteger(MQL_VISUAL_MODE);               }
//--- Return the timeframe description
   string            TimeframeDescription(const ENUM_TIMEFRAMES timeframe) const
                       { return ::StringSubstr(EnumToString(timeframe),7);             }

//--- Return the state of mouse buttons
   ENUM_MOUSE_STATE  MouseButtonState(const int x,const int y,bool pressed);
//--- Shift the panel to new coordinates
   void              Move(int x,int y);

//--- Convert RGB to color
   color             RGBToColor(const double r,const double g,const double b) const;
//--- Write RGB component values to variables
   void              ColorToRGB(const color clr,double &r,double &g,double &b);
//--- Return (1) Red, (2) Green, (3) Blue color components
   double            GetR(const color clr)      { return clr&0xff ;                    }
   double            GetG(const color clr)      { return(clr>>8)&0xff;                 }
   double            GetB(const color clr)      { return(clr>>16)&0xff;                }
//--- Return a new color
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Draw a panel
   void              Draw(const string title);
//--- (1) Collapse and (2) expand the panel
   void              Collapse(void);
   void              Expand(void);

//--- Set the (1) X and (2) Y panel coordinates
   bool              SetCoordX(const int coord_x);
   bool              SetCoordY(const int coord_y);
//--- Set the panel (1) width and (2) height
   bool              SetWidth(const int width,const bool redraw=false);
   bool              SetHeight(const int height,const bool redraw=false);

public:
//--- Display the panel
   void              View(const string title)   { this.Draw(title);                    }
//--- Return the (1) CCanvas object, (2) working space, (3) object ID
   CCanvas          *Canvas(void)               { return &this.m_canvas;               }
   CCanvas          *Workspace(void)            { return &this.m_workspace;            }
   uint              ID(void)                   { return this.m_id;                    }
   
//--- Return the panel (1) X and (2) Y coordinates
   int               CoordX(void)         const { return this.m_x;                     }
   int               CoordY(void)         const { return this.m_y;                     }
//--- Return the panel (1) width and (2) height
   int               Width(void)          const { return this.m_w;                     }
   int               Height(void)         const { return this.m_h;                     }

//--- Return the (1) width, (2) height and (3) size of the specified text
   int               TextWidth(const string text)
                       { return this.m_workspace.TextWidth(text);                      }
   int               TextHeight(const string text)
                       { return this.m_workspace.TextHeight(text);                     }
   void              TextSize(const string text,int &width,int &height)
                       { this.m_workspace.TextSize(text,width,height);                 }
   
//--- Set the close button (1) presence, (2) absence flag
   void              SetButtonCloseOn(void);
   void              SetButtonCloseOff(void);
//--- Set the collapse/expand button (1) presence, (2) absence flag
   void              SetButtonMinimizeOn(void);
   void              SetButtonMinimizeOff(void);
   
//--- Set the panel coordinates
   bool              SetCoords(const int x,const int y);
//--- Set the panel size
   bool              SetSizes(const int w,const int h,const bool update=false);
//--- Set panel coordinates and size
   bool              SetParams(const int x,const int y,const int w,const int h,const bool update=false);

//--- Set the transparency of the panel (1) header and (2) working space
   void              SetHeaderTransparency(const uchar value);
   void              SetTransparency(const uchar value);
//--- Set default panel font parameters
   void              SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0);
//--- Display a text message at the specified coordinates
   void              DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE);
//--- Draw a (1) background grid (2) with automatic cell size
   void              DrawGrid(const uint x,const uint y,const uint rows,const uint columns,const uint row_size,const uint col_size,const color line_color=clrNONE,bool alternating_color=true);
   void              DrawGridAutoFill(const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true);
//--- Print grid data (line intersection coordinates)
   void              GridPrint(const uint indent=0)   { this.m_table_data.Print(indent);  }
//--- Write the X and Y coordinate values of the specified table cell to variables
   void              CellXY(const uint row,const uint column, int &x, int &y) { this.m_table_data.CellXY(row,column,x,y);  }
//--- Return the (1) X and (2) Y coordinate of the specified table cell
   int               CellX(const uint row,const uint column)         { return this.m_table_data.CellX(row,column);         }
   int               CellY(const uint row,const uint column)         { return this.m_table_data.CellY(row,column);         }

//--- Event handler
   void              OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
//--- Constructor/destructor
                     CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1);
                    ~CDashboard();
  };

The declared variables and class methods are commented in detail in the code. Let's look at the implementation of some methods.


Class constructor:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CDashboard::CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1) : 
                        m_id(id),
                        m_chart_id(::ChartID()),
                        m_program_type((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
                        m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)),
                        m_wnd(wnd==-1 ? GetSubWindow() : wnd),
                        m_chart_w((int)::ChartGetInteger(m_chart_id,CHART_WIDTH_IN_PIXELS,m_wnd)),
                        m_chart_h((int)::ChartGetInteger(m_chart_id,CHART_HEIGHT_IN_PIXELS,m_wnd)),
                        m_mouse_state(MOUSE_STATE_NOT_PRESSED),
                        m_x(x),
                        m_y(::ChartGetInteger(m_chart_id,CHART_SHOW_ONE_CLICK) ? (y<79 ? 79 : y) : y),
                        m_w(w),
                        m_h(h),
                        m_x_dock(m_x),
                        m_y_dock(m_y),
                        m_header(true),
                        m_butt_close(true),
                        m_butt_minimize(true),
                        m_butt_pin(true),
                        m_header_h(18),
                        
                        //--- Panel header implementation
                        m_header_alpha(128),
                        m_header_alpha_c(m_header_alpha),
                        m_header_back_color(C'0,153,188'),
                        m_header_back_color_c(m_header_back_color),
                        m_header_fore_color(C'182,255,244'),
                        m_header_fore_color_c(m_header_fore_color),
                        m_header_border_color(C'167,167,168'),
                        m_header_border_color_c(m_header_border_color),
                        m_title("Dashboard"),
                        m_title_font("Calibri"),
                        m_title_font_size(-100),
                        
                        //--- close button
                        m_butt_close_back_color(C'0,153,188'),
                        m_butt_close_back_color_c(m_butt_close_back_color),
                        m_butt_close_fore_color(clrWhite),
                        m_butt_close_fore_color_c(m_butt_close_fore_color),
                        
                        //--- collapse/expand button
                        m_butt_min_back_color(C'0,153,188'),
                        m_butt_min_back_color_c(m_butt_min_back_color),
                        m_butt_min_fore_color(clrWhite),
                        m_butt_min_fore_color_c(m_butt_min_fore_color),
                        
                        //--- pin button
                        m_butt_pin_back_color(C'0,153,188'),
                        m_butt_pin_back_color_c(m_butt_min_back_color),
                        m_butt_pin_fore_color(clrWhite),
                        m_butt_pin_fore_color_c(m_butt_min_fore_color),
                        
                        //--- Panel implementation
                        m_alpha(240),
                        m_alpha_c(m_alpha),
                        m_fore_alpha(255),
                        m_fore_alpha_c(m_fore_alpha),
                        m_back_color(C'240,240,240'),
                        m_back_color_c(m_back_color),
                        m_fore_color(C'53,0,0'),
                        m_fore_color_c(m_fore_color),
                        m_border_color(C'167,167,168'),
                        m_border_color_c(m_border_color),
                        m_font("Calibri"),
                        m_font_size(-100),
                        
                        m_minimized(false),
                        m_movable(true)
  {
//--- Set the permission for the chart to send messages about events of moving and pressing mouse buttons,
//--- mouse scroll events, as well as graphical object creation/deletion
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_WHEEL,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_CREATE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_DELETE,true);
   
//--- Set the names of global terminal variables to store panel coordinates, collapsed/expanded state and pinning
   this.m_name_gv_x=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_X";
   this.m_name_gv_y=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Y";
   this.m_name_gv_m=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Minimize";
   this.m_name_gv_u=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Unpin";
   
//--- If a global variable does not exist, create it and write the current value,
//--- otherwise - read the value from the terminal global variable into it
//--- X coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_x))
      ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
   else
      this.m_x=(int)::GlobalVariableGet(this.m_name_gv_x);
//--- Y coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_y))
      ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
   else
      this.m_y=(int)::GlobalVariableGet(this.m_name_gv_y);
//--- Collapsed/expanded
   if(!::GlobalVariableCheck(this.m_name_gv_m))
      ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
   else
      this.m_minimized=(int)::GlobalVariableGet(this.m_name_gv_m);
//--- Collapsed/not collapsed
   if(!::GlobalVariableCheck(this.m_name_gv_u))
      ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
   else
      this.m_movable=(int)::GlobalVariableGet(this.m_name_gv_u);

//--- Set the flags for the size of the panel exceeding the size of the chart window
   this.m_higher_wnd=this.HigherWnd();
   this.m_wider_wnd=this.WiderWnd();

//--- If the panel graphical resource is created,
   if(this.m_canvas.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"P"+(string)this.m_id,this.m_x,this.m_y,this.m_w,this.m_h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the canvas font and fill the canvas with the transparent color 
      this.m_canvas.FontSet(this.m_title_font,this.m_title_font_size,FW_BOLD);
      this.m_canvas.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for canvas failed",(string)__FUNCTION__);

//--- If a working space of a graphical resource is created,
   if(this.m_workspace.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"W"+(string)this.m_id,this.m_x+1,this.m_y+this.m_header_h,this.m_w-2,this.m_h-this.m_header_h-1,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the font for the working area and fill it with the transparent color 
      this.m_workspace.FontSet(this.m_font,this.m_font_size);
      this.m_workspace.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for workspace failed",(string)__FUNCTION__);
  }

The class has one parametric constructor and one created by default. Naturally, we are only interested in the parametric one. It will be used when creating a class object. A unique object ID, the initial coordinates of the panel, its width and height and the index of the subwindow the panel will be placed to are passed to the constructor via formal parameters.

The unique panel ID is needed so that the class can create objects with unique names. If you use several indicators with panels on one chart, then, in order to avoid conflicts of object names, you need this unique number, which is added to the name of the panel object when it is created. The uniqueness of the ID should be repeatable - with each new launch, the number should be the same as during the previous launch. For example, GetTickCount() is not suitable for the ID.
If the subwindow index is set by default (-1), it is searched programmatically, otherwise we use the one specified in the parameter.

The default parameters are set in the constructor initialization list. Some parameters responsible for the visuals feature the two variables - the default value and the current property value. This is necessary for interactive changes, for example, when hovering over the area of the panel such parameters are responsible for.

The constructor body contains the values of global terminal variables. Two graphical objects are created - the canvas and the panel working area.

All constructor code is commented in detail.


The class destructor:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CDashboard::~CDashboard()
  {
//--- Write the current values to global terminal variables
   ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
   ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
   ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
   ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
//--- Delete panel objects
   this.m_canvas.Destroy();
   this.m_workspace.Destroy();
  }

Here, we first reset the coordinates and flags to global terminal variables, and then delete the canvas and working space objects.

To interact with the panel with the cursor and mouse buttons, we need to know the location of the cursor relative to the panel and its control elements. As the cursor moves, we can track its coordinates and the states of the buttons in the class event handler. The class event handler has the same parameters as the standard OnChartEvent handler:

void  OnChartEvent()
   const int       id,       // event ID 
   const long&     lparam,   // long type event parameter
   const double&   dparam,   // double type event parameter
   const string&   sparam    // string type event parameter
   );

Parameters

id

[in]  Event ID from the ENUM_CHART_EVENT enumeration.

lparam

[in]  long type event parameter

dparam

[in]  double type event parameter

sparam

[in]  string type event parameter

Return Value

No return value

Note

There are 11 types of events that can be handled using the predefined OnChartEvent() function. 65535 IDs from CHARTEVENT_CUSTOM to CHARTEVENT_CUSTOM_LAST inclusive are provided for custom events. To generate a custom event, use the EventChartCustom() function.

Short event description from the ENUM_CHART_EVENT enumeration:

  • CHARTEVENT_KEYDOWN — pressing a key on the keyboard when a chart window is in focus;
  • CHARTEVENT_MOUSE_MOVE — moving the mouse and mouse button clicks (if CHART_EVENT_MOUSE_MOVE=true is set for the chart);
  • CHARTEVENT_OBJECT_CREATE — creating a graphical object (if CHART_EVENT_OBJECT_CREATE=true is set for the chart);
  • CHARTEVENT_OBJECT_CHANGE — change object properties via the properties dialog;
  • CHARTEVENT_OBJECT_DELETE — delete a graphical object (if CHART_EVENT_OBJECT_DELETE=true is set for the chart);
  • CHARTEVENT_CLICK — clicking on a chart;
  • CHARTEVENT_OBJECT_CLICK — mouse click on a graphical object belonging to a chart;
  • CHARTEVENT_OBJECT_DRAG — dragging a graphical object with a mouse;
  • CHARTEVENT_OBJECT_ENDEDIT — finish editing text in the Edit input box of a graphical object (OBJ_EDIT);
  • CHARTEVENT_CHART_CHANGE — change a chart;
  • CHARTEVENT_CUSTOM+n — custom event ID, where n is within the range from 0 to 65535. CHARTEVENT_CUSTOM_LAST contains the last acceptable custom event ID (CHARTEVENT_CUSTOM+65535).

The lparam parameter contains the X coordinate, dparam contains the Y coordinate, while sparam contains the combination of flag values to determine the state of the mouse buttons. All these parameters need to be received and processed relative to the coordinates of the panel and its elements. The state should be determined and sent to the class event handler, where the reaction to all these states will be specified.


The method that returns the state of the cursor and mouse button relative to the panel:

//+------------------------------------------------------------------+
//| Returns the state of the mouse cursor and button                 |
//+------------------------------------------------------------------+
ENUM_MOUSE_STATE CDashboard::MouseButtonState(const int x,const int y,bool pressed)
  {
//--- If the button is pressed
   if(pressed)
     {
      //--- If the state has already been saved, exit
      if(this.m_mouse_state!=MOUSE_STATE_NOT_PRESSED)
         return this.m_mouse_state;
      //--- If the button is pressed inside the window 
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- If the button is pressed inside the header
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Bring the panel to the foreground 
            this.BringToTop();
            //--- Coordinates of the close, collapse/expand and pin buttons 
            int wc=(this.m_butt_close ? this.m_header_h : 0);
            int wm=(this.m_butt_minimize ? this.m_header_h : 0);
            int wp=(this.m_butt_pin ? this.m_header_h : 0);
            //--- If the close button is pressed, return this state
            if(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_PRESSED_INSIDE_CLOSE;
            //--- If the collapse/expand button is pressed, return this state
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_PRESSED_INSIDE_MINIMIZE;
            //--- If the pin button is pressed, return this state
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_PRESSED_INSIDE_PIN;
            //--- If the button is not pressed on the control buttons of the panel, record and return the state of the button press inside the header
            this.m_mouse_state=MOUSE_STATE_PRESSED_INSIDE_HEADER;
            return this.m_mouse_state;
           }
         //--- If a button inside the window is pressed, write the state to a variable and return it
         else if(y>this.m_y+this.m_header_h && y<this.m_y+this.m_h)
           {
            this.m_mouse_state=MOUSE_STATE_PRESSED_INSIDE_WINDOW;
            return this.m_mouse_state;
           }
        }
      //--- The button is pressed outside the window - write the state to a variable and return it
      else
        {
         this.m_mouse_state=MOUSE_STATE_PRESSED_OUTSIDE_WINDOW;
         return this.m_mouse_state;
        }
     }
//--- If the button is not pressed
   else
     {
      //--- Write the state of the unpressed button to the variable
      this.m_mouse_state=MOUSE_STATE_NOT_PRESSED;
      //--- If the cursor is inside the panel 
      if(x>this.m_x && x<this.m_x+this.m_w && y>this.m_y && y<this.m_y+this.m_h)
        {
         //--- If the cursor is inside the header
         if(y>this.m_y && y<=this.m_y+this.m_header_h)
           {
            //--- Specify the width of the close, collapse/expand and pin buttons
            int wc=(this.m_butt_close ? this.m_header_h : 0);
            int wm=(this.m_butt_minimize ? this.m_header_h : 0);
            int wp=(this.m_butt_pin ? this.m_header_h : 0);
            //--- If the cursor is inside the close button, return this state 
            if(x>this.m_x+this.m_w-wc)
               return MOUSE_STATE_INSIDE_CLOSE;
            //--- If the cursor is inside the minimize/expand button, return this state 
            if(x>this.m_x+this.m_w-wc-wm)
               return MOUSE_STATE_INSIDE_MINIMIZE;
            //--- If the cursor is inside the pin button, return this state 
            if(x>this.m_x+this.m_w-wc-wm-wp)
               return MOUSE_STATE_INSIDE_PIN;
            //--- If the cursor is outside the buttons inside the header area, return this state 
            return MOUSE_STATE_INSIDE_HEADER;
           }
         //--- Otherwise, the cursor is inside the working space. Return this state 
         else
            return MOUSE_STATE_INSIDE_WINDOW;
        }
     }
//--- In any other case, return the state of the unpressed mouse button
   return MOUSE_STATE_NOT_PRESSED;
  }

The method logic has been described in the code comments in detail. We simply determine the mutual coordinates of the cursor, the panel and its elements, and return the state. The flag of the pressed or released mouse button is immediately sent to the method. For each such state there is its own block of code that defines the states either when the button is pressed or when it is released. Using logic this way is quite simple and fast. But there are some disadvantages - you cannot detect a mouse click on a control element. Instead, you can only detect a click on it. Usually, a click is registered when the mouse button is released, and pressing and holding is registered when it is pressed. With the logic used here, pressing the mouse button is the only action considered to be a click and press-and-hold.

The states obtained in this method should be sent to the event handler, where each event has its own handler that changes the behavior and appearance of the panel:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CDashboard::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- If a graphical object is created
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }
//--- If the chart is changed
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- Get the chart subwindow index (it may change when removing the window of any indicator)
      this.m_wnd=this.GetSubWindow();
      //--- Get the new chart size
      int w=(int)::ChartGetInteger(this.m_chart_id,CHART_WIDTH_IN_PIXELS,this.m_wnd);
      int h=(int)::ChartGetInteger(this.m_chart_id,CHART_HEIGHT_IN_PIXELS,this.m_wnd);
      //--- Determine whether the panel dimensions extend beyond the chart window
      this.m_higher_wnd=this.HigherWnd();
      this.m_wider_wnd=this.WiderWnd();
      //--- If the chart height has changed, adjust the panel vertical position
      if(this.m_chart_h!=h)
        {
         this.m_chart_h=h;
         int y=this.m_y;
         if(this.m_y+this.m_h>h-1)
            y=h-this.m_h-1;
         if(y<1)
            y=1;
         this.Move(this.m_x,y);
        }
      //--- If the chart weight has changed, adjust the panel horizontal position
      if(this.m_chart_w!=w)
        {
         this.m_chart_w=w;
         int x=this.m_x;
         if(this.m_x+this.m_w>w-1)
            x=w-this.m_w-1;
         if(x<1)
            x=1;
         this.Move(x,this.m_y);
        }
     }

//--- Declare variables to store the current cursor shift relative to the initial coordinates of the panel
   static int diff_x=0;
   static int diff_y=0;
   
//--- Get the flag of the held mouse button. We also take into account the right button for the visual tester (sparam=="2")
   bool pressed=(!this.IsVisualMode() ? (sparam=="1" || sparam=="" ? true : false) : sparam=="1" || sparam=="2" ? true : false);
//--- Get the cursor X and Y coordinates. Take into account the shift for the Y coordinate when working in the chart subwindow
   int  mouse_x=(int)lparam;
   int  mouse_y=(int)dparam-(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);
//--- Get the state of the cursor and mouse buttons relative to the panel
   ENUM_MOUSE_STATE state=this.MouseButtonState(mouse_x,mouse_y,pressed);
//--- If the cursor moves
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- If a button is pressed inside the working area of the panel
      if(state==MOUSE_STATE_PRESSED_INSIDE_WINDOW)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
         return;
        }
      //--- If a button is pressed inside the panel header area
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,-10,-10,-10);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
         //--- Shift the panel following the cursor taking into account the amount of cursor displacement relative to the initial coordinates of the panel
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }
        
      //--- If the close button is pressed 
      else if(state==MOUSE_STATE_PRESSED_INSIDE_CLOSE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the close button with a new background color 
         color new_color=this.NewColor(clrRed,0,40,40);
         if(this.m_butt_close_back_color_c!=new_color)
           {
            this.RedrawButtonClose(new_color);
            this.m_canvas.Update();
           }
         //--- Close button press handling should be defined in the program.
         //--- Send the click event of this button to its OnChartEvent handler.
         //--- Event ID 1001,
         //--- lparam=panel ID (m_id),
         //--- dparam=0
         //--- sparam="Close button pressed"
         ushort event=CHARTEVENT_CUSTOM+1;
         ::EventChartCustom(this.m_chart_id,ushort(event-CHARTEVENT_CUSTOM),this.m_id,0,"Close button pressed");
        }
      //--- If the panel collapse/expand button is pressed 
      else if(state==MOUSE_STATE_PRESSED_INSIDE_MINIMIZE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- "flip" the panel collapse flag,
         this.m_minimized=!this.m_minimized;
         //--- redraw the panel taking into account the new state of the flag,
         this.Draw(this.m_title);
         //--- redraw the panel header area 
         this.RedrawHeaderArea();
         //--- If the panel is pinned and expanded, move it to the stored location coordinates
         if(this.m_minimized && !this.m_movable)
            this.Move(this.m_x_dock,this.m_y_dock);
         //--- Update the canvas with chart redrawing and
         this.m_canvas.Update();
         //--- write the state of the panel expand flag to the global terminal variable
         ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
        }
      //--- If the panel pin button is pressed
      else if(state==MOUSE_STATE_PRESSED_INSIDE_PIN)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- "flip" the panel collapse flag,
         this.m_movable=!this.m_movable;
         //--- Redraw the pin button with a new background color
         color new_color=this.NewColor(this.m_butt_pin_back_color,30,30,30);
         if(this.m_butt_pin_back_color_c!=new_color)
            this.RedrawButtonPin(new_color);
         //--- If the panel is collapsed and pinned, save its coordinates
         //--- When expanded and collapsed again, the panel returns to these coordinates
         //--- Relevant for pinning a collapsed panel at the bottom of the screen
         if(this.m_minimized && !this.m_movable)
           {
            this.m_x_dock=this.m_x;
            this.m_y_dock=this.m_y;
           }
         //--- Update the canvas with chart redrawing and
         this.m_canvas.Update();
         //--- write the state of the panel movability flag to the global terminal variable
         ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
        }
        
      //--- If the cursor is inside the panel header area
      else if(state==MOUSE_STATE_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,20,20,20);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the close button
      else if(state==MOUSE_STATE_INSIDE_CLOSE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the collapse/expand button with the default background color 
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_color);
         //--- Redraw the pin button with the default background color 
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_color);
         //--- Redraw the close button with the red background color
         if(this.m_butt_close_back_color_c!=clrRed)
           {
            this.RedrawButtonClose(clrRed);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the collapse/expand button
      else if(state==MOUSE_STATE_INSIDE_MINIMIZE)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the close button with the default background color
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Redraw the pin button with the default background color 
         if(this.m_butt_pin_back_color_c!=this.m_butt_pin_back_color)
            this.RedrawButtonPin(this.m_butt_pin_back_color);
         //--- Redraw the collapse/expand button with a new background color 
         new_color=this.NewColor(this.m_butt_min_back_color,20,20,20);
         if(this.m_butt_min_back_color_c!=new_color)
           {
            this.RedrawButtonMinimize(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the pin button
      else if(state==MOUSE_STATE_INSIDE_PIN)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with a minimal change in the background color
         color new_color=this.NewColor(this.m_header_back_color,0,0,1);
         if(this.m_header_back_color_c!=new_color)
            this.RedrawHeaderArea(new_color);
         //--- Redraw the close button with the default background color
         if(this.m_butt_close_back_color_c!=this.m_butt_close_back_color)
            this.RedrawButtonClose(this.m_butt_close_back_color);
         //--- Redraw the collapse/expand button with the default background color 
         if(this.m_butt_min_back_color_c!=this.m_butt_min_back_color)
            this.RedrawButtonMinimize(this.m_butt_min_back_color);
         //--- Redraw the pin button with a new background color
         new_color=this.NewColor(this.m_butt_pin_back_color,20,20,20);
         if(this.m_butt_pin_back_color_c!=new_color)
           {
            this.RedrawButtonPin(new_color);
            this.m_canvas.Update();
           }
        }
        
      //--- If the cursor is inside the working space
      else if(state==MOUSE_STATE_INSIDE_WINDOW)
        {
         //--- Disable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(false);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Otherwise (the cursor is outside the panel, and we need to restore the chart parameters) 
      else
        {
         //--- Enable chart scrolling, right-click menu and crosshair
         this.SetChartsTool(true);
         //--- Redraw the header area with the default background color 
         if(this.m_header_back_color_c!=this.m_header_back_color)
           {
            this.RedrawHeaderArea(this.m_header_back_color);
            this.m_canvas.Update();
           }
        }
      //--- Write the cursor shift by X and Y relative to the panel initial coordinates
      diff_x=mouse_x-this.m_x;
      diff_y=mouse_y-this.m_y;
     }
  }

The logic of the event handler is commented in sufficient detail in the code. We should consider some notable points here.

Handling the event of creating a new graphical object is specified at the very beginning:

//--- If a graphical object is created
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.BringToTop();
      ::ObjectSetInteger(this.m_chart_id,sparam,OBJPROP_SELECTED,true);
     }

What are its functions? If we create a new graphical object, it is placed above other graphical objects on the chart and, accordingly, is superimposed on top of the panel. Therefore, when defining such an event, the panel is brought to the foreground immediately. Then a new graphical object is highlighted. Why? If this is not done, then graphical objects that require several points for construction, for example, a trend line, will not be created normally - all their control points will be located in the same coordinate, and the object itself will not be visible. This happens due to the loss of control of the graphical object during its creation when the panel is brought to the foreground. Therefore, the new graphical object should be forced to be selected after bringing the panel to the foreground.
Thus, the mutual behavior of the panel and graphical objects when creating them will be as follows:

Fig. 6. New graphical object is built "under" the panel and does not lose focus when created


The event handler has its own processing block for each state. The logic of all these blocks is identical. For example, processing a mouse click and hold on the panel header:

      //--- If a button is pressed inside the panel header area
      else if(state==MOUSE_STATE_PRESSED_INSIDE_HEADER)
        {
         //--- Disable chart scrolling, right-click menu and crosshair 
         this.SetChartsTool(false);
         //--- Redraw the header area with a new background color 
         color new_color=this.NewColor(this.m_header_back_color,-10,-10,-10);
         if(this.m_header_back_color_c!=new_color)
           {
            this.RedrawHeaderArea(new_color);
            this.m_canvas.Update();
           }
         //--- Shift the panel following the cursor taking into account the amount of cursor displacement relative to the initial coordinates of the panel
         if(this.m_movable)
            this.Move(mouse_x-diff_x,mouse_y-diff_y);
         return;
        }

To prevent the chart from moving along with the panel, the events of scrolling the chart with the mouse, the right-click menu and the crosshair are disabled for it. Since the header should visually respond to mouse capture, its color is made darker. Before changing the color to a new one, we need to check whether it has already changed. There is no point in constantly changing it to the same one wasting CPU resources. If movement is not disabled (the panel is not pinned), we shift it to new coordinates calculated from the mouse coordinates minus the shift of the cursor location relative to the upper left corner of the panel. If the shift is not considered, the panel will be positioned exactly at the cursor coordinates in the upper left corner.


The method for moving the panel to specified coordinates:

//+------------------------------------------------------------------+
//| Move the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Move(int x,int y)
  {
   int h=this.m_canvas.Height();
   int w=this.m_canvas.Width();
   if(!this.m_wider_wnd)
     {
      if(x+w>this.m_chart_w-1)
         x=this.m_chart_w-w-1;
      if(x<1)
         x=1;
     }
   else
     {
      if(x>1)
         x=1;
      if(x<this.m_chart_w-w-1)
         x=this.m_chart_w-w-1;
     }
   if(!this.m_higher_wnd)
     {
      if(y+h>this.m_chart_h-2)
         y=this.m_chart_h-h-2;
      if(y<1)
         y=1;
     }
   else
     {
      if(y>1)
         y=1;
      if(y<this.m_chart_h-h-2)
         y=this.m_chart_h-h-2;
     }
   if(this.SetCoords(x,y))
      this.m_canvas.Update();
  }

The coordinates where the panel should be moved are passed to the method. If the panel goes outside the chart when changing the coordinates, then the coordinates are adjusted so that the panel is always located inside the chart window indented from any edge by 1 pixel. Upon completion of all checks and adjustments of the panel coordinates, new coordinates of its location on the chart are established.


The methods that set the panel coordinates:

//+------------------------------------------------------------------+
//| Set the panel X coordinate                                       |
//+------------------------------------------------------------------+
bool CDashboard::SetCoordX(const int coord_x)
  {
   int x=(int)::ObjectGetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_XDISTANCE);
   if(x==coord_x)
      return true;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_XDISTANCE,coord_x))
      return false;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_XDISTANCE,coord_x+1))
      return false;
   this.m_x=coord_x;
   return true;
  }
//+------------------------------------------------------------------+
//| Set the panel Y coordinate                                       |
//+------------------------------------------------------------------+
bool CDashboard::SetCoordY(const int coord_y)
  {
   int y=(int)::ObjectGetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_YDISTANCE);
   if(y==coord_y)
      return true;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_YDISTANCE,coord_y))
      return false;
   if(!::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_YDISTANCE,coord_y+this.m_header_h))
      return false;
   this.m_y=coord_y;
   return true;
  }

If a coordinate equal to the coordinate of the panel is passed to the method, then there is no need to set it again - the success of the method is simply returned. The canvas is shifted first followed by the working space. The working space is shifted taking into account its relative location on the canvas - on the left by one pixel inside the panel, and on top - by the height of the header.


The methods for setting panel dimensions:

//+------------------------------------------------------------------+
//| Set the panel width                                              |
//+------------------------------------------------------------------+
bool CDashboard::SetWidth(const int width,const bool redraw=false)
  {
   if(width<4)
     {
      ::PrintFormat("%s: Error. Width cannot be less than 4px",(string)__FUNCTION__);
      return false;
     }
   if(width==this.m_canvas.Width())
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
      return false;
   if(width-2<1)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   else
     {
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
      if(!this.m_workspace.Resize(width-2,this.m_workspace.Height()))
         return false;
     }
   this.m_w=width;
   return true;
  }
//+------------------------------------------------------------------+
//| Set the panel height                                             |
//+------------------------------------------------------------------+
bool CDashboard::SetHeight(const int height,const bool redraw=false)
  {
   if(height<::fmax(this.m_header_h,1))
     {
      ::PrintFormat("%s: Error. Width cannot be less than %lupx",(string)__FUNCTION__,::fmax(this.m_header_h,1));
      return false;
     }
   if(height==this.m_canvas.Height())
      return true;
   if(!this.m_canvas.Resize(this.m_canvas.Width(),height))
      return false;
   if(height-this.m_header_h-2<1)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   else
     {
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
      if(!this.m_workspace.Resize(this.m_workspace.Width(),height-this.m_header_h-2))
         return false;
     }
   this.m_h=height;
   return true;
  }

Here all is exactly the same as when setting coordinates - if the size passed to the method is the same one that the panel already has, then the methods simply return true. One detail worth noting is that the working space is always smaller than the canvas. If, when reducing the size of the working space, it turns out that the size becomes less than 1, then in order to avoid resizing errors, the working space is simply hidden without changing its size.


Auxiliary methods that set two coordinates at once, all dimensions and coordinates, as well as dimensions of the panel at once:

//+------------------------------------------------------------------+
//| Set the panel coordinates                                        |
//+------------------------------------------------------------------+
bool CDashboard::SetCoords(const int x,const int y)
  {
   bool res=true;
   res &=this.SetCoordX(x);
   res &=this.SetCoordY(y);
   return res;
  }
//+------------------------------------------------------------------+
//| Set the panel size                                               |
//+------------------------------------------------------------------+
bool CDashboard::SetSizes(const int w,const int h,const bool update=false)
  {
   bool res=true;
   res &=this.SetWidth(w);
   res &=this.SetHeight(h);
   if(res && update)
      this.Expand();
   return res;
  }
//+------------------------------------------------------------------+
//| Set panel coordinates and size                                   |
//+------------------------------------------------------------------+
bool CDashboard::SetParams(const int x,const int y,const int w,const int h,const bool update=false)
  {
   bool res=true;
   res &=this.SetCoords(x,y);
   res &=this.SetSizes(w,h);
   if(res && update)
      this.Expand();
   return res;
  }

The methods receive the parameters and the update flag. If the parameters and the update flag are set successfully, the panel expansion method is called, in which all panel elements are redrawn.


The method that draws the header area:

//+------------------------------------------------------------------+
//| Draw the header area                                             |
//+------------------------------------------------------------------+
void CDashboard::DrawHeaderArea(const string title)
  {
//--- Exit if the header is not used
   if(!this.m_header)
      return;
//--- Set the title text
   this.m_title=title;
//--- The Y coordinate of the text is located vertically in the center of the header area
   int y=this.m_header_h/2;
//--- Fill the area with color
   this.m_canvas.FillRectangle(0,0,this.m_w-1,this.m_header_h-1,::ColorToARGB(this.m_header_back_color,this.m_header_alpha));
//--- Display the header text
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(this.m_header_fore_color,this.m_header_alpha),TA_LEFT|TA_VCENTER);
//--- Save the current header background color
   this.m_header_back_color_c=this.m_header_back_color;
//--- Draw control elements (close, collapse/expand and pin buttons) and
   this.DrawButtonClose();
   this.DrawButtonMinimize();
   this.DrawButtonPin();
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

The method draws the header area - paints a rectangular area at the header coordinates and draws the controls - close, collapse/expand and pin buttons. The default header area colors and transparency are used.


Method that redraws the header area:

//+------------------------------------------------------------------+
//| Redraw header area                                               |
//+------------------------------------------------------------------+
void CDashboard::RedrawHeaderArea(const color new_color=clrNONE,const string title="",const color title_new_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the header is not used or all passed parameters have default values
   if(!this.m_header || (new_color==clrNONE && title=="" && title_new_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- Exit if all passed parameters are equal to those already set
   if(new_color==this.m_header_back_color && title==this.m_title && title_new_color==this.m_header_fore_color && new_alpha==this.m_header_alpha)
      return;
//--- If the title is not equal to the default value, set a new title
   if(title!="")
      this.m_title=title;
//--- Define new background and text colors, and transparency
   color back_clr=(new_color!=clrNONE ? new_color : this.m_header_back_color);
   color fore_clr=(title_new_color!=clrNONE ? title_new_color : this.m_header_fore_color);  
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- The Y coordinate of the text is located vertically in the center of the header area
   int y=this.m_header_h/2;
//--- Fill the area with color
   this.m_canvas.FillRectangle(0,0,this.m_w-1,this.m_header_h-1,::ColorToARGB(back_clr,alpha));
//--- Display the header text
   this.m_canvas.TextOut(2,y,this.m_title,::ColorToARGB(fore_clr,alpha),TA_LEFT|TA_VCENTER);
//--- Save the current header background color, text and transparency
   this.m_header_back_color_c=back_clr;
   this.m_header_fore_color_c=fore_clr;
   this.m_header_alpha_c=alpha;
//--- Draw control elements (close, collapse/expand and pin buttons) and
   this.RedrawButtonClose(back_clr,clrNONE,alpha);
   this.RedrawButtonMinimize(back_clr,clrNONE,alpha);
   this.RedrawButtonPin(back_clr,clrNONE,alpha);
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(true);
  }

The new background color, new header text, new header text color and new transparency are passed to the method. Exit the method if the passed parameters are completely identical to those already set. The method is used to update the header color, text and transparency.


The method that draws the panel frame:

//+------------------------------------------------------------------+
//| Draw the panel frame                                             |
//+------------------------------------------------------------------+
void CDashboard::DrawFrame(void)
  {
   this.m_canvas.Rectangle(0,0,this.m_w-1,this.m_h-1,::ColorToARGB(this.m_border_color,this.m_alpha));
   this.m_border_color_c=this.m_border_color;
   this.m_canvas.Update(false);
  }

Draw a frame around the canvas perimeter and save the set color as the current one.


The method that draws the panel close button:

//+------------------------------------------------------------------+
//| Draws the panel close button                                     |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonClose(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_close)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- Button coordinates and size
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_close_back_color,this.m_header_alpha));
//--- Draw the close button
   this.m_canvas.LineThick(x1+shift+1,y1+shift+1,x2-shift,y2-shift,::ColorToARGB(this.m_butt_close_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
   this.m_canvas.LineThick(x1+shift+1,y2-shift-1,x2-shift,y1+shift,::ColorToARGB(this.m_butt_close_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_close_back_color_c=this.m_butt_close_back_color;
   this.m_butt_close_fore_color_c=this.m_butt_close_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

The entire logic is described in the code comments: draw the background and the closing icon (a cross) on top of it.


The method that redraws the panel close button:

//+------------------------------------------------------------------+
//| Redraw the panel close button                                    |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonClose(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_close || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- Button coordinates and size
   int x1=this.m_w-w;
   int x2=this.m_w-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_close_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_close_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Draw the close button
   this.m_canvas.LineThick(x1+shift+1,y1+shift+1,x2-shift,y2-shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
   this.m_canvas.LineThick(x1+shift+1,y2-shift-1,x2-shift,y1+shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_close_back_color_c=back_color;
   this.m_butt_close_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

In order to redraw, at least one of the passed parameters should be different from the current one. The rest is identical to the button drawing method, except for selecting and setting new drawing parameters.


Other methods for drawing and redrawing collapse/expand and pin buttons:

//+------------------------------------------------------------------+
//| Draw the panel collapse/expand button                            |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonMinimize(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_minimize)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close button is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_min_back_color,this.m_header_alpha));
//--- If the panel is collapsed, draw a rectangle
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(this.m_butt_min_fore_color,255));
//--- Otherwise, the panel is expanded. Draw a line segment
   else
      this.m_canvas.LineThick(x1+shift,y2-shift,x2-shift,y2-shift,::ColorToARGB(this.m_butt_min_fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_min_back_color_c=this.m_butt_min_back_color;
   this.m_butt_min_fore_color_c=this.m_butt_min_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Redraw the panel collapse/expand button                          |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonMinimize(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_minimize || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close button is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-w;
   int x2=this.m_w-wc-1;
   int y1=0;
   int y2=w-1;
//--- Shift of the upper left corner of the rectangular area of the image from the upper left corner of the button
   int shift=4;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_min_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_min_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- If the panel is collapsed, draw a rectangle
   if(this.m_minimized)
      this.m_canvas.Rectangle(x1+shift,y1+shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255));
//--- Otherwise, the panel is expanded. Draw a line segment
   else
      this.m_canvas.LineThick(x1+shift,y2-shift,x2-shift,y2-shift,::ColorToARGB(fore_color,255),3,STYLE_SOLID,LINE_END_ROUND);
//--- Remember the current background color and button design
   this.m_butt_min_back_color_c=back_color;
   this.m_butt_min_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Draw the panel pin button                                        |
//+------------------------------------------------------------------+
void CDashboard::DrawButtonPin(void)
  {
//--- Exit if the button is not used
   if(!this.m_butt_pin)
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close and collapse buttons is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(this.m_butt_pin_back_color,this.m_header_alpha));
//--- Coordinates of the broken line points
   int x[]={x1+3, x1+6, x1+3,x1+4,x1+6,x1+9,x1+9,x1+10,x1+15,x1+14,x1+13,x1+10,x1+10,x1+9,x1+6};
   int y[]={y1+14,y1+11,y1+8,y1+7,y1+7,y1+4,y1+3,y1+2, y1+7, y1+8, y1+8, y1+11,y1+13,y1+14,y1+11};
//--- Draw the "button" shape 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- If the movability flag is reset (pinned) - cross out the drawn button
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Remember the current background color and button design
   this.m_butt_pin_back_color_c=this.m_butt_pin_back_color;
   this.m_butt_pin_fore_color_c=this.m_butt_pin_fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }
//+------------------------------------------------------------------+
//| Redraw the panel pin button                                      |
//+------------------------------------------------------------------+
void CDashboard::RedrawButtonPin(const color new_back_color=clrNONE,const color new_fore_color=clrNONE,const ushort new_alpha=USHORT_MAX)
  {
//--- Exit if the button is not used or all passed parameters have default values
   if(!this.m_butt_pin || (new_back_color==clrNONE && new_fore_color==clrNONE && new_alpha==USHORT_MAX))
      return;
//--- The button width is equal to the height of the header area
   int w=this.m_header_h;
//--- The width of the close and collapse buttons is zero if the button is not used
   int wc=(this.m_butt_close ? w : 0);
   int wm=(this.m_butt_minimize ? w : 0);
//--- Button coordinates and size
   int x1=this.m_w-wc-wm-w;
   int x2=this.m_w-wc-wm-1;
   int y1=0;
   int y2=w-1;
//--- Define new background and text colors, and transparency
   color back_color=(new_back_color!=clrNONE ? new_back_color : this.m_butt_pin_back_color);
   color fore_color=(new_fore_color!=clrNONE ? new_fore_color : this.m_butt_pin_fore_color);
   uchar alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : new_alpha>255 ? 255 : new_alpha);
//--- Draw the button background
   this.m_canvas.FillRectangle(x1,y1,x2,y2,::ColorToARGB(back_color,alpha));
//--- Coordinates of the broken line points
   int x[]={x1+3, x1+6, x1+3,x1+4,x1+6,x1+9,x1+9,x1+10,x1+15,x1+14,x1+13,x1+10,x1+10,x1+9,x1+6};
   int y[]={y1+14,y1+11,y1+8,y1+7,y1+7,y1+4,y1+3,y1+2, y1+7, y1+8, y1+8, y1+11,y1+13,y1+14,y1+11};
//--- Draw the "button" shape 
   this.m_canvas.Polygon(x,y,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- If the movability flag is reset (pinned) - cross out the drawn button
   if(!this.m_movable)
      this.m_canvas.Line(x1+3,y1+2,x1+15,y1+14,::ColorToARGB(this.m_butt_pin_fore_color,255));
//--- Remember the current background color and button design
   this.m_butt_pin_back_color_c=back_color;
   this.m_butt_pin_fore_color_c=fore_color;
//--- update the canvas without redrawing the screen
   this.m_canvas.Update(false);
  }

The methods are identical to the methods for drawing and redrawing the close button. The logic is exactly the same, and it is written in the comments to the code.


The method that draws the panel:

//+------------------------------------------------------------------+
//| Draw the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Draw(const string title)
  {
//--- Set the title text
   this.m_title=title;
//--- If the collapse flag is not set, expand the panel
   if(!this.m_minimized)
      this.Expand();
//--- Otherwise, collapse the panel
   else
      this.Collapse();
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
//--- Update the working space and redraw the chart
   this.m_workspace.Update();
  }

If the collapse flag is not set, expand the panel (draw it in expanded form). If the collapse flag is set, then collapse the panel: draw the panel in a collapsed form leaving only the header.


The method for collapsing the panel:

//+------------------------------------------------------------------+
//| Collapse the panel                                               |
//+------------------------------------------------------------------+
void CDashboard::Collapse(void)
  {
//--- Save the pixels of the working space and the panel background into arrays
   this.SaveWorkspace();
   this.SaveBackground();
//--- Remember the current height of the panel
   int h=this.m_h;
//--- Change the dimensions (height) of the canvas and working space
   if(!this.SetSizes(this.m_canvas.Width(),this.m_header_h))
      return;
//--- Draw the header area
   this.DrawHeaderArea(this.m_title);
//--- Return the saved panel height to the variable
   this.m_h=h;
  }

Before collapsing the panel, we need to save all the pixels of the background and working space into arrays. This is necessary to quickly expand the panel by simply restoring the panel images and working space from pixel arrays instead of drawing it anew. Besides, we can draw some additional decorations against the background of the panel (for example, a name plate). They will also be saved along with the background and then restored.


The method that expands the panel:

//+------------------------------------------------------------------+
//| Expand the panel                                                 |
//+------------------------------------------------------------------+
void CDashboard::Expand(void)
  {
//--- Resize the panel
   if(!this.SetSizes(this.m_canvas.Width(),this.m_h))
      return;
//--- If the panel background pixels have never been saved into an array
   if(this.m_array_ppx.Size()==0)
     {
      //--- Draw the panel and
      this.m_canvas.Erase(::ColorToARGB(this.m_back_color,this.m_alpha));
      this.DrawFrame();
      this.DrawHeaderArea(this.m_title);
      //--- save the background pixels of the panel and working space into arrays
      this.SaveWorkspace();
      this.SaveBackground();
     }
//--- If the background pixels of the panel and working space were previously saved,
   else
     {
      //--- restore the background pixels of the panel and working space from arrays
      this.RestoreBackground();
      if(this.m_array_wpx.Size()>0)
         this.RestoreWorkspace();
     }
//--- If, after expanding, the panel goes beyond the chart window, adjust the panel location
   if(this.m_y+this.m_canvas.Height()>this.m_chart_h-1)
      this.Move(this.m_x,this.m_chart_h-1-this.m_canvas.Height());
  }

If the arrays, in which the background and work area pixels are saved, are empty, then we draw the entire panel using drawing methods. If the arrays are already filled, then simply restore the background of the panel and its working space from the arrays.


Auxiliary methods for working with color:

//+------------------------------------------------------------------+
//| Returns color with a new color component                         |
//+------------------------------------------------------------------+
color CDashboard::NewColor(color base_color, int shift_red, int shift_green, int shift_blue)
  {
   double clR=0, clG=0, clB=0;
   this.ColorToRGB(base_color,clR,clG,clB);
   double clRn=(clR+shift_red  < 0 ? 0 : clR+shift_red  > 255 ? 255 : clR+shift_red);
   double clGn=(clG+shift_green< 0 ? 0 : clG+shift_green> 255 ? 255 : clG+shift_green);
   double clBn=(clB+shift_blue < 0 ? 0 : clB+shift_blue > 255 ? 255 : clB+shift_blue);
   return this.RGBToColor(clRn,clGn,clBn);
  }
//+------------------------------------------------------------------+
//| Convert RGB to color                                             |
//+------------------------------------------------------------------+
color CDashboard::RGBToColor(const double r,const double g,const double b) const
  {
   int int_r=(int)::round(r);
   int int_g=(int)::round(g);
   int int_b=(int)::round(b);
   int clr=0;
   clr=int_b;
   clr<<=8;
   clr|=int_g;
   clr<<=8;
   clr|=int_r;
//---
   return (color)clr;
  }
//+------------------------------------------------------------------+
//| Getting values of the RGB components                             |
//+------------------------------------------------------------------+
void CDashboard::ColorToRGB(const color clr,double &r,double &g,double &b)
  {
   r=GetR(clr);
   g=GetG(clr);
   b=GetB(clr);
  }

The methods are required to change color when the cursor interacts with the panel controls.


The method for setting the header transparency:

//+------------------------------------------------------------------+
//| Set the header transparency                                      |
//+------------------------------------------------------------------+
void CDashboard::SetHeaderTransparency(const uchar value)
  {
   this.m_header_alpha=value;
   if(this.m_header_alpha_c!=this.m_header_alpha)
      this.RedrawHeaderArea(clrNONE,NULL,clrNONE,value);
   this.m_header_alpha_c=value;
  }

First, the transparency value passed to the method is written to a variable that stores the default transparency, then the new value is compared with the current one. If the values are not equal, the header area is completely redrawn. At the end, the set transparency is written to the current one.


The method setting the panel transparency:

//+------------------------------------------------------------------+
//| Set the panel transparency                                       |
//+------------------------------------------------------------------+
void CDashboard::SetTransparency(const uchar value)
  {
   this.m_alpha=value;
   if(this.m_alpha_c!=this.m_alpha)
     {
      this.m_canvas.Erase(::ColorToARGB(this.m_back_color,value));
      this.DrawFrame();
      this.RedrawHeaderArea(clrNONE,NULL,clrNONE,value);
      this.m_canvas.Update(false);
     }
   this.m_alpha_c=value;
  }

The logic is similar to the above method. If the transparency passed to the method is not equal to the current one, then the panel is completely redrawn with the new transparency.


The method setting the default font parameters of the working space:

//+------------------------------------------------------------------+
//| Set the default font parameters of the working space             |
//+------------------------------------------------------------------+
void CDashboard::SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0)
  {
   if(!this.m_workspace.FontSet(name,size*-10,flags,angle))
     {
      ::PrintFormat("%s: Failed to set font options. Error %lu",(string)__FUNCTION__,::GetLastError());
      return;
     }
   this.m_font=name;
   this.m_font_size=size*-10;
  }

The font parameters passed to the method (font name, size, flags and angle) are set to the CCanvas object of the workspace and stored in variables.

The font size passed to the method is multiplied by -10 for the reason described in the TextSetFont function note:

If the font name uses "::", then the font is loaded from EX5 resource. If the font name is specified with an extension, then the font is loaded from the file, and if the path begins with "\" or "/", then the file is searched relative to the MQL5 directory, otherwise it is searched relative to the path of the EX5 file that called the TextSetFont() function.

The font size is set using positive or negative values. The sign defines whether the text size depends on the operating system settings (font scale).

  • If the size is positive, it is converted into device physical units (pixels) when displaying the logical font as a physical one. The size corresponds to the height of symbol cells from available fonts. It is not recommended in cases of shared usage of texts displayed using the TextOut() function and texts displayed using the OBJ_LABEL ("Text label") graphical object.
  • If the size is negative, it is assumed to be set in tenths of a logical point (the value -350 is equal to 35 logical points) and is divided by 10. The resulting value is converted into physical units of the device (pixels) and corresponds to the absolute value of the character height from available fonts. To obtain a text of the OBJ_LABEL object size on the screen, multiply the font size specified in the object properties by -10.


The method for enabling/disabling modes of working with a chart:

//+------------------------------------------------------------------+
//| Enable/disable modes of working with the chart                   |
//+------------------------------------------------------------------+
void CDashboard::SetChartsTool(const bool flag)
  {
//--- If the 'true' flag is passed and if chart scrolling is disabled
   if(flag && !::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- enable chart scrolling, right-click menu and crosshair
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,true);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,true);
     }
//--- otherwise, if the 'false' flag is passed and if chart scrolling is enabled
   else if(!flag && ::ChartGetInteger(this.m_chart_id,CHART_MOUSE_SCROLL))
     {
      //--- disable chart scrolling, right-click menu and crosshair
      ::ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
      ::ChartSetInteger(0,CHART_CONTEXT_MENU,false);
      ::ChartSetInteger(0,CHART_CROSSHAIR_TOOL,false);
     }
  }

Depending on the passed flag, the method checks the state of scrolling the chart with the mouse and either enables all modes of working with the chart or disables them. Checking the scrolling mode is necessary in order not to constantly send a command to set modes when the cursor is within or outside the panel window. Switching modes occurs only when the cursor enters the panel, or when the cursor leaves its limits.


The method that outputs a text message to the specified coordinates:

//+------------------------------------------------------------------+
//| Display a text message at the specified coordinates              |
//+------------------------------------------------------------------+
void CDashboard::DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE)
  {
//--- Declare variables to record the text width and height in them
   int w=width;
   int h=height;
//--- If the width and height of the text passed to the method have zero values,
//--- then the entire working space is completely cleared using the transparent color
   if(width==0 && height==0)
      this.m_workspace.Erase(0x00FFFFFF);
//--- Otherwise
   else
     {
      //--- If the passed width and height have default values (-1), we get its width and height from the text 
      if(width==WRONG_VALUE && height==WRONG_VALUE)
         this.m_workspace.TextSize(text,w,h);
      //--- otherwise,
      else
        {
         //--- if the width passed to the method has the default value (-1) - get the width from the text, or
         //--- if the width passed to the method has a value greater than zero, use the width passed to the method, or
         //--- if the width passed to the method has a zero value, use the value 1 for the width
         w=(width ==WRONG_VALUE ? this.m_workspace.TextWidth(text)  : width>0  ? width  : 1);
         //--- if the height passed to the method has a default value (-1), get the height from the text, or
         //--- if the height passed to the method has a value greater than zero, use the height passed to the method, or
         //--- if the height passed to the method has a zero value, use value 1 for the height
         h=(height==WRONG_VALUE ? this.m_workspace.TextHeight(text) : height>0 ? height : 1);
        }
      //--- Fill the space according to the specified coordinates and the resulting width and height with a transparent color (erase the previous entry)
      this.m_workspace.FillRectangle(x,y,x+w,y+h,0x00FFFFFF);
     }
//--- Display the text to the space cleared of previous text and update the working space without redrawing the screen
   this.m_workspace.TextOut(x,y,text,::ColorToARGB(this.m_fore_color));
   this.m_workspace.Update(false);
  }

Working with a canvas in the case of displaying graphics on it involves drawing these graphics on the canvas like using a brush on a canvas. One image painted on the canvas is placed on top of another image painted first. To replace an image, you should either completely redraw the entire canvas or calculate the dimensions of the previous image, erase this area and draw the next image on the erased area.

For texts, we can get the dimensions of the already drawn text on the canvas in order to erase that area before drawing the next text. Storing all previously drawn texts and shapes somewhere in an object is by no means an optimal solution. Therefore, a choice is made here between accuracy and simplicity paired with optimality. Here we will get the dimensions of the current text (which may not match the width of the text previously drawn in this place), and use these dimensions to erase the previously drawn text before displaying the next one.

If the previous text is not the same width as the current one, or rather larger, then not all of it will be erased. For such cases, the method provides for passing parameters indicating the required dimensions of the erased area in height and width:

  1. If the width and height values passed to the method are equal to -1 (by default), then the area equal to the width and height of the current text will be erased,
  2. If zeros are passed, the entire working space is completely erased,
  3. If a width or height value greater than zero is passed, then these values will be used for the width and height, respectively.
Typically, if text is displayed, its height will be equal to the previous one if the same font and the same size are used. But the width may vary. Therefore, you can select a width, that will erase only the area intended for the text (without affecting the neighboring ones, while ensuring that the previous text is erased), and pass it to the method.


The panel class is designed to display data in tabular format. For the convenience of calculating the coordinates of the data displayed on the panel and the visual design of the panel, two methods are provided that calculate the coordinates of table cells and draw name plates (if necessary) on the panel background.

  • The first method calculates the coordinates of table cells based on the data passed to it: the initial X and Y coordinates of the table in the panel, the number of rows, columns, row height and column width. We can also specify a custom own color for the table grid lines and the attribute of "alternating" rows.
  • The second method calculates the sizes of rows and columns automatically depending on their number and the width of the table indentation from the edges of the panel. In the second method, we can also specify our own color for the table grid lines and the attribute of "alternating" rows.


The method that draws a background grid based on the given parameters:

//+------------------------------------------------------------------+
//| Draw the background grid                                         |
//+------------------------------------------------------------------+
void CDashboard::DrawGrid(const uint x,const uint y,const uint rows,const uint columns,const uint row_size,const uint col_size,
                          const color line_color=clrNONE,bool alternating_color=true)
  {
//--- If the panel is collapsed, leave
   if(this.m_minimized)
      return;
//--- Clear all lists of the tabular data object (remove cells from rows and all rows)
   this.m_table_data.Clear();
//--- Line height cannot be less than 2
   int row_h=int(row_size<2 ? 2 : row_size);
//--- Column width cannot be less than 2
   int col_w=int(col_size<2 ? 2 : col_size);
   
//--- The X1 (left) coordinate of the table cannot be less than 1 (to leave one pixel around the perimeter of the panel for the frame)
   int x1=int(x<1 ? 1 : x);
//--- Calculate the X2 coordinate (right) depending on the number of columns and their width
   int x2=x1+col_w*int(columns>0 ? columns : 1);
//--- The Y1 coordinate is located under the panel title area
   int y1=this.m_header_h+(int)y;
//--- Calculate the Y2 coordinate (bottom) depending on the number of lines and their height
   int y2=y1+row_h*int(rows>0 ? rows : 1);
   
//--- Get the color of the table grid lines, either by default or passed to the method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- If the initial X coordinate is greater than 1, draw a table frame
//--- (in case of the coordinate 1, the table frame is the panel frame)
   if(x1>1)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));
//--- In the loop by table rows,
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate the Y coordinate of the next horizontal grid line (Y coordinate of the next table row)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" line colors is passed and the line is even
      if(alternating_color && i%2==0)
        {
         //--- lighten the table background color and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         this.m_canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw a table grid horizontal line
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create a new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to the list of rows of the tabular data object
      //--- (if adding an object failed, delete the created object)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Set its Y coordinate in the created row object taking into account the offset from the panel title
      row_obj.SetY(row_y-this.m_header_h);
     }
//--- In the loop by table columns,
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate the X coordinate of the next vertical grid line (X coordinate of the next table row)
      int col_x=x1+col_w*i;
      //--- If the grid line goes beyond the panel, interrupt the loop
      if(x1==1 && col_x>=x1+m_canvas.Width()-2)
         break;
      //--- Draw a vertical line of the table grid
      this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from the table data object 
      int total=this.m_table_data.RowsTotal();
      //--- In the loop by table rows
      for(int j=0;j<total;j++)
        {
         //--- get the next row
         CTableRow *row=m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create a new table cell 
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add the created cell to the row
         //--- (if adding an object failed, delete the created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In the created cell object, set its X coordinate and the Y coordinate from the row object 
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
  }

The logic of the method and the sequence of drawing a table and creating its instance in a tabular data object are described in detail right in the code under almost every line. The data that is written to the instance of the drawn table in the tabular data object will be needed to obtain the coordinates of each cell, so that it is convenient to indicate the required coordinates when displaying the data on the panel. We just need to specify the cell number by its location in the table (Row and Column) and get the coordinates of the upper left corner of this cell on the panel.


The method drawing a background grid with automatic cell size:

//+------------------------------------------------------------------+
//| Draws the background grid with automatic cell sizing             |
//+------------------------------------------------------------------+
void CDashboard::DrawGridAutoFill(const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true)
  {
//--- If the panel is collapsed, leave
   if(this.m_minimized)
      return;
//--- X1 (left) table coordinate
   int x1=(int)border;
//--- X2 (right) table coordinate
   int x2=this.m_canvas.Width()-(int)border-1;
//--- Y1 (upper) table coordinate
   int y1=this.m_header_h+(int)border;
//--- Y2 (lower) table coordinate
   int y2=this.m_canvas.Height()-(int)border-1;

//--- Get the color of the table grid lines, either by default or passed to the method
   color clr=(line_color==clrNONE ? C'200,200,200' : line_color);
//--- If the offset from the edge of the panel is greater than zero, draw a table border,
//--- otherwise, the panel border is used as the table border
   if(border>0)
      this.m_canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha));

//--- Height of the entire table grid
   int greed_h=y2-y1;
//--- Calculate the row height depending on the table height and the number of rows
   int row_h=(int)::round((double)greed_h/(double)rows);
//--- In the loop based on the number of rows
   for(int i=0;i<(int)rows;i++)
     {
      //--- calculate the Y coordinate of the next horizontal grid line (Y coordinate of the next table row)
      int row_y=y1+row_h*i;
      //--- if the flag of "alternating" line colors is passed and the line is even
      if(alternating_color && i%2==0)
        {
         //--- lighten the table background color and draw a background rectangle
         color new_color=this.NewColor(clr,45,45,45);
         this.m_canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha));
        }
      //--- Draw a table grid horizontal line
      this.m_canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha));
      
      //--- Create a new table row object
      CTableRow *row_obj=new CTableRow(i);
      if(row_obj==NULL)
        {
         ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i);
         continue;
        }
      //--- Add it to the list of rows of the tabular data object
      //--- (if adding an object failed, delete the created object)
      if(!this.m_table_data.AddRow(row_obj))
         delete row_obj;
      //--- Set its Y coordinate in the created row object taking into account the offset from the panel title
      row_obj.SetY(row_y-this.m_header_h);
     }
     
//--- Table grid width
   int greed_w=x2-x1;
//--- Calculate the column width depending on the table width and the number of columns
   int col_w=(int)::round((double)greed_w/(double)columns);
//--- In the loop by table columns,
   for(int i=0;i<(int)columns;i++)
     {
      //--- calculate the X coordinate of the next vertical grid line (X coordinate of the next table row)
      int col_x=x1+col_w*i;
      //--- If this is not the very first vertical line, draw it
      //--- (the first vertical line is either the table frame or the panel frame)
      if(i>0)
         this.m_canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));
      
      //--- Get the number of created rows from the table data object 
      int total=this.m_table_data.RowsTotal();
      //--- In the loop by table rows
      for(int j=0;j<total;j++)
        {
         //--- get the next row
         CTableRow *row=this.m_table_data.GetRow(j);
         if(row==NULL)
            continue;
         //--- Create a new table cell 
         CTableCell *cell=new CTableCell(row.Row(),i);
         if(cell==NULL)
           {
            ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i);
            continue;
           }
         //--- Add the created cell to the row
         //--- (if adding an object failed, delete the created object)
         if(!row.AddCell(cell))
           {
            delete cell;
            continue;
           }
         //--- In the created cell object, set its X coordinate and the Y coordinate from the row object 
         cell.SetXY(col_x,row.Y());
        }
     }
//--- Update the canvas without redrawing the chart
   this.m_canvas.Update(false);
  }

This method differs from the one discussed above only in the automatic calculation of the table width and height depending on the table indentation from the edge of the panel, row height (table height / number of rows) and column width (table width / number of columns). It is also fully commented directly in the code.

You need to create (draw on the panel) these name plates after creating the panel and drawing it on the chart. Otherwise, the name plate will not be drawn, but the tabular data will all be calculated and can be used. In other words, drawing a table after displaying a panel on a chart is only necessary in case a table should be drawn on the panel.


In order to quickly restore the background of the panel and working space, we use the arrays receiving the pixels from the image of the working space and panel before the panel is collapsed. When expanding a panel, instead of re-drawing everything previously drawn on it, the background of the panel and working space are simply restored from arrays of pixels. This is much more practical than saving everything that was drawn on the canvas in order to redraw it later.

There are two methods for the panel and working space - for saving an image to a pixel array and for restoring an image from a pixel array.


The method that saves the working space to the array of pixels:

//+------------------------------------------------------------------+
//| Save the working space to the array of pixels                    |
//+------------------------------------------------------------------+
void CDashboard::SaveWorkspace(void)
  {
//--- Calculate the required size of the array (width * height of the working space)
   uint size=this.m_workspace.Width()*this.m_workspace.Height();
//--- If the size of the array is not equal to the calculated one, change it
   if(this.m_array_wpx.Size()!=size)
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_wpx,size)!=size)
        {
         ::PrintFormat("%s: ArrayResize failed. Error %lu",(string)__FUNCTION__,::GetLastError());
         return;
        }
     }
   uint n=0;
//--- In the loop along the height of the working space (pixel Y coordinate)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- in the loop by the working space width (pixel X coordinate)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- calculate the pixel index in the receiving array
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- copy pixel to the receiving array from the working space X and Y
         this.m_array_wpx[n]=this.m_workspace.PixelGet(x,y);
        }
  }

Go through each pixel of each image line in two nested loops and copy them into the receiving array.


The method that restores the working space from the array of pixels:

//+------------------------------------------------------------------+
//| Restore the working space from the array of pixels               |
//+------------------------------------------------------------------+
void CDashboard::RestoreWorkspace(void)
  {
//--- Exit if the array is empty
   if(this.m_array_wpx.Size()==0)
      return;
   uint n=0;
//--- In the loop along the height of the working space (pixel Y coordinate)
   for(int y=0;y<this.m_workspace.Height();y++)
      //--- in the loop by the working space width (pixel X coordinate)
      for(int x=0;x<this.m_workspace.Width();x++)
        {
         //--- calculate the pixel index in the array
         n=this.m_workspace.Width()*y+x;
         if(n>this.m_array_wpx.Size()-1)
            break;
         //--- copy the pixel from the array to the X and Y coordinates of the working space
         this.m_workspace.PixelSet(x,y,this.m_array_wpx[n]);
        }
  }

In two nested loops, we calculate the index of each pixel of each image line in the array and copy them from the array to the X and Y coordinates of the image.


The method that saves the panel background to the array of pixels:

//+------------------------------------------------------------------+
//| Save the panel background to the pixel array                     |
//+------------------------------------------------------------------+
void CDashboard::SaveBackground(void)
  {
//--- Calculate the required size of the array (panel width * height)
   uint size=this.m_canvas.Width()*this.m_canvas.Height();
//--- If the size of the array is not equal to the calculated one, change it
   if(this.m_array_ppx.Size()!=size)
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_ppx,size)!=size)
        {
         ::PrintFormat("%s: ArrayResize failed. Error %lu",(string)__FUNCTION__,::GetLastError());
         return;
        }
     }
   uint n=0;
//--- In the loop by the panel height (pixel Y coordinate)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- in the loop by the panel width (pixel X coordinate)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- calculate the pixel index in the receiving array
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- copy pixel to the receiving array from the panel X and Y
         this.m_array_ppx[n]=this.m_canvas.PixelGet(x,y);
        }
  }


The method restoring the panel background from the array of pixels:

//+------------------------------------------------------------------+
//| Restore the panel background from the array of pixels            |
//+------------------------------------------------------------------+
void CDashboard::RestoreBackground(void)
  {
//--- Exit if the array is empty
   if(this.m_array_ppx.Size()==0)
      return;
   uint n=0;
//--- In the loop by the panel height (pixel Y coordinate)
   for(int y=0;y<this.m_canvas.Height();y++)
      //--- in the loop by the panel width (pixel X coordinate)
      for(int x=0;x<this.m_canvas.Width();x++)
        {
         //--- calculate the pixel index in the array
         n=this.m_canvas.Width()*y+x;
         if(n>this.m_array_ppx.Size()-1)
            break;
         //--- copy the pixel from the array to the X and Y coordinates of the panel
         this.m_canvas.PixelSet(x,y,this.m_array_ppx[n]);
        }
  }


To bring an object to the foreground, we need to perform two operations in a row: hide the object and show it immediately. Each graphical object has the OBJPROP_TIMEFRAMES property responsible for its visibility on each timeframe. To hide an object on all timeframes, we need to set this property to OBJ_NO_PERIODS. Accordingly, to display the object, we need to set the OBJPROP_TIMEFRAMES property to OBJ_ALL_PERIODS.


The method hiding the panel:

//+------------------------------------------------------------------+
//| Hide the panel                                                   |
//+------------------------------------------------------------------+
void CDashboard::Hide(const bool redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   ::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
   if(redraw)
      ::ChartRedraw(this.m_chart_id);
  }

For panel and working space objects, the OBJPROP_TIMEFRAMES property is set to OBJ_NO_PERIODS, and the chart is redrawn to immediately reflect the changes (if the corresponding flag is set).


The method that displays the panel:

//+------------------------------------------------------------------+
//| Display the panel                                                |
//+------------------------------------------------------------------+
void CDashboard::Show(const bool redraw=false)
  {
   ::ObjectSetInteger(this.m_chart_id,this.m_canvas.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
   if(!this.m_minimized)
      ::ObjectSetInteger(this.m_chart_id,this.m_workspace.ChartObjectName(),OBJPROP_TIMEFRAMES,OBJ_ALL_PERIODS);
   if(redraw)
      ::ChartRedraw(this.m_chart_id);
  }

For the panel object, the OBJPROP_TIMEFRAMES property is immediately set to OBJ_ALL_PERIODS. For the working space object, the value is set only if the panel is in the expanded state.

When the redraw flag is checked, the chart is redrawn to immediately display the changes.


The method that brings the panel to the foreground:

//+------------------------------------------------------------------+
//| Bring the panel to the foreground                                |
//+------------------------------------------------------------------+
void CDashboard::BringToTop(void)
  {
   this.Hide(false);
   this.Show(true);
  }

First, the panel and working space are hidden without redrawing the chart, then they are immediately displayed and the chart is redrawn.

For some cases, when pixel arrays cannot be stored in memory, they need to be saved to files and then loaded from there. The class contains methods for saving pixel arrays to a file and loading them from the file:

//+------------------------------------------------------------------+
//| Save the pixel array of the working space to a file              |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveWorkspace(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- If the saved array is empty, inform of that and return 'false'
   if(this.m_array_wpx.Size()==0)
     {
      ::PrintFormat("%s: Error. The workspace pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- If the array could not be saved to a file, report this and return 'false'
   if(!::FileSave(filename,this.m_array_wpx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Save the pixel array of the panel background to a file           |
//+------------------------------------------------------------------+
bool CDashboard::FileSaveBackground(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- If the saved array is empty, inform of that and return 'false'
   if(this.m_array_ppx.Size()==0)
     {
      ::PrintFormat("%s: Error. The background pixel array is empty.",__FUNCTION__);
      return false;
     }
//--- If the array could not be saved to a file, report this and return 'false'
   if(!::FileSave(filename,this.m_array_ppx))
     {
      ::PrintFormat("%s: FileSave '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Upload the array of working space pixels from a file             |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadWorkspace(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\workspace.bin";
//--- If failed to upload data from the file into the array, report this and return 'false'
   if(::FileLoad(filename,this.m_array_wpx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }
//+------------------------------------------------------------------+
//| Upload the array of panel background pixels from a file          |
//+------------------------------------------------------------------+
bool CDashboard::FileLoadBackground(void)
  {
//--- Define the folder and file name
   string filename=this.m_program_name+"\\Dashboard"+(string)this.m_id+"\\background.bin";
//--- If failed to upload data from the file into the array, report this and return 'false'
   if(::FileLoad(filename,this.m_array_ppx)==WRONG_VALUE)
     {
      ::PrintFormat("%s: FileLoad '%s' failed. Error %lu",__FUNCTION__,filename,::GetLastError());
      return false;
     }
//--- Successful, return 'true'
   return true;
  }

At the moment, handling these methods has not been implemented, since the need for them was discovered at the time of writing the article. Future articles about the dashboard class will likely use these methods.


Indicator with dashboard

To test the dashboard, let's create a simple indicator that draws a regular moving average. We will display the current Bid and Ask, as well as the candle data the mouse cursor is currently hovering over.

In the Indicators folder, create a new TestDashboard folder with a new custom indicator:


Next, set the parameters:


Select the first OnCalculate, OnChartEvent and OnTimer type in case of further improvements:


Select one buffer to be drawn and click Finish:


We get the following template:

//+------------------------------------------------------------------+
//|                                                TestDashboard.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot MA
#property indicator_label1  "MA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      InpPeriodMA=10;
input int      InpMethodMA=0;
input int      InpPriceMA=0;
input int      InpPanelX=20;
input int      InpPanelY=20;
input int      InpUniqID=0;
//--- indicator buffers
double         MABuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,MABuffer,INDICATOR_DATA);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   
  }
//+------------------------------------------------------------------+

Let's save the panel object class file to the created indicator folder, so that the included panel class file is in the same folder as the indicator. After finalizing the panel class, its final version can be placed in the Include folder of the file sandbox of the trading terminal to be used in custom programs.

Let's adjust the created indicator template. Include the panel object class, initialize the inputs with more understandable initial values, fix the name of the buffer being drawn and declare the global variables:

//+------------------------------------------------------------------+
//|                                                TestDashboard.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot MA
#property indicator_label1  "MA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- includes
#include "Dashboard.mqh"
//--- input variables
input int                  InpPeriodMA =  10;            /* MA Period   */ // Moving average calculation period
input ENUM_MA_METHOD       InpMethodMA =  MODE_SMA;      /* MA Method   */ // Moving average calculation method
input ENUM_APPLIED_PRICE   InpPriceMA  =  PRICE_CLOSE;   /* MA Price    */ // Moving average calculation price
input int                  InpPanelX   =  20;            /* Dashboard X */ // Panel X coordinate
input int                  InpPanelY   =  20;            /* Dashboard Y */ // Panel Y coordinate
input int                  InpUniqID   =  0;             /* Unique ID   */ // Unique ID for the panel object
//--- indicator buffers
double         BufferMA[];
//--- global variables
CDashboard    *dashboard=NULL;
int            handle_ma;        // Moving Average indicator handle
int            period_ma;        // Moving Average calculation period
int            mouse_bar_index;  // Index of the bar the data is taken from
string         plot_label;       // Name of the graphical indicator series displayed in DataWindow
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

In the OnInit() handler, create the handle of the Moving Average standard indicator, set the parameters of the indicator and the buffer to be drawn. Since the indicator is calculated from the beginning of the history to the current data, we will set the indexing of the indicator buffer as in the timeseries. Create the dashboard object in the same handler. Immediately after creating the object, display it and draw the table grid. Next, send the tabular data to the journal:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,BufferMA,INDICATOR_DATA);

//--- Create indicator handle
   period_ma=(InpPeriodMA<1 ? 1 : InpPeriodMA);
   ResetLastError();
   handle_ma=iMA(Symbol(),PERIOD_CURRENT,period_ma,0,InpMethodMA,InpPriceMA);
   if(handle_ma==INVALID_HANDLE)
     {
      PrintFormat("%s Failed to create MA indicator handle. Error %lu",__FUNCTION__,GetLastError());
      return INIT_FAILED;
     }
//--- Set the indicator parameters
   IndicatorSetInteger(INDICATOR_DIGITS,Digits());
   IndicatorSetString(INDICATOR_SHORTNAME,"Test Dashboard");
//--- Set the parameters of the buffer being drawn
   plot_label="MA("+(string)period_ma+","+StringSubstr(EnumToString(Period()),7)+")";
   PlotIndexSetString(0,PLOT_LABEL,plot_label);
   ArraySetAsSeries(BufferMA,true);

//--- Create the panel object
   dashboard=new CDashboard(InpUniqID,InpPanelX,InpPanelY,200,250);
   if(dashboard==NULL)
     {
      Print("Error. Failed to create dashboard object");
      return INIT_FAILED;
     }
//--- Display the panel with the "Symbol, Timeframe description" header text
   dashboard.View(Symbol()+", "+StringSubstr(EnumToString(Period()),7));
//--- Draw the name plate on the panel background
   dashboard.DrawGridAutoFill(2,12,2);
   //dashboard.DrawGrid(2,1,12,2,19,97);
//--- Display tabular data in the journal
   dashboard.GridPrint(2);
//--- Initialize the variable with the index of the mouse cursor bar
   mouse_bar_index=0;
//--- Successful initialization
   return(INIT_SUCCEEDED);
  }

All logic here is commented in the code. The table is created with automatic calculation of row and column sizes. The creation of a simple table is commented out in the code. You can comment out the automatic name plate, uncomment the simple one and recompile the indicator. The difference will be insignificant considering the current table parameters.

In the OnDeinit() handler, remove the panel, free the indicator handle and erase the chart comments:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- If the panel object exists, delete it
   if(dashboard!=NULL)
      delete dashboard;
//--- Release the handle of the MA indicator
   ResetLastError();
   if(!IndicatorRelease(handle_ma))
      PrintFormat("%s: IndicatorRelease failed. Error %ld",__FUNCTION__,GetLastError());
//--- Delete all comments
   Comment("");
  }

In the OnCalculate() handler, all predefined timeseries arrays will have indexation like the timeseries, so that they match the indexing of the drawing buffer. Everything else is described in the handler code comments:

//+------------------------------------------------------------------+
//| 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[])
  {
//--- Set indexing for the arrays as in a timeseries
   ArraySetAsSeries(time,true);
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Check for the minimum number of bars for calculation
   if(rates_total<period_ma) return 0;

//--- Check and calculate the number of calculated bars
   int limit=rates_total-prev_calculated;
//--- If 'limit' is 0, then only the current bar is calculated
//--- If 'limit' is 1 (opening a new bar), then two bars are calculated - the current newly opened one and the previous one
//--- If 'limit' is more than 1, then this is either the first launch of the indicator, or some changes in history - the indicator is completely recalculated
   if(limit>1)
     {
      limit=rates_total-period_ma-1;
      ArrayInitialize(BufferMA,EMPTY_VALUE);
     }
     
//--- Calculate the amount of data copied from the indicator handle to the drawing buffer
   int count=(limit>1 ? rates_total : 1),copied=0;
//--- Prepare data (receive data to the moving average buffer by handle)
   copied=CopyBuffer(handle_ma,0,0,count,BufferMA);
   if(copied!=count) return 0;

//--- Loop of indicator calculation based on the moving average data
   for(int i=limit; i>=0 && !IsStopped(); i--)
     {
      // Here we calculate a certain indicator based on the standard Moving Average data
     }

//--- Receive price and timeseries data and display it on the panel
//--- At the first launch, we display the data of the current bar on the panel
   static bool first=true;
   if(first)
     {
      DrawData(0,TimeCurrent());
      first=false;
     }

//--- Declare the price structure and get the current prices
   MqlTick  tick={0};
   if(!SymbolInfoTick(Symbol(),tick))
      return 0;

//--- If the cursor is on the current bar, display the data of the current bar on the panel
   if(mouse_bar_index==0)
      DrawData(0,time[0]);
//--- Otherwise, display only the Bid and Ask prices on the panel (we update the prices on the panel at each tick)
   else
     {
      dashboard.DrawText("Bid",dashboard.CellX(0,0)+2,dashboard.CellY(0,0)+2); dashboard.DrawText(DoubleToString(tick.bid,Digits()),dashboard.CellX(0,1)+2,dashboard.CellY(0,1)+2,90);
      dashboard.DrawText("Ask",dashboard.CellX(1,0)+2,dashboard.CellY(1,0)+2); dashboard.DrawText(DoubleToString(tick.ask,Digits()),dashboard.CellX(1,1)+2,dashboard.CellY(1,1)+2,90);
     }
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }

In the OnChartEvent() handler, the indicator first calls the OnChartEvent panel handler, then handle the necessary events:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Call the panel event handler
   dashboard.OnChartEvent(id,lparam,dparam,sparam);

//--- If the cursor moves or a click is made on the chart
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
     {
      //--- Declare the variables to record time and price coordinates in them
      datetime time=0;
      double price=0;
      int wnd=0;
      //--- If the cursor coordinates are converted to date and time
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- write the bar index where the cursor is located to a global variable
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         //--- Display the bar data under the cursor on the panel 
         DrawData(mouse_bar_index,time);
        }
     }

//--- If we received a custom event, display the appropriate message in the journal
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- Here we can implement handling a click on the close button on the panel
      PrintFormat("%s: Event id=%ld, object id (lparam): %lu, event message (sparam): %s",__FUNCTION__,id,lparam,sparam);
     }
  }

Here, when receiving an event from the panel when the close button is clicked, and if it is necessary to react to such an event, we need to register its processing. The decision about the program behavior at this event lies with the program developer.


The function that displays the current prices and bar data by index:

//+------------------------------------------------------------------+
//| Display data from the specified timeseries index to the panel    |
//+------------------------------------------------------------------+
void DrawData(const int index,const datetime time)
  {
//--- Declare the variables to receive data in them
   MqlTick  tick={0};
   MqlRates rates[1];
//--- Exit if unable to get the current prices
   if(!SymbolInfoTick(Symbol(),tick))
      return;
//--- Exit if unable to get the bar data by the specified index
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1)
      return;
//--- Display the current prices and data of the specified bar on the panel
   dashboard.DrawText("Bid",        dashboard.CellX(0,0)+2,   dashboard.CellY(0,0)+2);  dashboard.DrawText(DoubleToString(tick.bid,Digits()),       dashboard.CellX(0,1)+2,   dashboard.CellY(0,1)+2,90);
   dashboard.DrawText("Ask",        dashboard.CellX(1,0)+2,   dashboard.CellY(1,0)+2);  dashboard.DrawText(DoubleToString(tick.ask,Digits()),       dashboard.CellX(1,1)+2,   dashboard.CellY(1,1)+2,90);
   dashboard.DrawText("Date",       dashboard.CellX(2,0)+2,   dashboard.CellY(2,0)+2);  dashboard.DrawText(TimeToString(rates[0].time,TIME_DATE),   dashboard.CellX(2,1)+2,   dashboard.CellY(2,1)+2,90);
   dashboard.DrawText("Time",       dashboard.CellX(3,0)+2,   dashboard.CellY(3,0)+2);  dashboard.DrawText(TimeToString(rates[0].time,TIME_MINUTES),dashboard.CellX(3,1)+2,   dashboard.CellY(3,1)+2,90);
   
   dashboard.DrawText("Open",       dashboard.CellX(4,0)+2,   dashboard.CellY(4,0)+2);  dashboard.DrawText(DoubleToString(rates[0].open,Digits()),  dashboard.CellX(4,1)+2,   dashboard.CellY(4,1)+2,90);
   dashboard.DrawText("High",       dashboard.CellX(5,0)+2,   dashboard.CellY(5,0)+2);  dashboard.DrawText(DoubleToString(rates[0].high,Digits()),  dashboard.CellX(5,1)+2,   dashboard.CellY(5,1)+2,90);
   dashboard.DrawText("Low",        dashboard.CellX(6,0)+2,   dashboard.CellY(6,0)+2);  dashboard.DrawText(DoubleToString(rates[0].low,Digits()),   dashboard.CellX(6,1)+2,   dashboard.CellY(6,1)+2,90);
   dashboard.DrawText("Close",      dashboard.CellX(7,0)+2,   dashboard.CellY(7,0)+2);  dashboard.DrawText(DoubleToString(rates[0].close,Digits()), dashboard.CellX(7,1)+2,   dashboard.CellY(7,1)+2,90);
   
   dashboard.DrawText("Volume",     dashboard.CellX(8,0)+2,   dashboard.CellY(8,0)+2);  dashboard.DrawText((string)rates[0].real_volume,            dashboard.CellX(8,1)+2,   dashboard.CellY(8,1)+2,90);
   dashboard.DrawText("Tick Volume",dashboard.CellX(9,0)+2,   dashboard.CellY(9,0)+2);  dashboard.DrawText((string)rates[0].tick_volume,            dashboard.CellX(9,1)+2,   dashboard.CellY(9,1)+2,90);
   dashboard.DrawText("Spread",     dashboard.CellX(10,0)+2,  dashboard.CellY(10,0)+2); dashboard.DrawText((string)rates[0].spread,                 dashboard.CellX(10,1)+2,  dashboard.CellY(10,1)+2,90);
   dashboard.DrawText(plot_label,   dashboard.CellX(11,0)+2,  dashboard.CellY(11,0)+2); dashboard.DrawText(DoubleToString(BufferMA[index],Digits()),dashboard.CellX(11,1)+2,  dashboard.CellY(11,1)+2,90);
//--- Redraw the chart to immediately display all changes on the panel
   ChartRedraw(ChartID());
  }

If we pay attention to the DrawText method of the panel, class

void CDashboard::DrawText(const string text,const int x,const int y,const int width=WRONG_VALUE,const int height=WRONG_VALUE)

we can see that the X and Y coordinates are passed to the method after the text. This is what we get from tabular data, indicating the location of the table cell by its row and column number.

For example, Bid and Ask prices are displayed on the panel at the "address" of the table cells for Bid 0.0 ('Bid' text) and 0.1 (Bid price):

dashboard.DrawText("Bid"dashboard.CellX(0,0)+2, dashboard.CellY(0,0)+2);  dashboard.DrawText(DoubleToString(tick.bid,Digits()), dashboard.CellX(0,1)+2, dashboard.CellY(0,1)+2,90);

Cell values are taken here

for the "Bid" text:

  • CellX(0,0) — cell in the zero row and zero column — X coordinate value,
  • CellY0,0) — cell in the zero row and zero column — Y coordinate value.

for the Bid price value:

  • CellX(0,1) — cell in the zero row and first column — X coordinate value,
  • CellY0,1) — cell in the zero row and first column — Y coordinate value.

The value of 90 for the displayed text width in the second cell indicates the case when the current text may be less wide than the previous one. Accordingly, the previous text will not be completely erased, and the two texts will overlap each other. Therefore, here we explicitly indicate the width of the displayed inscription, which is guaranteed to erase the previous drawn text, but will not erase the adjacent data, since the table field where the text is written is wider than 90 pixels.

Thus, for each table cell, we can get its coordinates in the panel and display text in them. Since the coordinates are indicated for the points of intersection of the table grid lines, two pixels are added to the coordinates in X and Y to align the text inside the table cells.

After compiling the indicator and launching it on the chart, the data from the table created and drawn on the panel will be displayed to the journal:

Table: Rows: 12, Columns: 2
  Row   0    Column   0      Cell X:  2      Cell Y:  2   
  Row   0    Column   1      Cell X:  100    Cell Y:  2   
  Row   1    Column   0      Cell X:  2      Cell Y:  21  
  Row   1    Column   1      Cell X:  100    Cell Y:  21  
  Row   2    Column   0      Cell X:  2      Cell Y:  40  
  Row   2    Column   1      Cell X:  100    Cell Y:  40  
  Row   3    Column   0      Cell X:  2      Cell Y:  59  
  Row   3    Column   1      Cell X:  100    Cell Y:  59  
  Row   4    Column   0      Cell X:  2      Cell Y:  78  
  Row   4    Column   1      Cell X:  100    Cell Y:  78  
  Row   5    Column   0      Cell X:  2      Cell Y:  97  
  Row   5    Column   1      Cell X:  100    Cell Y:  97  
  Row   6    Column   0      Cell X:  2      Cell Y:  116 
  Row   6    Column   1      Cell X:  100    Cell Y:  116 
  Row   7    Column   0      Cell X:  2      Cell Y:  135 
  Row   7    Column   1      Cell X:  100    Cell Y:  135 
  Row   8    Column   0      Cell X:  2      Cell Y:  154 
  Row   8    Column   1      Cell X:  100    Cell Y:  154 
  Row   9    Column   0      Cell X:  2      Cell Y:  173 
  Row   9    Column   1      Cell X:  100    Cell Y:  173 
  Row   10   Column   0      Cell X:  2      Cell Y:  192 
  Row   10   Column   1      Cell X:  100    Cell Y:  192 
  Row   11   Column   0      Cell X:  2      Cell Y:  211 
  Row   11   Column   1      Cell X:  100    Cell Y:  211 


If we launch two indicators with a panel on the same chart, specifying different values for the panel’s unique ID, they will work independently of each other:


Here we can see that although the panels work separately - each indicator has the panel of its own. However, there is also a conflict: when the panels are moved, the chart also attempts to move. This happens because one panel, which we move, disables moving the chart, while the second one turns it on upon seeing that the cursor is outside of it.

To get rid of this behavior, the simplest thing we can do is to arrange a semaphore in the global terminal variables, in which the ID of the active panel will be written. The rest will not interfere in managing the chart upon seeing something other than their ID there.

If we run the indicator in the tester visual mode and try to move the panel, then it will move across the screen with some difficulty. At the same time, the data can be obtained from the bars of the tested chart - when you click on a bar, its data is displayed on the panel. Also, the right mouse button (holding it and moving the cursor along the chart) helps to indicate the panel where the cursor is currently located and display the data, or grab the panel by the header area and move it to the desired location. Unfortunately, we have to resort to such tricks in the tester visual mode due to the incomplete implementation of handling events.


Conclusion

Today we have created a small panel that may help in developing custom strategies using indicators. In subsequent articles, we will look at including indicators and handling their data in EAs for all standard indicators.


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

Attached files |
Dashboard.mqh (195.26 KB)
TestDashboard.mq5 (24.81 KB)
Last comments | Go to discussion (6)
Artyom Trishkin
Artyom Trishkin | 12 Jan 2024 at 02:31

Arpit T # :

//--- Display the text to the space cleared of previous text and update the working space without redrawing the screen
   this .m_workspace. TextOut (x,y,text,:: ColorToARGB (clr, this .m_alpha ));
   this .m_workspace.Update( false );

The highlighted color is the transparency of the panel itself, not the text. For text, it is better to set the transparency (or rather, opacity) to the default value of 255 - completely opaque text. But you can “play around” with the opacity value by entering regular numeric values from 0 to 255 instead of this.m_alpha. 0 is a completely transparent color, 255 is a completely opaque color.

[Deleted] | 14 Jan 2024 at 21:37
Artyom Trishkin #:

The highlighted color is the transparency of the panel itself, not the text. For text, it is better to set the transparency (or rather, opacity) to the default value of 255 - completely opaque text. But you can “play around” with the opacity value by entering regular numeric values from 0 to 255 instead of this.m_alpha. 0 is a completely transparent color, 255 is a completely opaque color.

Thank you all is good now


void DrawData(int cell, string title, string data, color clr ) {
   dashboard.DrawText(title,  dashboard.CellX(cell,0)+2,   dashboard.CellY(cell,0)+2, clr);
   dashboard.DrawText(data,   dashboard.CellX(cell,1)+2,   dashboard.CellY(cell,1)+2,90, clr);
   ChartRedraw(ChartID());
}

the highlighted code was making problem, I removed and all good.

VIKRAM SINGH
VIKRAM SINGH | 14 Jan 2024 at 21:43
well done work dark is nice
[Deleted] | 14 Jan 2024 at 21:50
VIKRAM SINGH #:
well done work dark is nice

Thanks

Here is the modified constructor for dark look in dashboard.mqh if you wish to use that theme

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CDashboard::CDashboard(const uint id,const int x,const int y, const int w,const int h,const int wnd=-1) : 
                        m_id(id),
                        m_chart_id(::ChartID()),
                        m_program_type((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
                        m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)),
                        m_wnd(wnd==-1 ? GetSubWindow() : wnd),
                        m_chart_w((int)::ChartGetInteger(m_chart_id,CHART_WIDTH_IN_PIXELS,m_wnd)),
                        m_chart_h((int)::ChartGetInteger(m_chart_id,CHART_HEIGHT_IN_PIXELS,m_wnd)),
                        m_mouse_state(MOUSE_STATE_NOT_PRESSED),
                        m_x(x),
                        m_y(::ChartGetInteger(m_chart_id,CHART_SHOW_ONE_CLICK) ? (y<79 ? 79 : y) : y),
                        m_w(w),
                        m_h(h),
                        m_x_dock(m_x),
                        m_y_dock(m_y),
                        m_header(true),
                        m_butt_close(true),
                        m_butt_minimize(true),
                        m_butt_pin(true),
                        m_header_h(18),
                        
                        //--- Panel header implementation
                        m_header_alpha(255),
                        m_header_alpha_c(m_header_alpha),
                        m_header_back_color(clrBlack),
                        m_header_back_color_c(m_header_back_color),
                        m_header_fore_color(clrSnow),
                        m_header_fore_color_c(m_header_fore_color),
                        m_header_border_color(clrSnow),
                        m_header_border_color_c(m_header_border_color),
                        m_title("Dashboard"),
                        m_title_font("Calibri"),
                        m_title_font_size(-100),
                        
                        //--- close button
                        m_butt_close_back_color(clrBlack),
                        m_butt_close_back_color_c(m_butt_close_back_color),
                        m_butt_close_fore_color(clrSnow),
                        m_butt_close_fore_color_c(m_butt_close_fore_color),
                        
                        //--- collapse/expand button
                        m_butt_min_back_color(clrBlack),
                        m_butt_min_back_color_c(m_butt_min_back_color),
                        m_butt_min_fore_color(clrSnow),
                        m_butt_min_fore_color_c(m_butt_min_fore_color),
                        
                        //--- pin button
                        m_butt_pin_back_color(clrBlack),
                        m_butt_pin_back_color_c(m_butt_min_back_color),
                        m_butt_pin_fore_color(clrSnow),
                        m_butt_pin_fore_color_c(m_butt_min_fore_color),
                        
                        //--- Panel implementation
                        m_alpha(255),
                        m_alpha_c(m_alpha),
                        m_fore_alpha(255),
                        m_fore_alpha_c(m_fore_alpha),
                        m_back_color(clrBlack),
                        m_back_color_c(m_back_color),
                        m_fore_color(clrSnow),
                        m_fore_color_c(m_fore_color),
                        m_border_color(clrSnow),
                        m_border_color_c(m_border_color),
                        m_font("Calibri"),
                        m_font_size(-100),
                        
                        m_minimized(false),
                        m_movable(true)
  {
//--- Set the permission for the chart to send messages about events of moving and pressing mouse buttons,
//--- mouse scroll events, as well as graphical object creation/deletion
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_MOUSE_WHEEL,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_CREATE,true);
   ::ChartSetInteger(this.m_chart_id,CHART_EVENT_OBJECT_DELETE,true);
   
//--- Set the names of global terminal variables to store panel coordinates, collapsed/expanded state and pinning
   this.m_name_gv_x=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_X";
   this.m_name_gv_y=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Y";
   this.m_name_gv_m=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Minimize";
   this.m_name_gv_u=this.m_program_name+"_id_"+(string)this.m_id+"_"+(string)this.m_chart_id+"_Unpin";
   
//--- If a global variable does not exist, create it and write the current value,
//--- otherwise - read the value from the terminal global variable into it
//--- X coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_x))
      ::GlobalVariableSet(this.m_name_gv_x,this.m_x);
   else
      this.m_x=(int)::GlobalVariableGet(this.m_name_gv_x);
//--- Y coordinate
   if(!::GlobalVariableCheck(this.m_name_gv_y))
      ::GlobalVariableSet(this.m_name_gv_y,this.m_y);
   else
      this.m_y=(int)::GlobalVariableGet(this.m_name_gv_y);
//--- Collapsed/expanded
   if(!::GlobalVariableCheck(this.m_name_gv_m))
      ::GlobalVariableSet(this.m_name_gv_m,this.m_minimized);
   else
      this.m_minimized=(int)::GlobalVariableGet(this.m_name_gv_m);
//--- Collapsed/not collapsed
   if(!::GlobalVariableCheck(this.m_name_gv_u))
      ::GlobalVariableSet(this.m_name_gv_u,this.m_movable);
   else
      this.m_movable=(int)::GlobalVariableGet(this.m_name_gv_u);

//--- Set the flags for the size of the panel exceeding the size of the chart window
   this.m_higher_wnd=this.HigherWnd();
   this.m_wider_wnd=this.WiderWnd();

//--- If the panel graphical resource is created,
   if(this.m_canvas.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"P"+(string)this.m_id,this.m_x,this.m_y,this.m_w,this.m_h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the canvas font and fill the canvas with the transparent color
      this.m_canvas.FontSet(this.m_title_font,this.m_title_font_size,FW_BOLD);
      this.m_canvas.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for canvas failed",(string)__FUNCTION__);

//--- If a working space of a graphical resource is created,
   if(this.m_workspace.CreateBitmapLabel(this.m_chart_id,this.m_wnd,"W"+(string)this.m_id,this.m_x+1,this.m_y+this.m_header_h,this.m_w-2,this.m_h-this.m_header_h-1,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- set the font for the working area and fill it with the transparent color
      this.m_workspace.FontSet(this.m_font,this.m_font_size);
      this.m_workspace.Erase(0x00FFFFFF);
     }
//--- otherwise - report unsuccessful object creation to the journal
   else
      ::PrintFormat("%s: Error. CreateBitmapLabel for workspace failed",(string)__FUNCTION__);
  }
Lukka Mayer
Lukka Mayer | 28 Jul 2024 at 01:30
Nice, thank you for this code. :)
Building Your First Glass-box Model Using Python And MQL5 Building Your First Glass-box Model Using Python And MQL5
Machine learning models are difficult to interpret and understanding why our models deviate from our expectations is critical if we want to gain any value from using such advanced techniques. Without comprehensive insight into the inner workings of our model, we might fail to spot bugs that are corrupting our model's performance, we may waste time over engineering features that aren't predictive and in the long run we risk underutilizing the power of these models. Fortunately, there is a sophisticated and well maintained all in one solution that allows us to see exactly what our model is doing underneath the hood.
Neural networks made easy (Part 56): Using nuclear norm to drive research Neural networks made easy (Part 56): Using nuclear norm to drive research
The study of the environment in reinforcement learning is a pressing problem. We have already looked at some approaches previously. In this article, we will have a look at yet another method based on maximizing the nuclear norm. It allows agents to identify environmental states with a high degree of novelty and diversity.
Implementation of the Augmented Dickey Fuller test in MQL5 Implementation of the Augmented Dickey Fuller test in MQL5
In this article we demonstrate the implementation of the Augmented Dickey-Fuller test, and apply it to conduct cointegration tests using the Engle-Granger method.
Design Patterns in software development and MQL5 (Part 4): Behavioral Patterns 2 Design Patterns in software development and MQL5 (Part 4): Behavioral Patterns 2
In this article, we will complete our series about the Design Patterns topic, we mentioned that there are three types of design patterns creational, structural, and behavioral. We will complete the remaining patterns of the behavioral type which can help set the method of interaction between objects in a way that makes our code clean.