Graphical interfaces X: New features for the Rendered table (build 9)

22 March 2017, 16:06
Anatoli Kazharski
0
5 343

Contents


Introduction

The first article Graphical Interfaces I: Preparation of the Library Structure (Chapter 1) considers in detail what this library is for. You will find a list of articles with links at the end of each chapter. There, you can also download a complete version of the library at the current stage of development. The files must be located in the same directories as in the archive.

Until now, the CTable was the most advanced type of tables in the developed library. This table is assembled from edit boxes of the OBJ_EDIT type, and its further development becomes problematic. For example, it is difficult to implement manual resizing of columns by dragging the header borders, as it is not possible to manage the visible areas of individual graphical objects of the table. The limit has been reached here.

Therefore, at the current stage of the library development, it is more reasonable to move on to the development of the rendered tables of the CCanvasTable type. Information on the previous versions and updates of the rendered table can be found here:

The screenshot shows the appearance of the latest version of the rendered table. As you can see, at the moment it is completely lifeless. It is simply a grid of table cells with data in them. The alignment method can be specified for the cells. Except for the scrollbars and the automatic adjustment of the sizes to the form, this table has no other interactivity.

 Fig. 1. The previous version of the rendered table.

Fig. 1. The previous version of the rendered table.

To rectify the situation, let us supplement the rendered table with new features. The following features will be included in the current update:

  • Formatting in Zebra style
  • Selecting a table row and deselecting it when clicked again
  • Added headers for columns with the ability to change the color when hovered by mouse and clicked
  • Automatic adjustment of text to the column width in case there is not enough space in the cell
  • Ability to change the header width of each column by dragging its border

Formatting in Zebra style

Formatting in Zebra style has been added to the CTable table in one of the recent articles. It helps to better navigate the table if it contains a lot of cells. Let us implement this mode in the rendered table as well.

Use the CCanvasTable::IsZebraFormatRows() method to enable the mode. It will be passed the second color of the style, while the general cell color will serve as the first color.

//+------------------------------------------------------------------+
//| Class for creating a rendered table                              |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
  {
private:
   //--- Mode of formatting in Zebra style
   color             m_is_zebra_format_rows;
   //---
public:
   //--- Formatting in Zebra style
   void              IsZebraFormatRows(const color clr)   { m_is_zebra_format_rows=clr;      }
      };

Methods for visualizing this style differ in the tables of different types. In the case of CCanvasTable, in the normal mode the table background (canvas for drawing) is completely filled with a common cell color. When the Zebra mode is activated, a cycle starts. Each of its iterations calculates the coordinates for every row, and the areas are colored in two colors alternately. This is done by the FillRectangle() method, which is used for drawing filled rectangles

class CCanvasTable : public CElement
  {
public:
   //--- Draws the background of the table rows
   void              DrawRows(void);
  };
//+------------------------------------------------------------------+
//| Draws the background of the table rows                           |
//+------------------------------------------------------------------+
void CCanvasTable::DrawRows(void)
  {
//--- If the mode of formatting in Zebra style is disabled
   if(m_is_zebra_format_rows==clrNONE)
     {
      //--- Fill the canvas with one color
      m_table.Erase(::ColorToARGB(m_cell_color));
      return;
     }
//--- Coordinates of the headers
   int x1=0,x2=m_table_x_size;
   int y1=0,y2=0;
//--- Formatting in Zebra style
   for(int r=0; r<m_rows_total; r++)
     {
      //--- Calculating coordinates
      y1=(r*m_cell_y_size)-r;
      y2=y1+m_cell_y_size;
      //--- Row color
      uint clr=::ColorToARGB((r%2!=0)? m_is_zebra_format_rows : m_cell_color);
      //--- Draw the row background
      m_table.FillRectangle(x1,y1,x2,y2,clr);
     }
      }

The row colors can be set to your liking. As a result, the rendered table in Zebra mode will look as follows:

 Fig. 2. Rendered table in the mode of formatting in Zebra style.

Fig. 2. Rendered table in the mode of formatting in Zebra style. 

 


Selecting and deselecting table rows

Selecting rows will require additional fields and methods for storing and setting:

  • The color of the background and text of the selected row
  • Index and text

class CCanvasTable : public CElement
  {
private:
   //--- Color of (1) the background and (2) selected row text
   color             m_selected_row_color;
   color             m_selected_row_text_color;
   //--- (1) Index and (2) text of the selected row
   int               m_selected_item;
   string            m_selected_item_text;
   //---
public:
   //--- Returns the (1) index and (2) text of the selected row in the table
   int               SelectedItem(void)             const { return(m_selected_item);         }
   string            SelectedItemText(void)         const { return(m_selected_item_text);    }
   //---
private:
   //--- Draws the background of the table rows
   void              DrawRows(void);
      };

The selectable row mode can be enabled/disabled by the CCanvasTable::SelectableRow() method:

class CCanvasTable : public CElement
  {
private:
   //--- Selectable row mode
   bool              m_selectable_row;
   //---
public:
   //--- Row selection
   void              SelectableRow(const bool flag)       { m_selectable_row=flag;           }
      };

A separate method for drawing a user-defined area is required in order to select a row. Code of the CCanvasTable::DrawSelectedRow() method is presented below. It calculates the coordinates for the selected area on the canvas, which are then used to draw the filled rectangle

class CCanvasTable : public CElement
  {
private:
   //--- Draws a selected row
   void              DrawSelectedRow(void);
  };
//+------------------------------------------------------------------+
//| Draws a selected row                                             |
//+------------------------------------------------------------------+
void CCanvasTable::DrawSelectedRow(void)
  {
//--- Set the initial coordinates for checking the condition
   int y_offset=(m_selected_item*m_cell_y_size)-m_selected_item;
//--- Coordinates
   int x1=0,x2=0,y1=0,y2=0;
//---
   x1=0;
   y1=y_offset;
   x2=m_table_x_size;
   y2=y_offset+m_cell_y_size-1;
//--- Draw a filled rectangle
   m_table.FillRectangle(x1,y1,x2,y2,::ColorToARGB(m_selected_row_color));
      }

An auxiliary CCanvasTable::TextColor() method is used to redraw the text, which determines the text color in the cells: 

class CCanvasTable : public CElement
  {
private:
   //--- Returns the color of the cell text
   uint              TextColor(const int row_index);
  };
//+------------------------------------------------------------------+
//| Returns the color of the cell text                               |
//+------------------------------------------------------------------+
uint CCanvasTable::TextColor(const int row_index)
  {
   uint clr=::ColorToARGB((row_index==m_selected_item)? m_selected_row_text_color : m_cell_text_color);
//--- Return the header color
   return(clr);
      }

To select a table row, it is necessary to double-click it. This will require the CCanvasTable::OnClickTable() method, which will be called in the event handler of the control by the CHARTEVENT_OBJECT_CLICK identifier.

Several checks should be passed at the start of the method. The program leaves the method if:

  • row selection mode is disabled;
  • scrollbar is active;
  • click was not on the table. 

If the checks are passed, then in order to calculate the click coordinate, it is necessary to obtain the current offset from the edge of the canvas and the Y coordinate of the mouse cursor. After that, determine the clicked row in a loop. Once the row is found, it is necessary to check if it is currently selected, and if so — deselect. If the row is selected, it is necessary to store its index and the text from the first column. The table is redrawn after the search for the row in a cycle is complete. A message is sent, which contains:

  • identifier of the ON_CLICK_LIST_ITEM event
  • identifier of the control
  • index of the selected row
  • text of the selected row. 
class CCanvasTable : public CElement
  {
private:
   //--- Handling of the press on the element
   bool              OnClickTable(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Handling clicking the control                                    |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickTable(const string clicked_object)
  {
//--- Leave, if the row selection mode is disabled
   if(!m_selectable_row)
      return(false);
//--- Leave, if the scrollbar is active
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- Leave, if it has a different object name
   if(m_table.Name()!=clicked_object)
      return(false);
//--- Get the offset along the X and Y axes
   int xoffset=(int)m_table.GetInteger(OBJPROP_XOFFSET);
   int yoffset=(int)m_table.GetInteger(OBJPROP_YOFFSET);
//--- Determine the text edit box coordinates below the mouse cursor
   int y=m_mouse.Y()-m_table.Y()+yoffset;
//--- Determine the clicked row
   for(int r=0; r<m_rows_total; r++)
     {
      //--- Set the initial coordinates for checking the condition
      int y_offset=(r*m_cell_y_size)-r;
      //--- Checking the condition along the Y axis
      bool y_pos_check=(y>=y_offset && y<y_offset+m_cell_y_size);
      //--- If the click was not on this line, go to the next
      if(!y_pos_check)
         continue;
      //--- If clicked on a selected row, deselect
      if(r==m_selected_item)
        {
         m_selected_item      =WRONG_VALUE;
         m_selected_item_text ="";
         break;
        }
      //--- Store the row index 
      m_selected_item      =r;
      m_selected_item_text =m_vcolumns[0].m_vrows[r];
      break;
     }
//--- Draw the table
   DrawTable();
//--- Send a message about it
   ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,m_selected_item_text);
   return(true);
      }

Rendered table with a selected row looks the following way:

Fig. 3. Demonstration of selecting and deselecting a row of the rendered table.

Fig. 3. Demonstration of selecting and deselecting a row of the rendered table. 

 

Headers for columns

Any table without headers looks like a blank. Headers will be drawn in this type of tables as well, but on a separate canvas. To do this, include another instance of the CRectCanvas class in the CCanvasTable class and create a separate method for creating a canvas. The code of this method will not be provided here: it is almost the same as for creating a table. The only differences are the preset sizes and the location of the object.

class CCanvasTable : public CElement
  {
private:
   //--- Objects for creating a table
   CRectCanvas       m_headers;
   //---
private:
   bool              CreateHeaders(void);
      };

Now consider the properties related to the column headers. They can be configured before creating the table.

  • Display mode of the table headers.
  • Size (height) of the headers.
  • Color of the headers' background in different states.
  • Header text color.

Fields and methods related to these properties: 

class CCanvasTable : public CElement
  {
private:
   //--- Display mode of the table headers
   bool              m_show_headers;
   //--- Size (height) of the headers
   int               m_header_y_size;
   //--- Color of the headers (background) in different states
   color             m_headers_color;
   color             m_headers_color_hover;
   color             m_headers_color_pressed;
   //--- Header text color
   color             m_headers_text_color;
   //---
public:
   //--- (1) Headers display mode, height of the (2) headers
   void              ShowHeaders(const bool flag)         { m_show_headers=flag;             }
   void              HeaderYSize(const int y_size)        { m_header_y_size=y_size;          }
   //--- (1) Background and (2) text color of the headers
   void              HeadersColor(const color clr)        { m_headers_color=clr;             }
   void              HeadersColorHover(const color clr)   { m_headers_color_hover=clr;       }
   void              HeadersColorPressed(const color clr) { m_headers_color_pressed=clr;     }
   void              HeadersTextColor(const color clr)    { m_headers_text_color=clr;        }
      };

A method is required to set the header names. In addition, an array to store these values is also required. The size of the array is equal to the number of columns and will be set in the same CCanvasTable::TableSize() method when setting the table size. 

class CCanvasTable : public CElement
  {
private:
   //--- Text of headers
   string            m_header_text[];
   //---
public:
   //--- Setting the text to the specified header
   void              SetHeaderText(const int column_index,const string value);
  };
//+------------------------------------------------------------------+
//| Fills the array of headers at the specified index                |
//+------------------------------------------------------------------+
void CCanvasTable::SetHeaderText(const uint column_index,const string value)
  {
//--- Checking for exceeding the column range
   uint csize=::ArraySize(m_vcolumns);
   if(csize<1 || column_index>=csize)
      return;
//--- Store the value into the array
   m_header_text[column_index]=value;
      }

Text alignment in the cells and headers will be performed using a common CCanvasTable::TextAlign() method. The method of alignment along the X axis in the table cells matches that of headers, and alignment along the Y axis is defined by the passed value. In this version, the header text along the Y axis will be positioned at the center — TA_VCENTER, and the cells will have the offset from the top edge of the cell adjusted — TA_TOP

class CCanvasTable : public CElement
  {
private:
   //--- Returns the text alignment mode in the specified column
   uint              TextAlign(const int column_index,const uint anchor);
  };
//+------------------------------------------------------------------+
//| Returns the text alignment mode in the specified column          |
//+------------------------------------------------------------------+
uint CCanvasTable::TextAlign(const int column_index,const uint anchor)
  {
   uint text_align=0;
//--- Text alignment for the current column
   switch(m_vcolumns[column_index].m_text_align)
     {
      case ALIGN_CENTER :
         text_align=TA_CENTER|anchor;
         break;
      case ALIGN_RIGHT :
         text_align=TA_RIGHT|anchor;
         break;
      case ALIGN_LEFT :
         text_align=TA_LEFT|anchor;
         break;
     }
//--- Return the alignment type
   return(text_align);
      }

In many tables and in the OS environment, the pointer changes when the cursor hovers the border between two headers. The screenshot below depicts such situation by the example of the table in the Toolbox window of the MetaTrader 5 trading terminal. If this newly appeared pointer is clicked, it toggles the mode of changing the column width. The background color of this column changes as well.

Fig. 4. Mouse pointer when hovering the border of the header joints.

Fig. 4. Mouse pointer when hovering the border of the header joints.

 

Let us prepare the same image for the developed library. The attachment at the end of the article contains a folder with all images for the library controls. Add new identifiers to the ENUM_MOUSE_POINTER enumeration of pointers in the Enums.mqh file for resizing along the X and Y axes: 

//+------------------------------------------------------------------+
//| Enumeration of the pointer types                                 |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_POINTER
  {
   MP_CUSTOM            =0,
   MP_X_RESIZE          =1,
   MP_Y_RESIZE          =2,
   MP_XY1_RESIZE        =3,
   MP_XY2_RESIZE        =4,
   MP_X_RESIZE_RELATIVE =5,
   MP_Y_RESIZE_RELATIVE =6,
   MP_X_SCROLL          =7,
   MP_Y_SCROLL          =8,
   MP_TEXT_SELECT       =9
      };

The corresponding additions need to be made to the CPointer class to make this pointer type available for use in the classes of the controls. 

//+------------------------------------------------------------------+
//|                                                      Pointer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Resources
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"
//+------------------------------------------------------------------+
//| Class for creating the mouse cursor                              |
//+------------------------------------------------------------------+
class CPointer : public CElement
  {
private:
   //--- Set images for the mouse cursor
   void              SetPointerBmp(void);
  };
//+------------------------------------------------------------------+
//| Set the cursor icons based on cursor type                        |
//+------------------------------------------------------------------+
void CPointer::SetPointerBmp(void)
  {
   switch(m_type)
     {
      ...
      case MP_X_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         break;
      case MP_Y_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         break;
      ...
     }
//--- If custom type (MP_CUSTOM) is specified
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > Both images must be set for the cursor!");
      }

Additional fields will be required here:

  • To determine the moment of dragging the header border
  • To determine the moment when the mouse cursor moves from the area of one header to the area of another header. This is required to save resources, so that the headers are redrawn only when the borders of the adjacent areas are crossed.

class CCanvasTable : public CElement
  {
private:
   //--- To determine the moment of mouse cursor transition from one header to another
   int               m_prev_header_index_focus;
   //--- The state of dragging the header border to change the column width
   int               m_column_resize_control;
      };

The CCanvasTable::HeaderColorCurrent() method allows to obtain the current color for the header depending on the current mode, mouse cursor location and the left mouse button state. The focus over the header will be determined in the CCanvasTable::DrawHeaders() method, which is designed to draw the header backgrounds, and will be passed here as a result of the check.

class CCanvasTable : public CElement
  {
private:
   //--- Returns the current header background color
   uint              HeaderColorCurrent(const bool is_header_focus);
  };
//+------------------------------------------------------------------+
//| Returns the current header background color                      |
//+------------------------------------------------------------------+
uint CCanvasTable::HeaderColorCurrent(const bool is_header_focus)
  {
   uint clr=clrNONE;
//--- If there is no focus
   if(!is_header_focus || !m_headers.MouseFocus())
      clr=m_headers_color;
   else
     {
      //--- If the left mouse button is pressed and not in the process of changing the column width
      bool condition=(m_mouse.LeftButtonState() && m_column_resize_control==WRONG_VALUE);
      clr=(condition)? m_headers_color_pressed : m_headers_color_hover;
     }
//--- Return the header color
   return(::ColorToARGB(clr));
      }

The code of the CCanvasTable::DrawHeaders() method is presented below. Here, if the mouse cursor is not in the area of headers, the entire canvas is filled with the specified color. If the focus is on the headers, then it is necessary to determine which of them has the focus. To do this, it is necessary to determine the relative coordinates of the mouse cursor, and check if each header is in focus during the calculation of the header coordinates in a cycle. In addition, the mode of changing the column width is considered here. An additional offset is used in calculations for this mode. If the focus is found, the column index must stored

class CCanvasTable : public CElement
  {
private:
   //--- Offset from the borders of separation lines to display the mouse pointer in the mode of changing the column width
   int               m_sep_x_offset;
   //---
private:
   //--- Draws the headers
   void              DrawHeaders(void);
  };
//+------------------------------------------------------------------+
//| Draws the background of headers                                  |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeaders(void)
  {
//--- If not in focus, reset the header colors
   if(!m_headers.MouseFocus())
     {
      m_headers.Erase(::ColorToARGB(m_headers_color));
      return;
     }
//--- To check the focus on the headers
   bool is_header_focus=false;
//--- Coordinates of the mouse cursor
   int x=0;
//--- Coordinates
   int x1=0,x2=0,y1=0,y2=m_header_y_size;
//--- Get the relative coordinates of the mouse cursor
   if(::CheckPointer(m_mouse)!=POINTER_INVALID)
     {
      //--- Get the offset along the X axis
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- Determine the coordinates of the mouse cursor
      x=m_mouse.X()-m_headers.X()+xoffset;
     }
//--- Clear the background of headers
   m_headers.Erase(::ColorToARGB(clrNONE,0));
//--- Offset considering the mode of changing the column widths
   int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- Draw the background of headers
   for(int i=0; i<m_columns_total; i++)
     {
      //--- Calculate the coordinates
      x2+=m_vcolumns[i].m_width;
      //--- Check the focus
      if(is_header_focus=x>x1+((i!=0)? sep_x_offset : 0) && x<=x2+sep_x_offset)
         m_prev_header_index_focus=i;
      //--- Draw the header background
      m_headers.FillRectangle(x1,y1,x2,y2,HeaderColorCurrent(is_header_focus));
      //--- Calculate the offset for the next header
      x1+=m_vcolumns[i].m_width;
     }
      }

Once the background of headers is drawn, it is necessary to draw the grid (header frames). The CCanvasTable::DrawHeadersGrid() method is used for this purpose. First, the common frame is drawn, and then the separation lines are applied in a loop.

class CCanvasTable : public CElement
  {
private:
   //--- Draws the grid of the table headers
   void              DrawHeadersGrid(void);
  };
//+------------------------------------------------------------------+
//| Draws the grid of the table headers                              |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersGrid(void)
  {
//--- Grid color
   uint clr=::ColorToARGB(m_grid_color);
//--- Coordinates
   int x1=0,x2=0,y1=0,y2=0;
   x2=m_table_x_size-1;
   y2=m_header_y_size-1;
//--- Draw frame
   m_headers.Rectangle(x1,y1,x2,y2,clr);
//--- Separation lines
   x2=x1=m_vcolumns[0].m_width;
   for(int i=1; i<m_columns_total; i++)
     {
      m_headers.Line(x1,y1,x2,y2,clr);
      x2=x1+=m_vcolumns[i].m_width;
     }
      }

And lastly, draw the text of the headers. This task is performed by the CCanvasTable::DrawHeadersText() method. Here, it is necessary to go through every header in a loop, determining the coordinate for the text and alignment mode at each iteration. The header name is applied as the last operation of the cycle. Adjustment of text relative to the column width is also used here. The CCanvasTable::CorrectingText() method is used for this purpose. It is described in more detail in the next section of the article. 

class CCanvasTable : public CElement
  {
private:
   //--- Draws the text of the table headers
   void              DrawHeadersText(void);
  };
//+------------------------------------------------------------------+
//| Draws the text of the table headers                              |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersText(void)
  {
//--- To calculate the coordinates and offsets
   int x=0,y=m_header_y_size/2;
   int column_offset =0;
   uint text_align   =0;
//--- Text color
   uint clr=::ColorToARGB(m_headers_text_color);
//--- Font properties
   m_headers.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- Draw text
   for(int c=0; c<m_columns_total; c++)
     {
      //--- Get the X coordinate of the text
      x=TextX(c,column_offset);
      //--- Get the text alignment mode
      text_align=TextAlign(c,TA_VCENTER);
      //--- Draw the column name
      m_headers.TextOut(x,y,CorrectingText(c,0,true),clr,text_align);
     }
      }

All the listed methods for drawing the headers are called in the common CCanvasTable::DrawTableHeaders() method. Entry to this method is blocked if the header display mode is disabled

class CCanvasTable : public CElement
  {
private:
   //--- Draws the table headers
   void              DrawTableHeaders(void);
  };
//+------------------------------------------------------------------+
//| Draws the table headers                                          |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTableHeaders(void)
  {
//--- Leave, if the headers are disabled
   if(!m_show_headers)
      return;
//--- Draws the headers
   DrawHeaders();
//--- Draw grid
   DrawHeadersGrid();
//--- Draw the text of headers
   DrawHeadersText();
      }

Focus on a header is checked using the CCanvasTable::CheckHeaderFocus() method. The program leaves the method in two cases:

  • if the header display mode is disabled
  • or if the process of changing the column width has started.

After that, the relative coordinates of the cursor on the canvas are obtained. The cycle looks for a focus on any header and checks if it has changed since the last call to this method. If a new focus is registered (the moment of crossing the header borders), then it is necessary to reset the previously saved header index and stop the cycle.

class CCanvasTable : public CElement
  {
private:
   //--- Checking the focus on the header
   void              CheckHeaderFocus(void);
  };
//+------------------------------------------------------------------+
//| Checking the focus on the header                                 |
//+------------------------------------------------------------------+
void CCanvasTable::CheckHeaderFocus(void)
  {
//--- Leave, if (1) the headers are disabled or (2) changing the column width has started
   if(!m_show_headers || m_column_resize_control!=WRONG_VALUE)
      return;
//--- Coordinates of the headers
   int x1=0,x2=0;
//--- Get the offset along the X axis
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Get the relative coordinates of the mouse cursor
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- Offset considering the mode of changing the column widths
   int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- Search for focus
   for(int i=0; i<m_columns_total; i++)
     {
      //--- Calculate the right coordinate
      x2+=m_vcolumns[i].m_width;
      //--- If the header focus has changed
      if((x>x1+sep_x_offset && x<=x2+sep_x_offset) && m_prev_header_index_focus!=i)
        {
         m_prev_header_index_focus=WRONG_VALUE;
         break;
        }
      //--- Calculate the left coordinate
      x1+=m_vcolumns[i].m_width;
     }
      }

In their turn, the headers are redrawn only when the borders are crossed. This saves the CPU resources. The CCanvasTable::ChangeHeadersColor() method is designed for this task. Here, the program leaves the method if the header display mode is disabled or the process of changing their width is under way. If the checks at the beginning of the method are passed, then the focus on the headers is checked and they are redrawn. 

class CCanvasTable : public CElement
  {
private:
   //--- Changes the color of headers
   void              ChangeHeadersColor(void);
  };
//+------------------------------------------------------------------+
//| Changing the color of the headers                                |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeHeadersColor(void)
  {
//--- Leave, if the headers are disabled
   if(!m_show_headers)
      return;
//--- If the cursor is activated
   if(m_column_resize.IsVisible() && m_mouse.LeftButtonState())
     {
      //--- Store the index of the dragged column
      if(m_column_resize_control==WRONG_VALUE)
         m_column_resize_control=m_prev_header_index_focus;
      //---
      return;
     }
//--- If not in focus
   if(!m_headers.MouseFocus())
     {
      //--- If not yet indicated that not in focus
      if(m_prev_header_index_focus!=WRONG_VALUE)
        {
         //--- Reset the focus
         m_prev_header_index_focus=WRONG_VALUE;
         //--- Change the color
         DrawTableHeaders();
         m_headers.Update();
        }
     }
//--- If in focus
   else
     {
      //--- Check the focus on the headers
      CheckHeaderFocus();
      //--- If there is no focus
      if(m_prev_header_index_focus==WRONG_VALUE)
        {
         //--- Change the color
         DrawTableHeaders();
         m_headers.Update();
        }
     }
      }

Below is the code of the CCanvasTable::CheckColumnResizeFocus() method. It is needed to determine the focus on the borders between the headers and is responsible for showing/hiding the cursor for changing the column widths. There are two checks at the beginning of the method. The program leaves the method if the column width changing mode is disabled. If it is enabled and the changing the column width is in progress, then it is necessary to update the mouse cursor coordinates and leave the method.

If the process of changing the column width has not started yet, then, if the cursor is in the headers' area, attempt to determine the focus on the border of one of them in a loop. If the focus is found, update the coordinates of the mouse cursor, make it visible and leave the method. If the focus was not found, then the pointer should be hidden

class CCanvasTable : public CElement
  {
private:
   //--- Checking the focus on borders of headers to change their widths
   void              CheckColumnResizeFocus(void);
  };
//+------------------------------------------------------------------+
//| Checking the focus on borders of headers to change their widths  |
//+------------------------------------------------------------------+
void CCanvasTable::CheckColumnResizeFocus(void)
  {
//--- Leave, if the mode of changing the column widths is disabled
   if(!m_column_resize_mode)
      return;
//--- Leave, if started changing the column width
   if(m_column_resize_control!=WRONG_VALUE)
     {
      //--- Update the cursor coordinates and make it visible
      m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
      return;
     }
//--- To check the focus on the borders of headers
   bool is_focus=false;
//--- If the mouse cursor is in the area of headers
   if(m_headers.MouseFocus())
     {
      //--- Coordinates of the headers
      int x1=0,x2=0;
      //--- Get the offset along the X axis
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- Get the relative coordinates of the mouse cursor
      int x=m_mouse.X()-m_headers.X()+xoffset;
      //--- Search for focus
      for(int i=0; i<m_columns_total; i++)
        {
         //--- Calculating coordinates
         x1=x2+=m_vcolumns[i].m_width;
         //--- Verifying the focus
         if(is_focus=x>x1-m_sep_x_offset && x<=x2+m_sep_x_offset)
            break;
        }
      //--- If there is a focus
      if(is_focus)
        {
         //--- Update the cursor coordinates and make it visible
         m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
         //--- Show the cursor
         m_column_resize.Show();
         return;
        }
     }
//--- Hide the pointer, if not in focus
   if(!m_headers.MouseFocus() || !is_focus)
      m_column_resize.Hide();
      }

The final result is as follows:

 Fig. 5. Headers for columns.

Fig. 5. Headers for columns.

 

 


Adjustment of the string length relative to the column width

Earlier, to make the text not overlap the adjacent cells, it was necessary to manually select the column width and to recompile the file to see the result. Naturally, this is inconvenient.

Let us make the string length adjust automatically if it does not fit the table cell. The previously adjusted strings will not be re-adjusted when redrawing the table. Add another array to the structure of the table properties in order to store these strings.

class CCanvasTable : public CElement
  {
private:
   //--- Array of values and properties of the table
   struct CTOptions
     {
      string            m_vrows[];
      string            m_text[];
      int               m_width;
      ENUM_ALIGN_MODE   m_text_align;
     };
   CTOptions         m_vcolumns[];
      };

As a result, the m_vrows[] will store the full text, while the m_text[] array will store the adjusted version of the text.

The CCanvasTable::CorrectingText() method will be responsible for adjusting the string length both in headers and table cells. After identifying the text to work with, obtain its width. Next, check if the full text of the string fits the cell with consideration of all offsets from the cell edges. If it fits, save it in the m_text[] array and leave the method. In the current version, the adjusted text is saved only for the cells, but not for headers.

If the text does not fit, then the excessive characters should be trimmed and an ellipsis ('…') should be added. The ellipsis will indicate that the displayed text has been shortened. This procedure is easy to implement:

1) Get the string length.

2) Then iterate over all characters in a cycle starting from the last character, deleting the last character and saving the trimmed text in a temporary variable.

If there are no characters left, return an empty string.

4) As long as there are characters left, get the width of the resulting string including the ellipsis.

5) Check if the string fits the table cell in this form, with consideration of the specified offsets from the cell edges.

6) If the string fits, then store it to a local variable of the method and stop the cycle.

7) After that, store the adjusted string to the m_text[] array and return it from the method. 

class CCanvasTable : public CElement
  {
private:
   //--- Returns the text adjusted to the column width
   string            CorrectingText(const int column_index,const int row_index,const bool headers=false);
  };
//+------------------------------------------------------------------+
//| Returns the text adjusted to the column width                    |
//+------------------------------------------------------------------+
string CCanvasTable::CorrectingText(const int column_index,const int row_index,const bool headers=false)
  {
//--- Get the current text
   string corrected_text=(headers)? m_header_text[column_index]: m_vcolumns[column_index].m_vrows[row_index];
//--- Offsets from the cell edges along the X axis
   int x_offset=m_text_x_offset*2;
//--- Get the pointer to the canvas object
   CRectCanvas *obj=(headers)? ::GetPointer(m_headers) : ::GetPointer(m_table);
//--- Get the width of the text
   int full_text_width=obj.TextWidth(corrected_text);
//--- If it fits the cell, save the adjusted text in a separate array and return it
   if(full_text_width<=m_vcolumns[column_index].m_width-x_offset)
     {
      //--- If those are not headers, save the adjusted text
      if(!headers)
         m_vcolumns[column_index].m_text[row_index]=corrected_text;
      //---
      return(corrected_text);
     }
//--- If the text does not fit the cell, it is necessary to adjust the text (trim the excessive characters and add an ellipsis)
   else
     {
      //--- For working with a string
      string temp_text="";
      //--- Get the string length
      int total=::StringLen(corrected_text);
      //--- Delete characters from the string one by one, until the desired text width is reached
      for(int i=total-1; i>=0; i--)
        {
         //--- Delete one character
         temp_text=::StringSubstr(corrected_text,0,i);
         //--- If nothing is left, leave an empty string
         if(temp_text=="")
           {
            corrected_text="";
            break;
           }
         //--- Add an ellipsis before checking
         int text_width=obj.TextWidth(temp_text+"...");
         //--- If fits the cell
         if(text_width<m_vcolumns[column_index].m_width-x_offset)
           {
            //--- Save the text and stop the cycle
            corrected_text=temp_text+"...";
            break;
           }
        }
     }
//--- If those are not headers, save the adjusted text
   if(!headers)
      m_vcolumns[column_index].m_text[row_index]=corrected_text;
//--- Return the adjusted text
   return(corrected_text);
      }

Using the adjusted strings while redrawing the table is especially relevant during the process of changing the column width. Instead of adjusting the text in all table cells over and over, it is sufficient to do this only in the cells of the column, the width of which is being changed. This saves the CPU resources.

The CCanvasTable::Text() method will determine if it is necessary to adjust the text for the specified column or if it is sufficient to send the previously adjusted version. Its code looks the following way: 
class CCanvasTable : public CElement
  {
private:
   //--- Returns the text
   string            Text(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| Returns the text                                                 |
//+------------------------------------------------------------------+
string CCanvasTable::Text(const int column_index,const int row_index)
  {
   string text="";
//--- Adjust the text, if not in the mode of changing the column width
   if(m_column_resize_control==WRONG_VALUE)
      text=CorrectingText(column_index,row_index);
//--- If in the mode of changing the column width, then...
   else
     {
      //--- ...adjust the text only for the column with the width being changed
      if(column_index==m_column_resize_control)
         text=CorrectingText(column_index,row_index);
      //--- For all others, use the previously adjusted text
      else
         text=m_vcolumns[column_index].m_text[row_index];
     }
//--- Return the text
   return(text);
      }

Below is the code of the CCanvasTable::ChangeColumnWidth() method, which is designed for changing the column width.

The minimum column width is set to 30 pixels. The program leaves the method if the header display is disabled. If the check is passed, the focus is then checked at the borders of the headers. If this check determines that the process has not started/completed, the auxiliary variables are zeroed and the program leaves the method. If the process is running, then get the relative X coordinate of the cursor. If the process has just begun, then it is necessary to store the current X coordinate of the cursor (the x_fixed variable) and the width of the dragged column (the prev_width variable). The local variables intended for this purpose are static. Therefore, every time this method is entered, their values will be stored, until they are zeroed when the process completes. 

Now, calculate the new width for the column. If it turns out that the minimum column width was reached, the program leaves the method. Otherwise, the new width is stored in the structure of the table properties at the specified column. After that, the table dimensions are recalculated and reapplied, and the table is redrawn at the end of the method. 

class CCanvasTable : public CElement
  {
private:
   //--- The minimum width of the columns
   int               m_min_column_width;
   //---
private:
   //--- Changes the width of the dragged column
   void              ChangeColumnWidth(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_min_column_width(30)
  {
   ...
  }
//+------------------------------------------------------------------+
//| Changes the width of the dragged column                          |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeColumnWidth(void)
  {
//--- Leave, if the headers are disabled
   if(!m_show_headers)
      return;
//--- Check the focus on the header borders
   CheckColumnResizeFocus();
//--- Auxiliary variables
   static int x_fixed    =0;
   static int prev_width =0;
//--- If completed, reset the value
   if(m_column_resize_control==WRONG_VALUE)
     {
      x_fixed    =0;
      prev_width =0;
      return;
     }
//--- Get the offset along the X axis
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Get the relative coordinates of the mouse cursor
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- If the process of changing the column width has just begun
   if(x_fixed<1)
     {
      //--- Store the current X coordinate and width of the column
      x_fixed    =x;
      prev_width =m_vcolumns[m_column_resize_control].m_width;
     }
//--- Calculate the new width for the column
   int new_width=prev_width+(x-x_fixed);
//--- Leave unchanged, if less than the specified limit
   if(new_width<m_min_column_width)
      return;
//--- Save the new width of the column
   m_vcolumns[m_column_resize_control].m_width=new_width;
//--- Calculate the table sizes
   CalculateTableSize();
//--- Resize the table
   ChangeTableSize();
//--- Draw the table
   DrawTable();
      }

The result is as follows:

 Fig. 5. Adjustment of the string length relative to the variable width of the column.

Fig. 5. Adjustment of the string length relative to the variable width of the column. 

 


Event Handling

The color management of the table objects and changing the widths of its columns is performed by the control's handler of the mouse movement event (CHARTEVENT_MOUSE_MOVE). 

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling of the cursor movement event
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Leave, if the control is hidden
      if(!CElementBase::IsVisible())
         return;
      //--- Leave, if numbers of subwindows do not match
      if(!CElementBase::CheckSubwindowNumber())
         return;
      //--- Checking the focus over elements
      CElementBase::CheckMouseFocus();
      m_headers.MouseFocus(m_mouse.X()>m_headers.X() && m_mouse.X()<m_headers.X2() &&
                           m_mouse.Y()>m_headers.Y() && m_mouse.Y()<m_headers.Y2());
      //--- If the scrollbar is active
      if(m_scrollv.ScrollBarControl() || m_scrollh.ScrollBarControl())
        {
         ShiftTable();
         return;
        }
      //--- Changing the object colors
      ChangeObjectsColor();
      //--- Change the width of the dragged column
      ChangeColumnWidth();
      return;
     }
   ...
      }

Another new event identifier will be required in order to determine the moment of changing the state of the left mouse button. It is needed to get rid of repeated checks and simultaneous handling in multiple blocks of the event handler code. Add the ON_CHANGE_MOUSE_LEFT_BUTTON identifier to the Define.mqh file: 

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
  #define ON_CHANGE_MOUSE_LEFT_BUTTON (33) // Changing the state of the left mouse button

In addition, the CMouse::CheckChangeLeftButtonState() method has been added to the class for getting the current parameters of the mouse (CMouse). It allows to determine the moment of change in the left mouse button state. This method is called in the handler of the class. If the state of the left mouse button changed, the method sends a message with the ON_CHANGE_MOUSE_LEFT_BUTTON identifier. This message can later be received and processed in any control. 

//+------------------------------------------------------------------+
//| Class for getting the mouse parameters                           |
//+------------------------------------------------------------------+
class CMouse
  {
private:
   //--- Checking the change in the state of the left mouse button
   bool              CheckChangeLeftButtonState(const string mouse_state);
  };
//+------------------------------------------------------------------+
//| Handle events of moving the mouse cursor                         |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Handling of the cursor movement event
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Coordinates and the state of left button of the mouse
      m_x                 =(int)lparam;
      m_y                 =(int)dparam;
      m_left_button_state =CheckChangeLeftButtonState(sparam);
      ...
     }
  }
//+------------------------------------------------------------------+
//| Checking the change in the state of the left mouse button        |
//+------------------------------------------------------------------+
bool CMouse::CheckChangeLeftButtonState(const string mouse_state)
  {
   bool left_button_state=(bool)int(mouse_state);
//--- Send a message about a change in the state of the left mouse button
   if(m_left_button_state!=left_button_state)
      ::EventChartCustom(m_chart.ChartId(),ON_CHANGE_MOUSE_LEFT_BUTTON,0,0.0,"");
//---
   return(left_button_state);
      }

Handling the events with the ON_CHANGE_MOUSE_LEFT_BUTTON identifier is required in the CCanvasTable class:

  •  to zero certain fields of the class;
  •  to adjust the scrollbars;
  •  to redraw the table
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Change in the state of the left mouse button
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON)
     {
      //--- Leave, if the headers are disabled
      if(!m_show_headers)
         return;
      //--- If the left mouse button is released
      if(!m_mouse.LeftButtonState())
        {
         //--- Reset the width changing mode
         m_column_resize_control=WRONG_VALUE;
         //--- Hide the cursor
         m_column_resize.Hide();
         //--- Adjust the scrollbar with consideration of the recent changes
         HorizontalScrolling(m_scrollh.CurrentPos());
        }
      //--- Reset the index of the last focus on a header
      m_prev_header_index_focus=WRONG_VALUE;
      //--- Changing the object colors
      ChangeObjectsColor();
     }
      }

The animates screenshots of the article demonstrate the operation result of the MQL application, which can be downloaded using the link below for further studying.

 

Conclusion

The current update of the library improves the rendered table of the CCanvasTable type. This is not the final version of the table. It will be developed further and new features will be added to it.

The current schematic of the library for creating graphical interfaces looks as shown below.

 Fig. 6. Structure of the library at the current stage of development.

Fig. 6. Structure of the library at the current stage of development.

 

You can download the latest version of the library and files for testing below.

If you have questions on using the material presented in those files, you can refer to the detailed description of the library development in one of the articles of this series or ask your question in the comments of this article.  

Translated from Russian by MetaQuotes Software Corp.
Original article: https://www.mql5.com/ru/articles/3030

Attached files |
Graphical Interfaces X: The Multiline Text box control (build 8) Graphical Interfaces X: The Multiline Text box control (build 8)

The Multiline Text box control is discussed. Unlike the graphical objects of the OBJ_EDIT type, the presented version will not have restrictions on the number of input characters. It also adds the mode for turning the text box into a simple text editor, where the cursor can be moved using the mouse or keys.

3D Modeling in MQL5 3D Modeling in MQL5

A time series is a dynamic system, in which values of a random variable are received continuously or at successive equally spaced points in time. Transition from 2D to 3D market analysis provides a new look at complex processes and research objects. The article describes visualization methods providing 3D representation of two-dimensional data.

Visualize this! MQL5 graphics library similar to 'plot' of R language Visualize this! MQL5 graphics library similar to 'plot' of R language

When studying trading logic, visual representation in the form of graphs is of great importance. A number of programming languages popular among the scientific community (such as R and Python) feature the special 'plot' function used for visualization. It allows drawing lines, point distributions and histograms to visualize patterns. In MQL5, you can do the same using the CGraphics class.

Graphical Interfaces X: Updates for the Rendered table and code optimization (build 10) Graphical Interfaces X: Updates for the Rendered table and code optimization (build 10)

We continue to complement the Rendered table (CCanvasTable) with new features. The table will now have: highlighting of the rows when hovered; ability to add an array of icons for each cell and a method for switching them; ability to set or modify the cell text during the runtime, and more.