
Graphical interfaces X: New features for the Rendered table (build 9)
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:
- Graphical Interfaces VII: the Tables Controls (Chapter 1)
- Graphical Interfaces X: Updates for Easy And Fast Library (Build 3)
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.
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.
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.
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.
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.
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.
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.
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 Ltd.
Original article: https://www.mql5.com/ru/articles/3030





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use