DoEasy. Controls (Part 16): TabControl WinForms object — several rows of tab headers, stretching headers to fit the container
Contents
Concept
In the previous article, I have provided explanations about the modes of displaying tab headers:
If a control has more tabs than the object width can fit (we assume they are located on top), then the titles that do not fit within the element can be cropped and have the scrolling buttons. Alternatively, if the multiline mode flag is set for the object, then the titles are placed in several pieces (depending on how many are included in the size of the element) and in several rows. There are three ways to set the size of the tabs arranged in rows (SizeMode):
- Normal — width of the tabs is set according to the width of the title text, the space specified in the PaddingWidth and PaddingHeight values of the title is added along the edges of the title;
- Fixed — fixed size specified in the control settings. The title text is cropped if it does not fit within its size;
- FillToRight — tabs that fit within the width of a control are stretched to its full width.
When a tab is selected in the active Multiline mode, its title, which does not border the tab field, along with the entire row it is located in, moves close to the tab field, and the titles that were adjacent to the field take the place of the row of the selected tab.
I have also implemented the functionality of placing tabs on top of the control in Normal and Fixed modes.
In the current article, I will place tabs in Multiline mode on all sides of the control and add the mode for setting the size of the tabs FillToRight — stretching the rows of tabs according to the size of the control. When placing rows of tab headers at the top or bottom of the container, the headers will stretch along the width of the control. When tab headers are positioned to the left or right, the headers will stretch to fit the height of the control. In this case, the header stretching area will be two pixels smaller on each side of this area, since the selected tab header increases in size by 4 pixels when clicking on it. Therefore, if we do not leave a gap of two pixels for the marginal heading, then when it is selected and its size increases accordingly, its edge goes beyond the control.
For the mode of displaying tab headers in one row and if there are more of them than we can fit by the control size, all headers that go beyond the container will be located outside of it. We do not yet have enough functionality to crop graphical elements that go beyond the container, so this mode is not yet considered. I will implement it in subsequent articles.
Improving library classes
We will need to search the lists of tab headers for all the headers located in the same row so that we can work only with the headers of this row. The easiest way to do this is to add new properties to the library's WinForms object and use the library's long-standing functionality to search and sort lists of objects.
In \MQL5\Include\DoEasy\Defines.mqh, add two new properties to the list of integer properties of the graphical element on canvas and increase their total number from 90 to 92:
//+------------------------------------------------------------------+ //| Integer properties of the graphical element on the canvas | //+------------------------------------------------------------------+ enum ENUM_CANV_ELEMENT_PROP_INTEGER { CANV_ELEMENT_PROP_ID = 0, // Element ID CANV_ELEMENT_PROP_TYPE, // Graphical element type //---... //---... CANV_ELEMENT_PROP_TAB_SIZE_MODE, // Tab size setting mode CANV_ELEMENT_PROP_TAB_PAGE_NUMBER, // Tab index number CANV_ELEMENT_PROP_TAB_PAGE_ROW, // Tab row index CANV_ELEMENT_PROP_TAB_PAGE_COLUMN, // Tab column index CANV_ELEMENT_PROP_ALIGNMENT, // Location of an object inside the control }; #define CANV_ELEMENT_PROP_INTEGER_TOTAL (92) // Total number of integer properties #define CANV_ELEMENT_PROP_INTEGER_SKIP (0) // Number of integer properties not used in sorting //+------------------------------------------------------------------+
Add these two new properties to the enumeration of possible criteria of sorting graphical elements on the canvas:
//+------------------------------------------------------------------+ //| Possible sorting criteria of graphical elements on the canvas | //+------------------------------------------------------------------+ #define FIRST_CANV_ELEMENT_DBL_PROP (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP) #define FIRST_CANV_ELEMENT_STR_PROP (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP) enum ENUM_SORT_CANV_ELEMENT_MODE { //--- Sort by integer properties SORT_BY_CANV_ELEMENT_ID = 0, // Sort by element ID SORT_BY_CANV_ELEMENT_TYPE, // Sort by graphical element type //---... //---... SORT_BY_CANV_ELEMENT_TAB_SIZE_MODE, // Sort by the mode of setting the tab size SORT_BY_CANV_ELEMENT_TAB_PAGE_NUMBER, // Sort by the tab index number SORT_BY_CANV_ELEMENT_TAB_PAGE_ROW, // Sort by tab row index SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN, // Sort by tab column index SORT_BY_CANV_ELEMENT_ALIGNMENT, // Sort by the location of the object inside the control //--- Sort by real properties //--- Sort by string properties SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,// Sort by an element object name SORT_BY_CANV_ELEMENT_NAME_RES, // Sort by the graphical resource name SORT_BY_CANV_ELEMENT_TEXT, // Sort by graphical element text SORT_BY_CANV_ELEMENT_DESCRIPTION, // Sort by graphical element description }; //+------------------------------------------------------------------+
Now we can quickly find and add to the list all the tab headers located in the same row and calculate their total size so that they stretch to the size of the control and are correctly placed.
If we add new properties to the object, then we need to add their description. All property descriptions are located in one multidimensional array, where the first dimension contains the message index, and the remaining dimensions contain texts in different languages. At the moment, we have message texts in English and Russian, but other languages can be easily added by increasing the array dimension and adding texts in the corresponding languages in the required dimensions.
In \MQL5\Include\DoEasy\Data.mqh, add the new message indices:
MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE, // Tab size setting mode MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER, // Tab index number MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW, // Tab row index MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN, // Tab column index MSG_CANV_ELEMENT_PROP_ALIGNMENT, // Location of an object inside the control //--- Real properties of graphical elements //--- String properties of graphical elements MSG_CANV_ELEMENT_PROP_NAME_OBJ, // Graphical element object name MSG_CANV_ELEMENT_PROP_NAME_RES, // Graphical resource name MSG_CANV_ELEMENT_PROP_TEXT, // Graphical element text MSG_CANV_ELEMENT_PROP_DESCRIPTION, // Graphical element description }; //+------------------------------------------------------------------+
and text messages corresponding to the newly added indices:
{"Режим установки размера вкладок","Tab Size Mode"}, {"Порядковый номер вкладки","Tab ordinal number"}, {"Номер ряда вкладки","Tab row number"}, {"Номер столбца вкладки","Tab column number"}, {"Местоположение объекта внутри элемента управления","Location of the object inside the control"}, //--- String properties of graphical elements {"Имя объекта-графического элемента","The name of the graphic element object"}, {"Имя графического ресурса","Image resource name"}, {"Текст графического элемента","Text of the graphic element"}, {"Описание графического элемента","Description of the graphic element"}, }; //+---------------------------------------------------------------------+
I have considered the library message class and the concept of storing its data in the separate article.
When using the CCanvas class of the MQL5 Standard Library, it is not always possible to get the code of the error, which prevented us from creating a graphical element. I gradually add fixes to the library to help users understand why an element was not created.
In the \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh graphical element file, namely in the methods for setting the width and height, add a description of the resize error along with displaying the error message:
//+------------------------------------------------------------------+ //| Set a new width | //+------------------------------------------------------------------+ bool CGCnvElement::SetWidth(const int width) { if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width) return true; if(!this.m_canvas.Resize(width,this.m_canvas.Height())) { CMessage::ToLog(DFUN+this.TypeElementDescription()+": width="+(string)width+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH); return false; } this.SetProperty(CANV_ELEMENT_PROP_WIDTH,width); return true; } //+------------------------------------------------------------------+ //| Set a new height | //+------------------------------------------------------------------+ bool CGCnvElement::SetHeight(const int height) { if(this.GetProperty(CANV_ELEMENT_PROP_HEIGHT)==height) return true; if(!this.m_canvas.Resize(this.m_canvas.Width(),height)) { CMessage::ToLog(DFUN+this.TypeElementDescription()+": height="+(string)height+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT); return false; } this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,height); return true; } //+------------------------------------------------------------------+
Here the macro substitution receives a description of the type of element that could not be resized and the parameter value passed to the method. This makes it easier to understand errors when developing library classes.
In order to be able to display descriptions of the two new properties of a graphical element, we need to add a code block to the method returning the description of the integer property of the element in \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh:
//+------------------------------------------------------------------+ //| Return the description of the control integer property | //+------------------------------------------------------------------+ string CWinFormBase::GetPropertyDescription(ENUM_CANV_ELEMENT_PROP_INTEGER property,bool only_prop=false) { return ( property==CANV_ELEMENT_PROP_ID ? CMessage::Text(MSG_CANV_ELEMENT_PROP_ID)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(string)this.GetProperty(property) ) : property==CANV_ELEMENT_PROP_TYPE ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TYPE)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.TypeElementDescription() ) : //---... //---... property==CANV_ELEMENT_PROP_TAB_SIZE_MODE ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+TabSizeModeDescription((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(property)) ) : property==CANV_ELEMENT_PROP_TAB_PAGE_NUMBER ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(string)this.GetProperty(property) ) : property==CANV_ELEMENT_PROP_TAB_PAGE_ROW ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(string)this.GetProperty(property) ) : property==CANV_ELEMENT_PROP_TAB_PAGE_COLUMN ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(string)this.GetProperty(property) ) : property==CANV_ELEMENT_PROP_ALIGNMENT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_ALIGNMENT)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property)) ) : "" ); } //+------------------------------------------------------------------+
Here, depending on the property passed to the method, create a text message and return it from the method.
Now let's finalize the classes of the TabControl WinForms object.
The object consists of a container in which tabs are located. The container, in turn, consists of two auxiliary objects — the tab field and its header. All objects that should be located on the tab are placed on the tab field, and we select the tab that we want to see and work with as the tab header. Tab headers are implemented in the TabHeader class derived from the Button WinForms object in \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.
Since now we have two new properties that indicate the tab header location in the general header list (header row and column used to place it on a control), remove two private variables, which stored these properties:
//+------------------------------------------------------------------+ //| TabHeader object class of WForms TabControl | //+------------------------------------------------------------------+ class CTabHeader : public CButton { private: int m_width_off; // Object width in the released state int m_height_off; // Object height in the released state int m_width_on; // Object width in the selected state int m_height_on; // Object height in the selected state int m_col; // Header column index int m_row; // Header row index //--- Adjust the size and location of the element depending on the state bool WHProcessStateOn(void); bool WHProcessStateOff(void); //--- Draws a header frame depending on its position virtual void DrawFrame(void); //--- Set the string of the selected tab header to the correct position, (1) top, (2) bottom, (3) left and (4) right void CorrectSelectedRowTop(void); void CorrectSelectedRowBottom(void); void CorrectSelectedRowLeft(void); void CorrectSelectedRowRight(void); protected:
In the public methods, setting and returning these two properties, their values are now set to (and returned from) the object properties, rather than to the variables as it was before:
//--- Returns the control size int WidthOff(void) const { return this.m_width_off; } int HeightOff(void) const { return this.m_height_off; } int WidthOn(void) const { return this.m_width_on; } int HeightOn(void) const { return this.m_height_on; } //--- (1) Set and (2) return the Tab row index void SetRow(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW,value); } int Row(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW); } //--- (1) Set and (2) return the Tab column index void SetColumn(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,value); } int Column(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN); } //--- Set the tab location void SetTabLocation(const int row,const int col) { this.SetRow(row); this.SetColumn(col); }
The tab header object is created in the constructor with default size values, and then it is sized to match the header sizing mode set in the object container class. This is due to the fact that for all WinForms objects of the library we use the same values of their parameters, common to all objects, while setting additional parameters that belong to a particular object after its creation. This has both convenience and limitations. The convenience lies in the fact that we can create any object with one method, while limitations lie in the fact that it is not always possible to immediately build an object with the desired values of its properties, and they have to be installed after the object has been created.
In this case, this concerns the header size which depends on the size setting mode. Here we are faced with the fact that the initially created object in the specified coordinates further changes its size, and its initial coordinate, located in the upper left corner of the constructed object, is no longer where it was originally planned. Therefore, in addition to changing the size of the header, we also need to control its location in the desired coordinate, since in some cases the object is displaced and finds itself beyond its container.
For the Normal sizing mode, we can find out in advance what sizes the header will receive. In this mode, the size of the object adjusts to the text written on it, and this text is known to us. When headers are located at the top and bottom of the container, the Padding values set for the object (PaddingLeft and PaddingRight) are added to the width calculated by the text size of the object, while PaddingTop and PaddingBottom are added to the height. When headers are positioned to the left and right of the container (vertically), PaddingLeft and PaddingRight are added to the height of the object, and PaddingTop and PaddingBottom are added to the width, since visually it looks like the header is simply rotated 90° and its text is vertical, so the actual height of the graphical element is the visible width of the vertically rotated object.
Let's make changes to the method that sets all the header sizes. In the code block for setting the header size to the text size for the Normal mode, implement the control of the side of the container the headers are located on — in order to set the headers at the top and bottom, the Padding values are added in correct order — Padding is added to the width on the left and right, while in case of the height, it is added at the top and bottom. For placing headers to the left and right, the Padding values added to the width and height are swapped — Padding at the top and bottom is added to the width and Padding to the left and right is added to the height:
//--- Depending on the header size setting mode switch(this.TabSizeMode()) { //--- set the width and height for the Normal mode case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL : switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.TextSize(this.Text(),width,height); width+=this.PaddingLeft()+this.PaddingRight(); height=h+this.PaddingTop()+this.PaddingBottom(); break; case CANV_ELEMENT_ALIGNMENT_LEFT : case CANV_ELEMENT_ALIGNMENT_RIGHT : this.TextSize(this.Text(),height,width); height+=this.PaddingLeft()+this.PaddingRight(); width=w+this.PaddingTop()+this.PaddingBottom(); break; default: break; } break; //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED //---CANV_ELEMENT_TAB_SIZE_MODE_FILL //--- For the Fixed mode, the dimensions remain specified, //--- In case of Fill, they are calculated in the StretchHeaders methods of the TabControl class default: break; } //--- Set the results of changing the width and height to 'res'
At the end of the method, sizes are set for the header in the selected and unselected states in exactly the same way. Since when you select a tab by clicking on the header, the header of the selected tab increases in size by two pixels on three sides, the actual size of the horizontally located header should be increased in width by 4 pixels, and in height — by two. When a header is placed vertically, the real object width is the height of the header rotated vertically, and the real height is the width of the header. For this case, the width is increased by two pixels, while the height is increased by four:
//--- Set the changed size for different button states switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.SetWidthOn(this.Width()+4); this.SetHeightOn(this.Height()+2); this.SetWidthOff(this.Width()); this.SetHeightOff(this.Height()); break; case CANV_ELEMENT_ALIGNMENT_LEFT : case CANV_ELEMENT_ALIGNMENT_RIGHT : this.SetWidthOn(this.Width()+2); this.SetHeightOn(this.Height()+4); this.SetWidthOff(this.Width()); this.SetHeightOff(this.Height()); break; default: break; }
Now the method with all the implemented changes looks like this:
//+------------------------------------------------------------------+ //| Set all header sizes | //+------------------------------------------------------------------+ bool CTabHeader::SetSizes(const int w,const int h) { //--- If the passed width or height is less than 4 pixels, //--- make them equal to four pixels int width=(w<4 ? 4 : w); int height=(h<4 ? 4 : h); //--- Depending on the header size setting mode switch(this.TabSizeMode()) { //--- set the width and height for the Normal mode case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL : switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.TextSize(this.Text(),width,height); width+=this.PaddingLeft()+this.PaddingRight(); height=h+this.PaddingTop()+this.PaddingBottom(); break; case CANV_ELEMENT_ALIGNMENT_LEFT : case CANV_ELEMENT_ALIGNMENT_RIGHT : this.TextSize(this.Text(),height,width); height+=this.PaddingLeft()+this.PaddingRight(); width=w+this.PaddingTop()+this.PaddingBottom(); break; default: break; } break; //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED //---CANV_ELEMENT_TAB_SIZE_MODE_FILL //--- For the Fixed mode, the dimensions remain specified, //--- In case of Fill, they are calculated in the StretchHeaders methods of the TabControl class default: break; } //--- Set the results of changing the width and height to 'res' bool res=true; res &=this.SetWidth(width); res &=this.SetHeight(height); //--- If there is an error in changing the width or height, return 'false' if(!res) return false; //--- Set the changed size for different button states switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.SetWidthOn(this.Width()+4); this.SetHeightOn(this.Height()+2); this.SetWidthOff(this.Width()); this.SetHeightOff(this.Height()); break; case CANV_ELEMENT_ALIGNMENT_LEFT : case CANV_ELEMENT_ALIGNMENT_RIGHT : this.SetWidthOn(this.Width()+2); this.SetHeightOn(this.Height()+4); this.SetWidthOff(this.Width()); this.SetHeightOff(this.Height()); break; default: break; } return true; } //+------------------------------------------------------------------+
In the method that adjusts the size and position of the element in the "selected" state depending on its position, I have not previously implemented a shift of the enlarged header for cases where the titles are located to the left and right of the control. In addition, I have made a small error in the bottom header position processing block — I have skipped the 'break' operator, which did not cause any errors since all the cases were empty and no code was called. Now this will cause incorrect behavior — the case following the skipped 'break' statement will be processed.
Let's add code blocks that shift the enlarged header by two points in the right direction for positioning headers to the left and right:
//+------------------------------------------------------------------+ //| Adjust the element size and location | //| in the "selected" state depending on its location | //+------------------------------------------------------------------+ bool CTabHeader::WHProcessStateOn(void) { //--- If failed to set a new size, leave if(!this.SetSizeOn()) return false; //--- Get the base object CWinFormBase *base=this.GetBase(); if(base==NULL) return false; //--- Depending on the title location, switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : //--- Adjust the location of the row with the selected header this.CorrectSelectedRowTop(); //--- shift the header by two pixels to the new location coordinates and //--- set the new relative coordinates if(this.Move(this.CoordX()-2,this.CoordY()-2)) { this.SetCoordXRelative(this.CoordXRelative()-2); this.SetCoordYRelative(this.CoordYRelative()-2); } break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : //--- Adjust the location of the row with the selected header this.CorrectSelectedRowBottom(); //--- shift the header by two pixels to the new location coordinates and //--- set the new relative coordinates if(this.Move(this.CoordX()-2,this.CoordY())) { this.SetCoordXRelative(this.CoordXRelative()-2); this.SetCoordYRelative(this.CoordYRelative()); } break; case CANV_ELEMENT_ALIGNMENT_LEFT : //--- Adjust the location of the row with the selected header this.CorrectSelectedRowLeft(); //--- shift the header by two pixels to the new location coordinates and //--- set the new relative coordinates if(this.Move(this.CoordX()-2,this.CoordY()-2)) { this.SetCoordXRelative(this.CoordXRelative()-2); this.SetCoordYRelative(this.CoordYRelative()-2); } break; case CANV_ELEMENT_ALIGNMENT_RIGHT : //--- Adjust the location of the row with the selected header this.CorrectSelectedRowRight(); //--- shift the header by two pixels to the new location coordinates and //--- set the new relative coordinates if(this.Move(this.CoordX(),this.CoordY()-2)) { this.SetCoordXRelative(this.CoordXRelative()); this.SetCoordYRelative(this.CoordYRelative()-2); } break; default: break; } return true; } //+------------------------------------------------------------------+
In the same way, finalize the method that adjusts the size and position of the element in the "not selected" state depending on its position — add the code blocks that move the header with restored size back to its original position after shifting the header when increasing the size in the above method during its selection:
//+------------------------------------------------------------------+ //| Adjust the element size and location | //| in the "released" state depending on its location | //+------------------------------------------------------------------+ bool CTabHeader::WHProcessStateOff(void) { //--- If failed to set a new size, leave if(!this.SetSizeOff()) return false; //--- Depending on the title location, switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX()+2,this.CoordY()+2)) { this.SetCoordXRelative(this.CoordXRelative()+2); this.SetCoordYRelative(this.CoordYRelative()+2); } break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX()+2,this.CoordY())) { this.SetCoordXRelative(this.CoordXRelative()+2); this.SetCoordYRelative(this.CoordYRelative()); } break; case CANV_ELEMENT_ALIGNMENT_LEFT : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX()+2,this.CoordY()+2)) { this.SetCoordXRelative(this.CoordXRelative()+2); this.SetCoordYRelative(this.CoordYRelative()+2); } break; case CANV_ELEMENT_ALIGNMENT_RIGHT : //--- shift the header to its original position and set the previous relative coordinates if(this.Move(this.CoordX(),this.CoordY()+2)) { this.SetCoordXRelative(this.CoordXRelative()); this.SetCoordYRelative(this.CoordYRelative()+2); } break; default: break; } return true; } //+------------------------------------------------------------------+
Now, after these improvements, when selecting the headers of the tabs located on the left or right, they will correctly increase their size when selected and decrease it when deselected visually becoming a little larger and visually remaining in their original place.
When we have tab headers arranged in several rows, then when selecting a tab whose header is not directly adjacent to the tab itself, but is located somewhere among the rows of other tabs, we need to move the entire row, in which the header of the selected tab is located, close to the tab fields, and move the line, that was previously adjacent to the fields, to replace the line with the selected header. I already implemented a similar method for placing tab headers on top of the control in the previous article. Now we need to make similar methods for positioning headers at the bottom, left and right.
The method that sets the selected tab header bar to the correct position at the bottom:
//+------------------------------------------------------------------+ //| Set the row of a selected tab header | //| to the correct position at the bottom | //+------------------------------------------------------------------+ void CTabHeader::CorrectSelectedRowBottom(void) { int row_pressed=this.Row(); // Selected header row int y_pressed=this.CoordY(); // Coordinate where all headers with Row() equal to zero should be moved to int y0=0; // Zero row coordinate (Row == 0) //--- If the zero row is selected, then nothing needs to be done - leave if(row_pressed==0) return; //--- Get the tab field object corresponding to this header and set the Y coordinate of the zero line CWinFormBase *obj=this.GetFieldObj(); if(obj==NULL) return; y0=obj.CoordY()+obj.Height(); //--- Get the base object (TabControl) CWinFormBase *base=this.GetBase(); if(base==NULL) return; //--- Get the list of all tab headers from the base object CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); if(list==NULL) return; //--- Swap rows in the loop through all headers - //--- set the row of the selected header to the zero position, while the zero one is set to the position of the selected header row for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If this is a zero row if(header.Row()==0) { //--- move the header to the position of the selected row if(header.Move(header.CoordX(),y_pressed)) { header.SetCoordXRelative(header.CoordX()-base.CoordX()); header.SetCoordYRelative(header.CoordY()-base.CoordY()); //--- Set the Row value to -1. It will be used as a label of the moved zero row instead of the selected one header.SetRow(-1); } } //--- If this is the clicked header line, if(header.Row()==row_pressed) { //--- move the header to the position of the zero row if(header.Move(header.CoordX(),y0)) { header.SetCoordXRelative(header.CoordX()-base.CoordX()); header.SetCoordYRelative(header.CoordY()-base.CoordY()); //--- Set the Row value to -2. It will be used as a label of the moved selected row instead of the zero one header.SetRow(-2); } } } //--- Set the correct Row and Col for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it if(header.Row()==-1) header.SetRow(row_pressed); //--- If this is the selected row moved to the zero position, set Row of the zero row if(header.Row()==-2) header.SetRow(0); } } //+------------------------------------------------------------------+
The method that sets the selected tab header bar to the correct left position:
//+------------------------------------------------------------------+ //| Set the row of a selected tab header | //| to the correct position on the left | //+------------------------------------------------------------------+ void CTabHeader::CorrectSelectedRowLeft(void) { int row_pressed=this.Row(); // Selected header row int x_pressed=this.CoordX(); // Coordinate where all headers with Row() equal to zero should be moved to int x0=0; // Zero row coordinate (Row == 0) //--- If the zero row is selected, then nothing needs to be done - leave if(row_pressed==0) return; //--- Get the tab field object corresponding to this header and set the X coordinate of the zero line CWinFormBase *obj=this.GetFieldObj(); if(obj==NULL) return; x0=obj.CoordX()-this.Width()+2; //--- Get the base object (TabControl) CWinFormBase *base=this.GetBase(); if(base==NULL) return; //--- Get the list of all tab headers from the base object CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); if(list==NULL) return; //--- Swap rows in the loop through all headers - //--- set the row of the selected header to the zero position, while the zero one is set to the position of the selected header row for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If this is a zero row if(header.Row()==0) { //--- move the header to the position of the selected row if(header.Move(x_pressed,header.CoordY())) { header.SetCoordXRelative(header.CoordX()-base.CoordX()); header.SetCoordYRelative(header.CoordY()-base.CoordY()); //--- Set the Row value to -1. It will be used as a label of the moved zero row instead of the selected one header.SetRow(-1); } } //--- If this is the clicked header line, if(header.Row()==row_pressed) { //--- move the header to the position of the zero row if(header.Move(x0,header.CoordY())) { header.SetCoordXRelative(header.CoordX()-base.CoordX()); header.SetCoordYRelative(header.CoordY()-base.CoordY()); //--- Set the Row value to -2. It will be used as a label of the moved selected row instead of the zero one header.SetRow(-2); } } } //--- Set the correct Row and Col for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it if(header.Row()==-1) header.SetRow(row_pressed); //--- If this is the selected row moved to the zero position, set Row of the zero row if(header.Row()==-2) header.SetRow(0); } } //+------------------------------------------------------------------+
The method that sets the selected tab header bar to the correct right position:
//+------------------------------------------------------------------+ //| Set the row of a selected tab header | //| to the correct position on the right | //+------------------------------------------------------------------+ void CTabHeader::CorrectSelectedRowRight(void) { int row_pressed=this.Row(); // Selected header row int x_pressed=this.CoordX(); // Coordinate where all headers with Row() equal to zero should be moved to int x0=0; // Zero row coordinate (Row == 0) //--- If the zero row is selected, then nothing needs to be done - leave if(row_pressed==0) return; //--- Get the tab field object corresponding to this header and set the X coordinate of the zero line CWinFormBase *obj=this.GetFieldObj(); if(obj==NULL) return; x0=obj.RightEdge(); //--- Get the base object (TabControl) CWinFormBase *base=this.GetBase(); if(base==NULL) return; //--- Get the list of all tab headers from the base object CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); if(list==NULL) return; //--- Swap rows in the loop through all headers - //--- set the row of the selected header to the zero position, while the zero one is set to the position of the selected header row for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If this is a zero row if(header.Row()==0) { //--- move the header to the position of the selected row if(header.Move(x_pressed,header.CoordY())) { header.SetCoordXRelative(header.CoordX()-base.CoordX()); header.SetCoordYRelative(header.CoordY()-base.CoordY()); //--- Set the Row value to -1. It will be used as a label of the moved zero row instead of the selected one header.SetRow(-1); } } //--- If this is the clicked header line, if(header.Row()==row_pressed) { //--- move the header to the position of the zero row if(header.Move(x0,header.CoordY())) { header.SetCoordXRelative(header.CoordX()-base.CoordX()); header.SetCoordYRelative(header.CoordY()-base.CoordY()); //--- Set the Row value to -2. It will be used as a label of the moved selected row instead of the zero one header.SetRow(-2); } } } //--- Set the correct Row and Col for(int i=0;i<list.Total();i++) { CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it if(header.Row()==-1) header.SetRow(row_pressed); //--- If this is the selected row moved to the zero position, set Row of the zero row if(header.Row()==-2) header.SetRow(0); } } //+------------------------------------------------------------------+
I considered a similar method in the previous article — the one for moving a number of headers when they are located on top of the control. The logic behind these new methods is exactly the same, but for bottom positioning, we move the header rows along the Y axis, and for left and right positioning, we move them along the X axis. All the logic is described in detail in the comments to the code.
When a tab is selected by clicking on the header, the header becomes slightly larger and, if necessary, is transferred from the list of headers rows close to the tab field, and the resulting visible border (due to the field frame) between the header and the tab field is erased so that the field and the header look like one indivisible whole. I erased the border between the field and the header in the previous article, but only for the position of the header above and below. Now we need to add the erasion of the border between the field and the header when the latter is located on the left and on the right.
In \MQL5\Include\DoEasy\Objects\Graph\WForms\TabField.mqh of the tab field object class in the method drawing the control frame depending on the header location, add drawing the line using the background color at the location of the header on the left and right:
//+------------------------------------------------------------------+ //| Draw the element frame depending on the header position | //+------------------------------------------------------------------+ void CTabField::DrawFrame(void) { //--- Set the initial coordinates int x1=0; int y1=0; int x2=this.Width()-1; int y2=this.Height()-1; //--- Get the tab header corresponding to the field CTabHeader *header=this.GetHeaderObj(); if(header==NULL) return; //--- Draw a rectangle that completely outlines the field this.DrawRectangle(x1,y1,x2,y2,this.BorderColor(),this.Opacity()); //--- Depending on the location of the header, draw a line on the edge adjacent to the header. //--- The line size is calculated from the heading size and corresponds to it with a one-pixel indent on each side //--- thus, visually the edge will not be drawn on the adjacent side of the header switch(header.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : this.DrawLine(header.CoordXRelative()+1,0,header.RightEdgeRelative()-2,0,this.BackgroundColor(),this.Opacity()); break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.DrawLine(header.CoordXRelative()+1,this.Height()-1,header.RightEdgeRelative()-2,this.Height()-1,this.BackgroundColor(),this.Opacity()); break; case CANV_ELEMENT_ALIGNMENT_LEFT : this.DrawLine(0,header.BottomEdgeRelative()-2,0,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity()); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : this.DrawLine(this.Width()-1,header.BottomEdgeRelative()-2,this.Width()-1,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity()); break; default: break; } } //+------------------------------------------------------------------+
Here all is simple. Get the pointer to the header object corresponding to this field, get its size from it and draw a line with the background color in the field area the header adjoins to in accordance with the location specified for the header. Visually, this erases the border between the field and the header, and the two objects begin to appear as one — the TabControl tab I am going to develop further.
In the TabControl object class file in \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh, declare four private methods for stretching header rows by width and height:
//--- Arrange the tab headers at the (1) top, (2) bottom, (3) left and (4) right void ArrangeTabHeadersTop(void); void ArrangeTabHeadersBottom(void); void ArrangeTabHeadersLeft(void); void ArrangeTabHeadersRight(void); //--- Stretch tab headers by control size void StretchHeaders(void); //--- Stretch tab headers by (1) control width and height when positioned on the (2) left and (3) right void StretchHeadersByWidth(void); void StretchHeadersByHeightLeft(void); void StretchHeadersByHeightRight(void); public:
Implement these methods outside the class body.
The method that stretches the tab headers to the size of the control:
//+------------------------------------------------------------------+ //| Stretch tab headers by control size | //+------------------------------------------------------------------+ void CTabControl::StretchHeaders(void) { //--- Leave if the headers are in one row if(!this.Multiline()) return; //--- Depending on the location of headers switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : this.StretchHeadersByWidth(); break; case CANV_ELEMENT_ALIGNMENT_LEFT : this.StretchHeadersByHeightLeft(); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : this.StretchHeadersByHeightRight(); break; default: break; } } //+------------------------------------------------------------------+
The method simply calls the appropriate methods depending on the location of the tab headers. For stretching in width, only one method is sufficient, since all headers are always located from left to right, while for stretching in height, it matters which side the headers are located on. When they are located on the left, they are arranged from bottom to top, and when they are located on the left, they go from top to bottom. Therefore, we have two separate methods for stretching by height for headers positioned on the left and right.
The method stretching tab headers by control width:
//+------------------------------------------------------------------+ //| Stretch tab headers by control width | //+------------------------------------------------------------------+ void CTabControl::StretchHeadersByWidth(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); if(last==NULL) return; //--- In the loop by the number of header rows for(int i=0;i<last.Row()+1;i++) { //--- Get the list with the row index equal to the loop index CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL); if(list_row==NULL) continue; //--- Get the width of the container, as well as the number of headers in a row, and calculate the width of each header int base_size=this.Width()-4; int num=list_row.Total(); int w=base_size/(num>0 ? num : 1); //--- In the loop by row headers for(int j=0;j<list_row.Total();j++) { //--- Get the current and previous headers from the list by loop index CTabHeader *header=list_row.At(j); CTabHeader *prev=list_row.At(j-1); if(header==NULL) continue; //--- If the header size is changed if(header.Resize(w,header.Height(),false)) { //--- Set new sizes for the header for pressed/unpressed states header.SetWidthOn(w+4); header.SetWidthOff(w); //--- If this is the first header in the row (there is no previous header in the list), //--- then it is not necessary to shift it - move on to the next iteration if(prev==NULL) continue; //--- Shift the header to the coordinate of the right edge of the previous header if(header.Move(prev.RightEdge(),header.CoordY())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } } } } } //+------------------------------------------------------------------+
Here we first find out the number of header rows. This number can be found by getting the last header from the list — it will contain the index of its row in the Row property. Since the row indices start from zero, we need to add one to the resulting value to specify the number of rows.
Next, we need to get a list of headers located in each row and stretch all the headers in it to the width of the object. Since we added the Row and Column values to the object properties, it became quite simple to get a list of the headers of one row — sort the list of all headers by row value and get a list containing pointers to objects with the specified row index. In the loop by the resulting list, we change the width of each header to the previously calculated value - the width of the container divided by the number of headers in the row. Instead of using the entire container width, we remove two pixels from the left and right, so that extreme headers do not go beyond the container when they are selected and increased in size. Since we divide the size by an unknown value in advance, we check if the divisor matches the value and divide by 1 in case of 0 to avoid division by zero. If the previous header is not present in the list (the loop index points to the very first header), the header does not need to be shifted anywhere. It remains in place while all subsequent ones need to be moved to the right edge of the previous header — all headers have changed their width becoming larger and overlapping each other.
The method that stretches tab headers to fit the height of the control when positioned to the left:
//+------------------------------------------------------------------+ //| Stretch tab headers by control height | //| when placed on the left | //+------------------------------------------------------------------+ void CTabControl::StretchHeadersByHeightLeft(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); if(last==NULL) return; //--- In the loop by the number of header rows for(int i=0;i<last.Row()+1;i++) { //--- Get the list with the row index equal to the loop index CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL); if(list_row==NULL) continue; //--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header int base_size=this.Height()-4; int num=list_row.Total(); int h=base_size/(num>0 ? num : 1); //--- In the loop by row headers for(int j=0;j<list_row.Total();j++) { //--- Get the current and previous headers from the list by loop index CTabHeader *header=list_row.At(j); CTabHeader *prev=list_row.At(j-1); if(header==NULL) continue; //--- Save the initial header height int h_prev=header.Height(); //--- If the header size is changed if(header.Resize(header.Width(),h,false)) { //--- Set new sizes for the header for pressed/unpressed states header.SetHeightOn(h+4); header.SetHeightOff(h); //--- If this is the first header in the row (there is no previous header in the list) if(prev==NULL) { //--- Calculate the Y offset int y_shift=header.Height()-h_prev; //--- Shift the header by its calculated offset and move on to the next one if(header.Move(header.CoordX(),header.CoordY()-y_shift)) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } continue; } //--- Move the header by the coordinate of the top edge of the previous header minus the height of the current one and its calculated offset if(header.Move(header.CoordX(),prev.CoordY()-header.Height())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } } } } } //+------------------------------------------------------------------+
The logic of the method is similar to the previous one, although it is a little more complicated here. Since headers are positioned to the left starting from the bottom edge of their container and the header anchor point is in its upper left corner, resizing it will cause the bottom edge of the header to be below the bottom edge of the container. Therefore, here we need to move the very first header up by the calculated offset. To do this, we need to save the header height before changing it and calculate by how much the size has been changed after the resize. We use the obtained value to move the very first header along the Y axis so that its bottom edge does not extend beyond its container.
The method that stretches tab headers to fit the height of the control when positioned to the right:
//+------------------------------------------------------------------+ //| Stretch tab headers by control height | //| when placed on the right | //+------------------------------------------------------------------+ void CTabControl::StretchHeadersByHeightRight(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); if(last==NULL) return; //--- In the loop by the number of header rows for(int i=0;i<last.Row()+1;i++) { //--- Get the list with the row index equal to the loop index CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL); if(list_row==NULL) continue; //--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header int base_size=this.Height()-4; int num=list_row.Total(); int h=base_size/(num>0 ? num : 1); //--- In the loop by row headers for(int j=0;j<list_row.Total();j++) { //--- Get the current and previous headers from the list by loop index CTabHeader *header=list_row.At(j); CTabHeader *prev=list_row.At(j-1); if(header==NULL) continue; //--- If the header size is changed if(header.Resize(header.Width(),h,false)) { //--- Set new sizes for the header for pressed/unpressed states header.SetHeightOn(h+4); header.SetHeightOff(h); //--- If this is the first header in the row (there is no previous header in the list), //--- then it is not necessary to shift it - move on to the next iteration if(prev==NULL) continue; //--- Shift the header to the coordinate of the bottom edge of the previous header if(header.Move(header.CoordX(),prev.BottomEdge())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } } } } } //+------------------------------------------------------------------+
The method is identical to the one that stretches the headers to fir the container width, but here we stretch them in height. Since here the headers are on the left, and their report goes from top to bottom, then you do not need to adjust the location of the first header after changing its size — its initial coordinates coincide with the coordinates of its placement point, and the object will increase downwards without going up beyond the container.
The method that creates the specified number of tabs has been changed since we need to calculate the initial coordinates and sizes based on the location of the headers. In order to place the headers on the left and right, we assign the header height and width, passed to the method, to the width and height accordingly. If the header is on the left, rotate the header vertically by 90°, if it is on the right — by 270:
//+------------------------------------------------------------------+ //| Create the specified number of tabs | //+------------------------------------------------------------------+ bool CTabControl::CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="") { //--- Calculate the size and initial coordinates of the tab title int w=(tab_w==0 ? this.ItemWidth() : tab_w); int h=(tab_h==0 ? this.ItemHeight() : tab_h); //--- In the loop by the number of tabs CTabHeader *header=NULL; CTabField *field=NULL; for(int i=0;i<total;i++) { //--- Depending on the location of tab titles, set their initial coordinates int header_x=2; int header_y=0; int header_w=w; int header_h=h; //--- Set the current X and Y coordinate depending on the location of the tab headers switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : header_w=w; header_h=h; header_x=(header==NULL ? 2 : header.RightEdgeRelative()); header_y=0; break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : header_w=w; header_h=h; header_x=(header==NULL ? 2 : header.RightEdgeRelative()); header_y=this.Height()-header_h; break; case CANV_ELEMENT_ALIGNMENT_LEFT : header_w=h; header_h=w; header_x=2; header_y=(header==NULL ? this.Height()-header_h-2 : header.CoordYRelative()-header_h); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : header_w=h; header_h=w; header_x=this.Width()-header_w; header_y=(header==NULL ? 2 : header.BottomEdgeRelative()); break; default: break; } //--- Create the TabHeader object if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,header_w,header_h,clrNONE,255,this.Active(),false)) { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1)); return false; } header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,i); if(header==NULL) { ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1)); return false; } header.SetBase(this.GetObject()); header.SetPageNumber(i); header.SetGroup(this.Group()+1); header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true); header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN); header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER); header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true); header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON); header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON); header.SetBorderStyle(FRAME_STYLE_SIMPLE); header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true); header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN); header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER); header.SetAlignment(this.Alignment()); header.SetPadding(this.HeaderPaddingWidth(),this.HeaderPaddingHeight(),this.HeaderPaddingWidth(),this.HeaderPaddingHeight()); if(header_text!="" && header_text!=NULL) this.SetHeaderText(header,header_text+string(i+1)); else this.SetHeaderText(header,"TabPage"+string(i+1)); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT) header.SetFontAngle(90); if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT) header.SetFontAngle(270); header.SetTabSizeMode(this.TabSizeMode()); //--- Save the initial height of the header and set its size in accordance with the header size setting mode int h_prev=header_h; header.SetSizes(header_w,header_h); //--- Get the Y offset of the header position after changing its height and //--- shift it by the calculated value only for headers on the left int y_shift=header.Height()-h_prev; if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : 0))) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } //--- Depending on the location of the tab headers, set the initial coordinates of the tab fields int field_x=0; int field_y=0; int field_w=this.Width(); int field_h=this.Height()-header.Height(); int header_shift=0; switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : field_x=0; field_y=header.BottomEdgeRelative(); field_w=this.Width(); field_h=this.Height()-header.Height(); break; case CANV_ELEMENT_ALIGNMENT_BOTTOM : field_x=0; field_y=0; field_w=this.Width(); field_h=this.Height()-header.Height(); break; case CANV_ELEMENT_ALIGNMENT_LEFT : field_x=header.RightEdgeRelative(); field_y=0; field_h=this.Height(); field_w=this.Width()-header.Width(); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : field_x=0; field_y=0; field_h=this.Height(); field_w=this.Width()-header.Width(); break; default: break; } //--- Create the TabField object (tab field) if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,field_w,field_h,clrNONE,255,true,false)) { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1)); return false; } field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,i); if(field==NULL) { ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1)); return false; } field.SetBase(this.GetObject()); field.SetPageNumber(i); field.SetGroup(this.Group()+1); field.SetBorderSizeAll(1); field.SetBorderStyle(FRAME_STYLE_SIMPLE); field.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true); field.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true); field.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN); field.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER); field.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true); field.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN); field.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER); field.SetForeColor(CLR_DEF_FORE_COLOR,true); field.SetPadding(this.FieldPaddingLeft(),this.FieldPaddingTop(),this.FieldPaddingRight(),this.FieldPaddingBottom()); field.Hide(); } //--- Arrange all titles in accordance with the specified display modes and select the specified tab this.ArrangeTabHeaders(); this.Select(selected_page,true); return true; } //+------------------------------------------------------------------+
The logic of the method and improvements are described in the comments to the code, and the main features are indicated before the listing of the method and highlighted with color. To locate the headers on the left, we need to save the header size before changing it, calculate the offset and move the resized header to the correct position.
The method that places tab headers on top, described in the previous article, has also undergone changes:
//+------------------------------------------------------------------+ //| Arrange tab headers on top | //+------------------------------------------------------------------+ void CTabControl::ArrangeTabHeadersTop(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Declare the variables int col=0; // Column int row=0; // Row int x1_base=2; // Initial X coordinate int x2_base=this.RightEdgeRelative()-2; // Final X coordinate int x_shift=0; // Shift the tab set for calculating their exit beyond the container int n=0; // The variable for calculating the column index relative to the loop index //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next tab header object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If the flag for positioning headers in several rows is set if(this.Multiline()) { //--- Calculate the value of the right edge of the header, taking into account that //--- the origin always comes from the left edge of TabControl + 2 pixels int x2=header.RightEdgeRelative()-x_shift; //--- If the calculated value does not go beyond the right edge of the TabControl minus 2 pixels, //--- set the column number equal to the loop index minus the value in the n variable if(x2<x2_base) col=i-n; //--- If the calculated value goes beyond the right edge of the TabControl minus 2 pixels, else { //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels), //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row row++; x_shift=header.CoordXRelative()-2; n=i; col=0; } //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates header.SetTabLocation(row,col); if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } } //--- If only one row of headers is allowed else { } } //--- The location of all tab titles is set. Now place them all together with the fields //--- according to the header row and column indices. //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); //--- If the object is received if(last!=NULL) { //--- If the mode of stretching headers to the width of the container is set, call the stretching method if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL) this.StretchHeaders(); //--- If this is not the first row (with index 0) if(last.Row()>0) { //--- Calculate the offset of the tab field Y coordinate int y_shift=last.Row()*last.Height(); //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- get the tab field corresponding to the received header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- shift the tab header by the calculated row coordinates if(header.Move(header.CoordX(),header.CoordY()+y_shift)) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } //--- shift the tab field by the calculated shift if(field.Move(field.CoordX(),field.CoordY()+y_shift)) { field.SetCoordXRelative(field.CoordX()-this.CoordX()); field.SetCoordYRelative(field.CoordY()-this.CoordY()); //--- change the size of the shifted field by the value of its shift field.Resize(field.Width(),field.Height()-y_shift,false); } } } } } //+------------------------------------------------------------------+
The logic of the method is fully described in the comments to the code. I will not repeat it here.
The method that places tab headers at the bottom:
//+------------------------------------------------------------------+ //| Arrange tab headers at the bottom | //+------------------------------------------------------------------+ void CTabControl::ArrangeTabHeadersBottom(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Declare the variables int col=0; // Column int row=0; // Row int x1_base=2; // Initial X coordinate int x2_base=this.RightEdgeRelative()-2; // Final X coordinate int x_shift=0; // Shift the tab set for calculating their exit beyond the container int n=0; // The variable for calculating the column index relative to the loop index //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next tab header object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If the flag for positioning headers in several rows is set if(this.Multiline()) { //--- Calculate the value of the right edge of the header, taking into account that //--- the origin always comes from the left edge of TabControl + 2 pixels int x2=header.RightEdgeRelative()-x_shift; //--- If the calculated value does not go beyond the right edge of the TabControl minus 2 pixels, //--- set the column number equal to the loop index minus the value in the n variable if(x2<x2_base) col=i-n; //--- If the calculated value goes beyond the right edge of the TabControl minus 2 pixels, else { //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels), //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row row++; x_shift=header.CoordXRelative()-2; n=i; col=0; } //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates header.SetTabLocation(row,col); if(header.Move(header.CoordX()-x_shift,header.CoordY()+header.Row()*header.Height())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } } //--- If only one row of headers is allowed else { } } //--- The location of all tab titles is set. Now place them all together with the fields //--- according to the header row and column indices. //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); //--- If the object is received if(last!=NULL) { //--- If the mode of stretching headers to the width of the container is set, call the stretching method if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL) this.StretchHeaders(); //--- If this is not the first row (with index 0) if(last.Row()>0) { //--- Calculate the offset of the tab field Y coordinate int y_shift=last.Row()*last.Height(); //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- get the tab field corresponding to the received header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- shift the tab header by the calculated row coordinates if(header.Move(header.CoordX(),header.CoordY()-y_shift)) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } //--- shift the tab field by the calculated shift if(field.Move(field.CoordX(),field.CoordY())) { field.SetCoordXRelative(field.CoordX()-this.CoordX()); field.SetCoordYRelative(field.CoordY()-this.CoordY()); //--- change the size of the shifted field by the value of its shift field.Resize(field.Width(),field.Height()-y_shift,false); } } } } } //+------------------------------------------------------------------+
The method is identical to the one that puts headers on top. The only difference is in the direction of header rows offset, as the headers are located at the bottom and move in inverse manner compared to the previous method.
The method locating tab headers on the left:
//+------------------------------------------------------------------+ //| Arrange tab headers on the left | //+------------------------------------------------------------------+ void CTabControl::ArrangeTabHeadersLeft(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Declare the variables int col=0; // Column int row=0; // Row int y1_base=this.BottomEdgeRelative()-2; // Initial Y coordinate int y2_base=2; // Final Y coordinate int y_shift=0; // Shift the tab set for calculating their exit beyond the container int n=0; // The variable for calculating the column index relative to the loop index //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next tab header object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If the flag for positioning headers in several rows is set if(this.Multiline()) { //--- Calculate the value of the upper edge of the header, taking into account that //--- the origin always comes from the bottom edge of TabControl minus 2 pixels int y2=header.CoordYRelative()+y_shift; //--- If the calculated value does not go beyond the upper edge of the TabControl minus 2 pixels, //--- set the column number equal to the loop index minus the value in the n variable if(y2>=y2_base) col=i-n; //--- If the calculated value goes beyond the upper edge of the TabControl minus 2 pixels, else { //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels), //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row row++; y_shift=this.BottomEdge()-header.BottomEdge()-2; n=i; col=0; } //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates header.SetTabLocation(row,col); if(header.Move(header.CoordX()-header.Row()*header.Width(),header.CoordY()+y_shift)) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } } //--- If only one row of headers is allowed else { } } //--- The location of all tab titles is set. Now place them all together with the fields //--- according to the header row and column indices. //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); //--- If the object is received if(last!=NULL) { //--- If the mode of stretching headers to the width of the container is set, call the stretching method if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL) this.StretchHeaders(); //--- If this is not the first row (with index 0) if(last.Row()>0) { //--- Calculate the offset of the tab field X coordinate int x_shift=last.Row()*last.Width(); //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- get the tab field corresponding to the received header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- shift the tab header by the calculated row coordinates if(header.Move(header.CoordX()+x_shift,header.CoordY())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } //--- shift the tab field by the calculated shift if(field.Move(field.CoordX()+x_shift,field.CoordY())) { field.SetCoordXRelative(field.CoordX()-this.CoordX()); field.SetCoordYRelative(field.CoordY()-this.CoordY()); //--- change the size of the shifted field by the value of its shift field.Resize(field.Width()-x_shift,field.Height(),false); } } } } } //+------------------------------------------------------------------+
Here, the headers are on the left and the rows are shifted along the X axis. Otherwise, the logic is identical to the previous methods.
The method that positions tab headers to the right:
//+------------------------------------------------------------------+ //| Arrange tab headers to the right | //+------------------------------------------------------------------+ void CTabControl::ArrangeTabHeadersRight(void) { //--- Get the list of tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- Declare the variables int col=0; // Column int row=0; // Row int y1_base=2; // Initial Y coordinate int y2_base=this.BottomEdgeRelative()-2; // Final Y coordinate int y_shift=0; // Shift the tab set for calculating their exit beyond the container int n=0; // The variable for calculating the column index relative to the loop index //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next tab header object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- If the flag for positioning headers in several rows is set if(this.Multiline()) { //--- Calculate the value of the bottom edge of the header, taking into account that //--- the origin always comes from the upper edge of TabControl + 2 pixels int y2=header.BottomEdgeRelative()-y_shift; //--- If the calculated value does not go beyond the bottom edge of the TabControl minus 2 pixels, //--- set the column number equal to the loop index minus the value in the n variable if(y2<y2_base) col=i-n; //--- If the calculated value goes beyond the bottom edge of the TabControl minus 2 pixels, else { //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl bottom edge + 2 pixels), //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row row++; y_shift=header.CoordYRelative()-2; n=i; col=0; } //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates header.SetTabLocation(row,col); if(header.Move(header.CoordX()+header.Row()*header.Width(),header.CoordY()-y_shift)) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); } } //--- If only one row of headers is allowed else { } } //--- The location of all tab titles is set. Now place them all together with the fields //--- according to the header row and column indices. //--- Get the last title in the list CTabHeader *last=this.GetTabHeader(list.Total()-1); //--- If the object is received if(last!=NULL) { //--- If the mode of stretching headers to the width of the container is set, call the stretching method if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL) this.StretchHeaders(); //--- If this is not the first row (with index 0) if(last.Row()>0) { //--- Calculate the offset of the tab field X coordinate int x_shift=last.Row()*last.Width(); //--- In a loop by the list of headers, for(int i=0;i<list.Total();i++) { //--- get the next object CTabHeader *header=list.At(i); if(header==NULL) continue; //--- get the tab field corresponding to the received header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- shift the tab header by the calculated row coordinates if(header.Move(header.CoordX()-x_shift,header.CoordY())) { header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); //--- change the tab field size to the X offset value field.Resize(field.Width()-x_shift,field.Height(),false); } } } } } //+------------------------------------------------------------------+
The logic is similar to that of the previous method, but the row offsets are mirrored because the headers are on the right.
All of the above methods are commented in detail in the code — we will leave them for independent study. If you have any questions, feel free to ask them in the comments below.
Now we can test all the changes and improvements. In order to arrange the headers of tabs in one row, we need to be able to crop the visible/invisible part of the graphical element. Therefore, if we select the mode of placing headers in one row (Multiline mode is off) while there are many tabs, then all headers will be lined up going beyond the control. I will deal with this issue in subsequent articles. I have left "stubs" in the methods of the considered classes for this mode. I will enter the code for handling this mode there.
Test
To perform the test, I will use the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part116\ as TestDoEasy116.mq5.
In the EA inputs, add the variables to specify the Multiline mode and the side where the tab headers are placed:
//--- input parameters sinput bool InpMovable = true; // Panel Movable flag sinput ENUM_INPUT_YES_NO InpAutoSize = INPUT_YES; // Panel Autosize sinput ENUM_AUTO_SIZE_MODE InpAutoSizeMode = AUTO_SIZE_MODE_GROW; // Panel Autosize mode sinput ENUM_BORDER_STYLE InpFrameStyle = BORDER_STYLE_SIMPLE; // Label border style sinput ENUM_ANCHOR_POINT InpTextAlign = ANCHOR_CENTER; // Label text align sinput ENUM_INPUT_YES_NO InpTextAutoSize = INPUT_NO; // Label autosize sinput ENUM_ANCHOR_POINT InpCheckAlign = ANCHOR_LEFT; // Check flag align sinput ENUM_ANCHOR_POINT InpCheckTextAlign = ANCHOR_LEFT; // Check label text align sinput ENUM_CHEK_STATE InpCheckState = CHEK_STATE_UNCHECKED; // Check flag state sinput ENUM_INPUT_YES_NO InpCheckAutoSize = INPUT_YES; // CheckBox autosize sinput ENUM_BORDER_STYLE InpCheckFrameStyle = BORDER_STYLE_NONE; // CheckBox border style sinput ENUM_ANCHOR_POINT InpButtonTextAlign = ANCHOR_CENTER; // Button text align sinput ENUM_INPUT_YES_NO InpButtonAutoSize = INPUT_YES; // Button autosize sinput ENUM_AUTO_SIZE_MODE InpButtonAutoSizeMode= AUTO_SIZE_MODE_GROW; // Button Autosize mode sinput ENUM_BORDER_STYLE InpButtonFrameStyle = BORDER_STYLE_NONE; // Button border style sinput bool InpButtonToggle = true ; // Button toggle flag sinput bool InpButtListMSelect = false; // ButtonListBox Button MultiSelect flag sinput bool InpListBoxMColumn = true; // ListBox MultiColumn flag sinput bool InpTabCtrlMultiline = true; // Tab Control Multiline flag sinput ENUM_ELEMENT_ALIGNMENT InpHeaderAlignment = ELEMENT_ALIGNMENT_TOP; // TabHeader Alignment sinput ENUM_ELEMENT_TAB_SIZE_MODE InpTabPageSizeMode = ELEMENT_TAB_SIZE_MODE_NORMAL; // TabHeader Size Mode //--- global variables
Let's slightly increase the width of the created panel (by 10 pixels):
//--- Create WinForms Panel object CPanel *pnl=NULL; pnl=engine.CreateWFPanel("WFPanel",50,50,410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false); if(pnl!=NULL) {
and the width of the second GroupBox container — by 12 pixels:
//--- Create the GroupBox2 WinForms object CGroupBox *gbox2=NULL; //--- The indent from the attached panels by 6 pixels will be the Y coordinate of GrotupBox2 w=gbox1.Width()+12; int x=gbox1.RightEdgeRelative()+1; int h=gbox1.BottomEdgeRelative()-6; //--- If the attached GroupBox object is created if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false)) {
All this is done only because we do not have the functionality of cropping the invisible part of graphical elements, and all WinForm objects located on their parent objects and having the size larger than the container (parent object) will go beyond its limits. For example, CheckBox placed on the tab field will either extend outside the tab field when headers are on the left, or also go outside the tab field and cover the tab headers on the right side of TabControl. While there is not enough functionality yet, I have to hide such shortcomings :)
In the OnInit() handler, after creating TabControl, set the position of the tab headers and the permission for headers to be arranged in multiple rows specified in the EA inputs:
//--- Create the TabControl object gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,4,12,gbox2.Width()-12,gbox2.Height()-20,clrNONE,255,true,false); //--- get the pointer to the TabControl object by its index in the list of bound objects of the TabControl type CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0); //--- If TabControl is created and the pointer to it is received if(tab_ctrl!=NULL) { //--- Set the location of the tab titles on the element and the tab text, as well as create nine tabs tab_ctrl.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode); tab_ctrl.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment); tab_ctrl.SetMultiline(InpTabCtrlMultiline); tab_ctrl.SetHeaderPadding(6,0); tab_ctrl.CreateTabPages(9,0,50,16,TextByLanguage("Вкладка","TabPage"));
When creating the ListBox control in the third tab of TabControl, set its Y coordinate closer to the top of the tab:
//--- Create the ListBox object on the third tab int lbw=146; if(!InpListBoxMColumn) lbw=100; tab_ctrl.CreateNewElement(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,4,2,lbw,60,clrNONE,255,true,false); //--- get the pointer to the ListBox object from the third tab by its index in the list of attached objects of the ListBox type
Previously, the object was located at the coordinate 12, which leads to its going beyond the tab field from the bottom in case of a multi-row arrangement of tab headers (since the size of the tab field decreases in proportion to the increase in the number of header rows).
Compile the EA and launch it on the chart:
As we can see, the layout of the tab titles on the left and right works correctly. There are some shortcomings we will describe and fix in the next article but so far everything is good.
What's next?
In the next article, I will continue working on TabControl.
*Previous articles within the series:
DoEasy. Controls (Part 10): WinForms objects — Animating the interface
DoEasy. Controls (Part 11): WinForms objects — groups, CheckedListBox WinForms object
DoEasy. Controls (Part 12): Base list object, ListBox and ButtonListBox WinForms objects
DoEasy. Controls (Part 13): Optimizing interaction of WinForms objects with the mouse, starting the development of the TabControl WinForms object
DoEasy. Controls (Part 14): New algorithm for naming graphical elements. Continuing work on the TabControl WinForms object
DoEasy. Controls (Part 15): TabControl WinForms object — several rows of tab headers, tab handling methods
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/11356
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use