![DoEasy. Controls (Part 19): Scrolling tabs in TabControl, WinForms object events](https://c.mql5.com/2/49/doeasy_019_600x314.jpg)
DoEasy. Controls (Part 19): Scrolling tabs in TabControl, WinForms object events
Contents
Concept
In the previous article, I tested scrolling the tab header bar when selecting a partially hidden header — it shifted to the left becoming fully visible. Here I will implement the methods for shifting the header bar left-right and up-down based on the created functionality. When scrolling headers up and down, it will matter where the heading line is located — on the left or on the right. All of these methods will be called both when a partially hidden header is selected to display it in its entirety, and when the scroll buttons are pressed — up, down, left and right. In order to understand which button and in which control was pressed, we will use the event model — when the button is pressed, an event will be sent, which the library will intercept and handle, sending it to the event handler of the element the pressed button is attached to for its further handling inside the control. I will use this model to work in other controls.
At the moment, the names of the main and base graphical elements are sent to the event handler in the 'sparam' parameter. The main control is the one featuring an object, in which an event has occurred. It is considered the base control here. But, if this base control is a composite one and features some more controls attached to it (for example, buttons), and an event has already occurred in them, then we will not be able to find the base element, since it is not attached directly to the main object. To avoid this, in the 'sparam' parameter, pass the names of the main and base objects plus the name of the object the event has occurred in.
Thus, we will have input data for the main object, for the base one, inside which there was an event in one of the elements attached to it, and the name of the element the event has occurred in. To define the control, in which we clicked the button (a special case), we will send the type of this base object in the 'dparam' parameter. Thus, knowing the type of the base object, we get a list of all controls in the main object with the type recorded in 'dparam'. Then in a loop through all these objects, we look for an object attached to it with the name passed last in 'sparam', in which there was an event (clicking on the control).
At the moment, such a structure does not look very reliable in the sense of its versatility for handling more complex objects. But at this stage, it is quite sufficient for the library development. When creating more complex controls with a more sophisticated nesting of objects within each other, we will get a clear practical example of how it is necessary to correctly and universally identify events in such objects in the library (remember the "simple-to-complex" principle).
Currently, the library allows hiding and displaying controls. In order to display or hide elements, it is enough to hide only the main or base element. All elements attached to it will be hidden or displayed accordingly. But if we keep in mind that the control should contain the objects displayed regardless of the main object, i.e. their visibility is set inside the control itself, and if such an object is hidden and its parent element is displayed, then such an object should not be displayed. In such situations, we need to manage the visibility of these objects from their base control. In order to implement this possibility, we need to introduce one more property of the graphical element — its display flag. Then, if the main object was hidden and then displayed, the control the display flag was cleared for will still remain hidden until it is explicitly displayed from its base control.
But enough theory, let's get down to work...
Improving library classes
Since I am doing everything with the future in mind, I will not create our own event IDs for the scroll control buttons. I will create them with an eye on further new controls that will also use buttons (scroll bar, drop-down lists etc). In other words, let's create some generic events of clicking on the scroll or pop-up control.
In \MQL5\Include\DoEasy\Defines.mqh, namely in the list of possible WinForms control events, add new enumeration constants:
//+------------------------------------------------------------------+ //| List of possible WinForms control events | //+------------------------------------------------------------------+ enum ENUM_WF_CONTROL_EVENT { WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,// No event WF_CONTROL_EVENT_CLICK, // "Click on the control" event WF_CONTROL_EVENT_CLICK_CANCEL, // "Canceling the click on the control" event WF_CONTROL_EVENT_TAB_SELECT, // "TabControl tab selection" event WF_CONTROL_EVENT_CLICK_SCROLL_LEFT, // "Clicking the control left button" event WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT, // "Clicking the control right button" event WF_CONTROL_EVENT_CLICK_SCROLL_UP, // "Clicking the control up button" event WF_CONTROL_EVENT_CLICK_SCROLL_DOWN, // "Clicking the control down button" event }; #define WF_CONTROL_EVENTS_NEXT_CODE (WF_CONTROL_EVENT_TAB_SELECT+1) // The code of the next event after the last graphical element event code //+------------------------------------------------------------------+
Here, just in case, I have created an event ID for canceling the click on the control (in some cases, it is possible to handle such an event) and added general events for controls that have buttons managing the appearance of the control or making it possible to interact with it.
In the enumeration of canvas-based graphical element integer properties, add a new property and increase the total number of properties from 96 to 97:
//+------------------------------------------------------------------+ //| 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_VISIBLE_AREA_WIDTH, // Visibility scope width CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT, // Visibility scope height CANV_ELEMENT_PROP_DISPLAYED, // Non-hidden control display flag CANV_ELEMENT_PROP_GROUP, // Group the graphical element belongs to CANV_ELEMENT_PROP_ZORDER, // Priority of a graphical object for receiving the event of clicking on a chart //---... //---... 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 (97) // Total number of integer properties #define CANV_ELEMENT_PROP_INTEGER_SKIP (0) // Number of integer properties not used in sorting //+------------------------------------------------------------------+
The flag for displaying a non-hidden control means that if the control is not hidden, but this flag is cleared (false), the control is not displayed. In other words, if the main control is displayed, then its descendants this flag is cleared for will still remain hidden until they are forced to be displayed by setting this flag to true and calling the Show() method.
Add a new property to the list of possible criteria of sorting graphical elements on 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_VISIBLE_AREA_WIDTH, // Sort by visibility scope width SORT_BY_CANV_ELEMENT_VISIBLE_AREA_HEIGHT, // Sort by visibility scope height SORT_BY_CANV_ELEMENT_DISPLAYED, // Sort by non-hidden control display flag SORT_BY_CANV_ELEMENT_GROUP, // Sort by a group the graphical element belongs to SORT_BY_CANV_ELEMENT_ZORDER, // Sort by the priority of a graphical object for receiving the event of clicking on a chart //---... //---... 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 will be able to select and sort the lists of graphical element objects by this new property.
In \MQL5\Include\DoEasy\Data.mqh, add new message indices of the library:
MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY, // Request outside the array MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY, // Failed to convert graphical object coordinates to screen ones MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY, // Failed to convert time/price coordinates to screen ones MSG_LIB_SYS_FAILED_ENQUEUE_EVENT, // Failed to put the event in the chart event queue
...
MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH, // Visibility scope width MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT, // Visibility scope height MSG_CANV_ELEMENT_PROP_DISPLAYED, // Non-hidden control display flag MSG_CANV_ELEMENT_PROP_ENABLED, // Element availability flag MSG_CANV_ELEMENT_PROP_FORE_COLOR, // Default text color for all control objects
and the message texts corresponding to the newly added indices:
{"Запрос за пределами массива","Data requested outside the array"}, {"Не удалось преобразовать координаты графического объекта в экранные","Failed to convert graphics object coordinates to screen coordinates"}, {"Не удалось преобразовать координаты время/цена в экранные","Failed to convert time/price coordinates to screen coordinates"}, {"Не удалось поставить событие в очередь событий графика","Failed to put event in chart event queue"},
...
{"Ширина области видимости","Width of object visibility area"}, {"Высота области видимости","Height of object visibility area"}, {"Флаг отображения не скрытого элемента управления","Flag that sets the display of a non-hidden control"}, {"Флаг доступности элемента","Element Availability Flag"}, {"Цвет текста по умолчанию для всех объектов элемента управления","Default text color for all objects in the control"},
Let's improve the graphical element object class in \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.
In the object structure, add a newly added property:
private: int m_shift_coord_x; // Offset of the X coordinate relative to the base object int m_shift_coord_y; // Offset of the Y coordinate relative to the base object struct SData { //--- Object integer properties int id; // Element ID int type; // Graphical element type //---... //---... int visible_area_w; // Visibility scope width int visible_area_h; // Visibility scope height bool displayed; // Non-hidden control display flag //--- Object real properties //--- Object string properties uchar name_obj[64]; // Graphical element object name uchar name_res[64]; // Graphical resource name uchar text[256]; // Graphical element text uchar descript[256]; // Graphical element description }; SData m_struct_obj; // Object structure uchar m_uchar_array[]; // uchar array of the object structure
All properties of the object are set in the structure of the object in order to save and restore the object from the file.
Add two new methods for setting and getting the object display property to the block of methods for simplified access to object properties:
//+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+ //--- Set the (1) X, (2) Y coordinates, (3) element width, (4) height, (5) right (6) and bottom edge, virtual bool SetCoordX(const int coord_x); virtual bool SetCoordY(const int coord_y); virtual bool SetWidth(const int width); virtual bool SetHeight(const int height); void SetRightEdge(void) { this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge()); } void SetBottomEdge(void) { this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge()); } //--- Set the shift of the (1) left, (2) top, (3) right, (4) bottom edge of the active area relative to the element, //--- (5) all shifts of the active area edges relative to the element, (6) opacity void SetActiveAreaLeftShift(const int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,fabs(value)); } void SetActiveAreaRightShift(const int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,fabs(value)); } void SetActiveAreaTopShift(const int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,fabs(value)); } void SetActiveAreaBottomShift(const int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,fabs(value)); } void SetActiveAreaShift(const int left_shift,const int bottom_shift,const int right_shift,const int top_shift); void SetOpacity(const uchar value,const bool redraw=false); //--- (1) Set and (2) return the flag for displaying a non-hidden control void SetDisplayed(const bool flag) { this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,flag); } bool Displayed(void) { return (bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED); } //--- (1) Set and (2) return the graphical element type void SetTypeElement(const ENUM_GRAPH_ELEMENT_TYPE type) { CGBaseObj::SetTypeElement(type); this.SetProperty(CANV_ELEMENT_PROP_TYPE,type); } ENUM_GRAPH_ELEMENT_TYPE TypeGraphElement(void) const { return (ENUM_GRAPH_ELEMENT_TYPE)this.GetProperty(CANV_ELEMENT_PROP_TYPE); }
The methods simply write the passed flag to the object property and return the value set there.
In both class constructors, add the default value to the new property:
//+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const int element_id, const int element_num, const long chart_id, const int wnd_num, const string descript, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable=true, const bool activity=true, const bool redraw=false) : m_shadow(false) { this.SetTypeElement(element_type); this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=NULL; this.m_element_base=NULL; this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND); this.m_name=this.CreateNameGraphElement(element_type); this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id); this.m_subwindow=wnd_num; this.SetFont(DEF_FONT,DEF_FONT_SIZE); this.m_text_anchor=0; this.m_text_x=0; this.m_text_y=0; this.SetBackgroundColor(colour,true); this.SetOpacity(opacity); this.m_shift_coord_x=0; this.m_shift_coord_y=0; if(::ArrayResize(this.m_array_colors_bg,1)==1) this.m_array_colors_bg[0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1) this.m_array_colors_bg_dwn[0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1) this.m_array_colors_bg_ovr[0]=this.BackgroundColor(); if(this.Create(chart_id,wnd_num,x,y,w,h,redraw)) { this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID()); // Chart ID //---... //---... this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w); // Visibility scope width this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h); // Visibility scope height this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true); // Non-hidden control display flag //--- this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM); // Graphical element affiliation this.SetProperty(CANV_ELEMENT_PROP_ZORDER,0); // Priority of a graphical object for receiving the event of clicking on a chart this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL); // Font width type //---... //---... this.SetProperty(CANV_ELEMENT_PROP_TEXT,""); // Graphical element text this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript); // Graphical element description this.SetVisibleFlag(false,false); } else { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj()); } } //+------------------------------------------------------------------+ //| Protected constructor | //+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const long chart_id, const int wnd_num, const string descript, const int x, const int y, const int w, const int h) : m_shadow(false) { this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=NULL; this.m_element_base=NULL; this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND); this.m_name=this.CreateNameGraphElement(element_type); this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id); this.m_subwindow=wnd_num; this.m_type_element=element_type; this.SetFont(DEF_FONT,DEF_FONT_SIZE); this.m_text_anchor=0; this.m_text_x=0; this.m_text_y=0; this.SetBackgroundColor(CLR_CANV_NULL,true); this.SetOpacity(0); this.m_shift_coord_x=0; this.m_shift_coord_y=0; if(::ArrayResize(this.m_array_colors_bg,1)==1) this.m_array_colors_bg[0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1) this.m_array_colors_bg_dwn[0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1) this.m_array_colors_bg_ovr[0]=this.BackgroundColor(); if(this.Create(chart_id,wnd_num,x,y,w,h,false)) { this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID()); // Chart ID //---... //---... this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w); // Visibility scope width this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h); // Visibility scope height this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true); // Non-hidden control display flag //--- this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM); // Graphical element affiliation this.SetProperty(CANV_ELEMENT_PROP_ZORDER,0); // Priority of a graphical object for receiving the event of clicking on a chart this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL); // Font width type //---... //---... this.SetProperty(CANV_ELEMENT_PROP_TEXT,""); // Graphical element text this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript); // Graphical element description this.SetVisibleFlag(false,false); } else { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj()); } } //+------------------------------------------------------------------+
By default, an object is set to be visible and displayed when the visibility of the base or main object is turned on. To set the mode of manual control of the object visibility, the value of the flag should be set to false. In this case, if the base or main object was hidden and then displayed, then the current object will still remain in a hidden state, and in order to display it, we will need to set the Displayed property to true and call the Show() method of the object.
In the method creating an object structure, set the object property value to the appropriate structure field:
//+------------------------------------------------------------------+ //| Create the object structure | //+------------------------------------------------------------------+ bool CGCnvElement::ObjectToStruct(void) { //--- Save integer properties this.m_struct_obj.id=(int)this.GetProperty(CANV_ELEMENT_PROP_ID); // Element ID this.m_struct_obj.type=(int)this.GetProperty(CANV_ELEMENT_PROP_TYPE); // Graphical element type //---... //---... this.m_struct_obj.belong=(int)this.GetProperty(CANV_ELEMENT_PROP_BELONG); // Graphical element affiliation this.m_struct_obj.number=(int)this.GetProperty(CANV_ELEMENT_PROP_NUM); // Element ID in the list //---... //---... this.m_struct_obj.visible_area_x=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_X); // Visibility scope X coordinate this.m_struct_obj.visible_area_y=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_Y); // Visibility scope Y coordinate //---... //---... this.m_struct_obj.visible_area_w=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH); // Visibility scope width this.m_struct_obj.visible_area_h=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT); // Visibility scope height this.m_struct_obj.displayed=(bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED); // Flag for displaying a non-hidden control this.m_struct_obj.zorder=this.GetProperty(CANV_ELEMENT_PROP_ZORDER); // Priority of a graphical object for receiving the on-chart mouse click event this.m_struct_obj.enabled=(bool)this.GetProperty(CANV_ELEMENT_PROP_ENABLED); // Element availability flag this.m_struct_obj.fore_color=(color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR); // Default text color for all control objects //---... //---... this.m_struct_obj.fore_color_opacity=(uchar)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY); // Opacity of the default text color for all control objects this.m_struct_obj.background_color=(color)this.GetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR); // Element background color this.m_struct_obj.tab_alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT); // Location of tabs inside the control this.m_struct_obj.alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_ALIGNMENT); // Location of an object inside the control //--- Save real properties //--- Save string properties ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ),this.m_struct_obj.name_obj); // Graphical element object name ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_RES),this.m_struct_obj.name_res); // Graphical resource name ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_TEXT),this.m_struct_obj.text); // Graphical element text ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_DESCRIPTION),this.m_struct_obj.descript);// Graphical element description //--- Save the structure to the uchar array ::ResetLastError(); if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,true); return false; } return true; } //+------------------------------------------------------------------+
In the method that creates an object from a structure,set the value of the new object property from the corresponding structure field:
//+------------------------------------------------------------------+ //| Create the object from the structure | //+------------------------------------------------------------------+ void CGCnvElement::StructToObject(void) { //--- Save integer properties this.SetProperty(CANV_ELEMENT_PROP_ID,this.m_struct_obj.id); // Element ID this.SetProperty(CANV_ELEMENT_PROP_TYPE,this.m_struct_obj.type); // Graphical element type //---... //---... this.SetProperty(CANV_ELEMENT_PROP_BELONG,this.m_struct_obj.belong); // Graphical element affiliation this.SetProperty(CANV_ELEMENT_PROP_NUM,this.m_struct_obj.number); // Element index in the list //---... //---... this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,this.m_struct_obj.visible_area_h); // Visibility scope height this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,this.m_struct_obj.displayed); // Non-hidden control display flag this.SetProperty(CANV_ELEMENT_PROP_ZORDER,this.m_struct_obj.zorder); // Priority of a graphical object for receiving the event of clicking on a chart this.SetProperty(CANV_ELEMENT_PROP_ENABLED,this.m_struct_obj.enabled); // Element availability flag //---... //---... this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR,this.m_struct_obj.fore_color); // Default text color for all control objects this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY,this.m_struct_obj.fore_color_opacity); // Opacity of the default text color for all control objects this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,this.m_struct_obj.tab_alignment); // Location of tabs inside the control this.SetProperty(CANV_ELEMENT_PROP_ALIGNMENT,this.m_struct_obj.alignment); // Location of an object inside the control //--- Save real properties //--- Save string properties this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,::CharArrayToString(this.m_struct_obj.name_obj)); // Graphical element object name this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,::CharArrayToString(this.m_struct_obj.name_res)); // Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_TEXT,::CharArrayToString(this.m_struct_obj.text)); // Graphical element text this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,::CharArrayToString(this.m_struct_obj.descript));// Graphical element description } //+------------------------------------------------------------------+
In the class of the base WinForms object in \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, namely in the method returning the description of the integer element property, add the code block for returning the description of the new object property:
//+------------------------------------------------------------------+ //| 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_VISIBLE_AREA_HEIGHT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(string)this.GetProperty(property) ) : property==CANV_ELEMENT_PROP_DISPLAYED ? CMessage::Text(MSG_CANV_ELEMENT_PROP_DISPLAYED)+ (only_prop ? "" : !this.SupportProperty(property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(string)this.GetProperty(property) ) : property==CANV_ELEMENT_PROP_GROUP ? CMessage::Text(MSG_GRAPH_OBJ_PROP_GROUP)+ (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)) ) : "" ); } //+------------------------------------------------------------------+
If the property is passed to the method, then, depending on the only_prop flag, an appropriate text message is created — either a simple property name (only_prop = true), or together with the value set for the property (only_prop = false).
All controls will use the event functionality in one way or another — both for their internal use and for notifying the program about events occurring in its GUI. The main class for user interaction is the form object class — it implements the mouse interaction functionality, and the base class of all WinForms library objects is inherited from it. Let's create a method for sending messages to graphical elements in the same class.
In the \MQL5\Include\DoEasy\Objects\Graph\Form.mqh form object class file, namely in its protected section, declare the method for sending messages.
The method will be virtual in case it should be overridden for any derived object:
//--- 'The cursor is inside the window scrolling area, a mouse button is clicked (any)' event handler virtual void MouseScrollAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- 'The cursor is inside the window scrolling area, the mouse wheel is being scrolled' event handler virtual void MouseScrollAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Send a message about the event virtual bool SendEvent(const long chart_id,const ushort event_id); public:
In the method showing the form, set checking the object display flag (checking a new graphical element property):
//+------------------------------------------------------------------+ //| Show the form | //+------------------------------------------------------------------+ void CForm::Show(void) { //--- If the element should not be displayed (hidden inside another control), leave if(!this.Displayed()) return; //--- If the object has a shadow, display it if(this.m_shadow_obj!=NULL) this.m_shadow_obj.Show(); //--- Display the main form CGCnvElement::Show(); //--- In the loop by all bound graphical objects, for(int i=0;i<this.m_list_elements.Total();i++) { //--- get the next graphical element CGCnvElement *element=this.m_list_elements.At(i); if(element==NULL) continue; //--- and display it element.Show(); } //--- Update the form CGCnvElement::Update(); } //+------------------------------------------------------------------+
When calling this method for any object of CForm type or higher, the object display flag is checked first, and if the flag is not set (manual visibility control is enabled), then we immediately leave the method.
Let's implement the method that sends the event message outside the class body:
//+------------------------------------------------------------------+ //| Send a message about the event | //+------------------------------------------------------------------+ bool CForm::SendEvent(const long chart_id,const ushort event_id) { //--- Create the event: //--- Get the base and main objects CGCnvElement *base=this.GetBase(); CGCnvElement *main=this.GetMain(); //--- find the names of the main and base objects string name_main=(main!=NULL ? main.Name() : this.IsMain() ? this.Name() : "Lost name of object"); string name_base=(base!=NULL ? base.Name() : "Lost name of object"); ENUM_GRAPH_ELEMENT_TYPE base_base_type=(base!=NULL ? base.GetBase().TypeGraphElement() : this.TypeGraphElement()); //--- pass the object ID in the event 'long' parameter //--- pass the object type in the event 'double' parameter //--- in the event 'string' parameter, pass the names of the main, base and current objects separated by ";" long lp=this.ID(); double dp=base_base_type; string sp=::StringSubstr(name_main,::StringLen(this.NamePrefix()))+";"+ ::StringSubstr(name_base,::StringLen(this.NamePrefix()))+";"+ ::StringSubstr(this.Name(),::StringLen(this.NamePrefix())); //--- Send the event of clicking on the control to the control program chart bool res=true; ::ResetLastError(); res=::EventChartCustom(chart_id,event_id,lp,dp,sp); if(res) return true; ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_ENQUEUE_EVENT),". ",CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(::GetLastError())); return false; } //+------------------------------------------------------------------+
Here we get the pointers to the main and base objects, and receive their names. If the pointer to the main object is NULL, then most likely this is the main object. To do this, check if this is true. If yes, use the name of the current object. If for some reason the pointer is not received, use the "Lost name of object" string to manage it.
Then we need to find out the type of the base object the base object of the current object is bound to (i.e., get its base object from the base object followed by its type) and write all the received data to the variables sent in the event message. In lparam, send the current object ID, while in dparam, send the type of the base object the base object of the current one is bound to, while in 'sparam', pass the string featuring the names of three objects (main, base and current) separated by ";". When receiving an event, we can use this data to determine exactly which object the event message came from.
At the moment, this logic is sufficient to determine the object that generated the event, but I will change it later, since it does not allow tracking the entire nesting of objects into each other when creating more complex controls with a deeper hierarchy of nesting into each other.
Now let's add sending event messages to the event handlers of the WinForms objects.
In the \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh button object class file, namely in "The cursor is inside the active area, the left mouse button is clicked" event handler, add sending event messages:
//+------------------------------------------------------------------+ //| 'The cursor is inside the active area, | //| left mouse button released | //+------------------------------------------------------------------+ void CButton::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { //--- The mouse button released outside the element means refusal to interact with the element if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge()) { //--- If this is a simple button, set the initial background and text color if(!this.Toggle()) { this.SetBackgroundColor(this.BackgroundColorInit(),false); this.SetForeColor(this.ForeColorInit(),false); } //--- If this is the toggle button, set the initial background and text color depending on whether the button is pressed or not else { this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false); this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false); } //--- Set the initial frame color this.SetBorderColor(this.BorderColorInit(),false); //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL); //--- Send the test message to the journal Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel")); } //--- The mouse button released within the element means a click on the control else { //--- If this is a simple button, set the background and text color for "The cursor is over the active area" status if(!this.Toggle()) { this.SetBackgroundColor(this.BackgroundColorMouseOver(),false); this.SetForeColor(this.ForeColorMouseOver(),false); } //--- If this is the toggle button, else { //--- if the button does not work in the group, set its state to the opposite, if(!this.GroupButtonFlag()) this.SetState(!this.State()); //--- if the button is not pressed yet, set it to the pressed state else if(!this.State()) this.SetState(true); //--- set the background and text color for "The cursor is over the active area" status depending on whether the button is clicked or not this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false); this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false); } //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK); //--- Send the test message to the journal Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group()); //--- Set the frame color for "The cursor is over the active area" status this.SetBorderColor(this.BorderColorMouseOver(),false); } //--- Redraw the object this.Redraw(false); } //+------------------------------------------------------------------+
In \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\RadioButton.mqh:
//+------------------------------------------------------------------+ //| 'The cursor is inside the active area, | //| left mouse button released | //+------------------------------------------------------------------+ void CRadioButton::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { //--- The mouse button released outside the element means refusal to interact with the element if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge()) { this.SetCheckBackgroundColor(this.BackgroundColorInit(),false); this.SetCheckBorderColor(this.CheckBorderColorInit(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL); //--- Send a test entry to the journal Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel")); } //--- The mouse button released within the element means a click on the control else { this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false); this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); if(!this.Checked()) this.SetChecked(true); //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK); //--- Send a test entry to the journal Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID(),", Group=",this.Group()); } this.Redraw(false); } //+------------------------------------------------------------------+
In \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh:
//+------------------------------------------------------------------+ //| 'The cursor is inside the active area, | //| left mouse button released | //+------------------------------------------------------------------+ void CCheckBox::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { //--- The mouse button released outside the element means refusal to interact with the element if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge()) { this.SetCheckBackgroundColor(this.CheckBackgroundColorInit(),false); this.SetCheckBorderColor(this.CheckBorderColorInit(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); this.SetBackgroundColor(this.BackgroundColorInit(),false); //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL); //--- Send a test entry to the journal Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel")); } //--- The mouse button released within the element means a click on the control else { this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false); this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); this.SetBackgroundColor(this.BackgroundColorMouseOver(),false); this.SetChecked(!this.Checked()); //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK); //--- Send a test entry to the journal Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID()); } this.Redraw(false); } //+------------------------------------------------------------------+
After publishing the previous article, I noticed that two class files of button objects with left-right and up-down arrows stopped compiling on their own. They can only be compiled as part of the library — when compiling the main file of the Engine.mqh library, but not on their own, which is incorrect. To fix this, we need to change the list of included files in the files of these objects.
Previously, I had the panel object file included:
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\Containers\Panel.mqh" //+------------------------------------------------------------------+
Now I am going to include only the files that should be used by these classes.
For the button object file with up-down arrows \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowUpDownBox.mqh:
//+------------------------------------------------------------------+ //| ArrowUpDownBox.mqh | //| Copyright 2022, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\Containers\Container.mqh" #include "..\Helpers\ArrowUpButton.mqh" #include "..\Helpers\ArrowDownButton.mqh" //+------------------------------------------------------------------+
For the button object file with left-right arrows \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowLeftRightBox.mqh:
//+------------------------------------------------------------------+ //| ArrowLeftRightBox.mqh | //| Copyright 2022, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\Containers\Container.mqh" #include "..\Helpers\ArrowLeftButton.mqh" #include "..\Helpers\ArrowRightButton.mqh" //+------------------------------------------------------------------+
Now both files will compile normally, both independently and as part of the library.
Let's finalize the class of the tab header object of TabControl in \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabHeader.mqh.
From the "The cursor is inside the active area, the left mouse button is clicked" event handler, remove the code block for creating an event:
//--- Send the test message to the journal Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group()); //--- Create the event: //--- Get the base and main objects CWinFormBase *base=this.GetBase(); CWinFormBase *main=this.GetMain(); //--- in the 'long' event parameter, pass a string, while in the 'double' parameter, the tab header location column long lp=this.Row(); double dp=this.Column(); //--- in the 'string' parameter of the event, pass the names of the main and base objects separated by ";" string name_main=(main!=NULL ? main.Name() : ""); string name_base=(base!=NULL ? base.Name() : ""); string sp=name_main+";"+name_base; //--- Send the tab selection event to the chart of the control program ::EventChartCustom(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT,lp,dp,sp); //--- Set the frame color for "The cursor is over the active area" status this.SetBorderColor(this.BorderColorMouseOver(),false); } } //+------------------------------------------------------------------+
Now we have the method for creating and sending an event. So let's use it:
//+------------------------------------------------------------------+ //| 'The cursor is inside the active area, | //| left mouse button released | //+------------------------------------------------------------------+ void CTabHeader::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam) { //--- The mouse button released outside the element means refusal to interact with the element if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge()) { //--- If this is a simple button, set the initial background and text color if(!this.Toggle()) { this.SetBackgroundColor(this.BackgroundColorInit(),false); this.SetForeColor(this.ForeColorInit(),false); } //--- If this is the toggle button, set the initial background and text color depending on whether the button is pressed or not else { this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false); this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false); } //--- Set the initial frame color this.SetBorderColor(this.BorderColorInit(),false); //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL); //--- Send the test message to the journal Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel")); } //--- The mouse button released within the element means a click on the control else { //--- If this is a simple button, set the background and text color for "The cursor is over the active area" status if(!this.Toggle()) { this.SetBackgroundColor(this.BackgroundColorMouseOver(),false); this.SetForeColor(this.ForeColorMouseOver(),false); } //--- If this is the toggle button, else { //--- if the button does not work in the group, set its state to the opposite, if(!this.GroupButtonFlag()) this.SetState(!this.State()); //--- if the button is not pressed yet, set it to the pressed state else if(!this.State()) this.SetState(true); //--- set the background and text color for "The cursor is over the active area" status depending on whether the button is clicked or not this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false); this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false); //--- Get the field object corresponding to the header CWinFormBase *field=this.GetFieldObj(); if(field!=NULL) { //--- Display the field, bring it to the front, draw a frame and crop the excess field.Show(); field.BringToTop(); field.DrawFrame(); field.Crop(); } } //--- Send the event: this.SendEvent(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT); //--- Send the test message to the journal Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group()); //--- Set the frame color for "The cursor is over the active area" status this.SetBorderColor(this.BorderColorMouseOver(),false); } //--- Redraw an object and a chart this.Redraw(true); } //+------------------------------------------------------------------+
For example, when we scroll the header bar to the left, the leftmost header moves out of the control, and the one to the right of it takes its place. Since the initial coordinate of the headers is shifted to the right of the left edge of the container by two pixels, the header that goes beyond the left edge remains visible in this very area of two pixels — it is cropped at the edges of the container area, inside which the elements should be visible.
In order to hide this thin visible part of the header that has gone beyond the left edge, we need to slightly adjust the size of the container area, in which the attached objects are displayed. Besides, we also need to take into account whether the header is selected or not since the selected header increases in size by two pixels on each side. This means we need to dynamically resize the area of the container where the objects are visible, depending on which object is located on the edge. If selected, then the size remains unchanged. Otherwise, it is reduced by two pixels.
In the method that crops the image outlined by the calculated rectangular visibility scope, add adjusting the container visibility scope size and applying the obtained value to the edges of the visible area:
//+------------------------------------------------------------------+ //| Crop the image outlined by the calculated | //| rectangular visibility scope | //+------------------------------------------------------------------+ void CTabHeader::Crop(void) { //--- Get the pointer to the base object CGCnvElement *base=this.GetBase(); //--- If the object does not have a base object it is attached to, then there is no need to crop the hidden areas - leave if(base==NULL) return; //--- Set the initial coordinates and size of the visibility scope to the entire object int vis_x=0; int vis_y=0; int vis_w=this.Width(); int vis_h=this.Height(); //--- Set the size of the top, bottom, left and right areas that go beyond the container int crop_top=0; int crop_bottom=0; int crop_left=0; int crop_right=0; //--- Get the additional size, by which to crop the titles when the arrow buttons are visible int add_size_lr=(this.IsVisibleLeftRightBox() ? this.m_arr_butt_lr_size : 0); int add_size_ud=(this.IsVisibleUpDownBox() ? this.m_arr_butt_ud_size : 0); int dec_size_vis=(this.State() ? 0 : 2); //--- Calculate the boundaries of the container area, inside which the object is fully visible int top=fmax(base.CoordY()+(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_TOP),base.CoordYVisibleArea())+dec_size_vis+(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? add_size_ud : 0); int bottom=fmin(base.BottomEdge()-(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_BOTTOM),base.BottomEdgeVisibleArea()+1)-dec_size_vis-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT ? add_size_ud : 0); int left=fmax(base.CoordX()+(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_LEFT),base.CoordXVisibleArea())+dec_size_vis; int right=fmin(base.RightEdge()-(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_RIGHT),base.RightEdgeVisibleArea()+1)-add_size_lr; //--- Calculate the values of the top, bottom, left and right areas, at which the object goes beyond //--- the boundaries of the container area, inside which the object is fully visible crop_top=this.CoordY()-top; if(crop_top<0) vis_y=-crop_top; crop_bottom=bottom-this.BottomEdge()-1; if(crop_bottom<0) vis_h=this.Height()+crop_bottom-vis_y; crop_left=this.CoordX()-left; if(crop_left<0) vis_x=-crop_left; crop_right=right-this.RightEdge()-1; if(crop_right<0) vis_w=this.Width()+crop_right-vis_x; //--- If there are areas that need to be hidden, call the cropping method with the calculated size of the object visibility scope if(crop_top<0 || crop_bottom<0 || crop_left<0 || crop_right<0) this.Crop(vis_x,vis_y,vis_w,vis_h); } //+------------------------------------------------------------------+
Now, when headers go beyond the container, their narrow section, two pixels in size, is not displayed.
In the TabControl \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh WinForms object class file, namely in its private section, declare new methods:
//--- Return the list of (1) headers, (2) tab fields, the pointer to the (3) up-down and (4) left-right button objects CArrayObj *GetListHeaders(void) { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER); } CArrayObj *GetListFields(void) { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD); } CArrowUpDownBox *GetArrUpDownBox(void) { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0); } CArrowLeftRightBox *GetArrLeftRightBox(void) { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,0); } //--- Return the pointer to the (1) last and (2) first visible tab header CTabHeader *GetLastHeader(void) { return this.GetTabHeader(this.TabPages()-1); } CTabHeader *GetFirstVisibleHeader(void); //--- Set the tab as selected void SetSelected(const int index); //--- Set the tab as released void SetUnselected(const int index); //--- Set the number of a selected tab void SetSelectedTabPageNum(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value); } //--- Arrange the tab headers according to the set modes void ArrangeTabHeaders(void); //--- 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); //--- Scroll the header row (1) to the left, (2) to the right, (3) up when headers are on the left, (4) down, (3) up, (4) down void ScrollHeadersRowToLeft(void); void ScrollHeadersRowToRight(void); //--- Scroll the row of headers when they are located on the left (1) up, (2) down void ScrollHeadersRowLeftToUp(void); void ScrollHeadersRowLeftToDown(void); //--- Scroll the row of headers when they are located on the right (1) up, (2) down void ScrollHeadersRowRightToUp(void); void ScrollHeadersRowRightToDown(void); public:
and remove the public method
//--- Show the control virtual void Show(void); //--- Shift the header row void ShiftHeadersRow(const int selected); //--- Event handler virtual void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Constructor
I made this public method in the previous article. It shifted the header bar to the left when a partially hidden header was clicked. Now this will be done by the methods declared above. Moreover, they will handle both clicking on a partially hidden header, and clicking on the button managing the scrolling of the header bar.
Each of the declared methods is designed to scroll the header bar in a certain direction:
- When headers are located at the top or bottom — two methods for scrolling left and right.
- When headers are located on the left — two methods for scrolling left and scrolling right.
- When headers are located on the right — two methods for scrolling left and scrolling right.
When creating button objects with left-right and up-down arrows in the method that creates the specified number of tabs, we need to specify the main and base objects for these objects and each arrow button object within these objects, otherwise we will not be able to find these objects when creating an event message when the button is clicked:
//+------------------------------------------------------------------+ //| 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 //---... //---... //--- Create left-right and up-down button objects this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-32,0,15,15,clrNONE,255,this.Active(),false); this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0,this.Height()-32,15,15,clrNONE,255,this.Active(),false); //--- CArrowLeftRightBox *box_lr=this.GetArrLeftRightBox(); if(box_lr!=NULL) { this.SetVisibleLeftRightBox(false); this.SetSizeLeftRightBox(box_lr.Width()); box_lr.SetMain(this.GetMain()); box_lr.SetBase(this.GetObject()); box_lr.SetBorderStyle(FRAME_STYLE_NONE); box_lr.SetBackgroundColor(CLR_CANV_NULL,true); box_lr.SetOpacity(0); box_lr.Hide(); CArrowLeftButton *lb=box_lr.GetArrowLeftButton(); if(lb!=NULL) { lb.SetMain(this.GetMain()); lb.SetBase(box_lr); } CArrowRightButton *rb=box_lr.GetArrowRightButton(); if(rb!=NULL) { rb.SetMain(this.GetMain()); rb.SetBase(box_lr); } } //--- CArrowUpDownBox *box_ud=this.GetArrUpDownBox(); if(box_ud!=NULL) { this.SetVisibleUpDownBox(false); this.SetSizeUpDownBox(box_ud.Height()); box_ud.SetMain(this.GetMain()); box_ud.SetBase(this.GetObject()); box_ud.SetBorderStyle(FRAME_STYLE_NONE); box_ud.SetBackgroundColor(CLR_CANV_NULL,true); box_ud.SetOpacity(0); box_ud.Hide(); CArrowDownButton *db=box_ud.GetArrowDownButton(); if(db!=NULL) { db.SetMain(this.GetMain()); db.SetBase(box_ud); } CArrowUpButton *ub=box_ud.GetArrowUpButton(); if(ub!=NULL) { ub.SetMain(this.GetMain()); ub.SetBase(box_ud); } } //--- Arrange all titles in accordance with the specified display modes and select the specified tab this.ArrangeTabHeaders(); this.Select(selected_page,true); return true; } //+------------------------------------------------------------------+
After creating button objects with left-right and up-down arrows, we get the pointer to the created object and set the main and base objects for it. From the received object, get its arrow button objects and specify the main and base objects for each of them.
In the method displaying the control, add checking the object display flag:
//+------------------------------------------------------------------+ //| Show the control | //+------------------------------------------------------------------+ void CTabControl::Show(void) { //--- If the element should not be displayed (hidden inside another control), leave if(!this.Displayed()) return; //--- Get the list of all tab headers CArrayObj *list=this.GetListHeaders(); if(list==NULL) return; //--- If the object has a shadow, display it if(this.m_shadow_obj!=NULL) this.m_shadow_obj.Show(); //--- Display the container CGCnvElement::Show(); //--- Move all elements of the object to the foreground this.BringToTop(); } //+------------------------------------------------------------------+
If the mode of manual display management is enabled for the object, then leave the method.
The method that returns the pointer to the first visible header:
//+------------------------------------------------------------------+ //| Return the pointer to the first visible header | //+------------------------------------------------------------------+ CTabHeader *CTabControl::GetFirstVisibleHeader(void) { for(int i=0;i<this.TabPages();i++) { CTabHeader *obj=this.GetTabHeader(i); if(obj==NULL) continue; switch(this.Alignment()) { case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : if(obj.CoordX()==this.CoordXWorkspace()+(obj.State() ? 0 : 2)) return obj; break; case CANV_ELEMENT_ALIGNMENT_LEFT : if(obj.BottomEdge()==this.BottomEdgeWorkspace()+(obj.State() ? 0 : -2)) return obj; break; case CANV_ELEMENT_ALIGNMENT_RIGHT : if(obj.CoordY()==this.CoordYWorkspace()+(obj.State() ? 0 : 2)) return obj; break; default: break; } } return NULL; } //+------------------------------------------------------------------+
The first visible header is the one on the left if headers are located at the top/bottom, or at the bottom when headers are on the left, or at the top when headers are on the right of the control. To find this extreme header, we need to loop through all the headers of the object to check the coordinates of its location in accordance with the location of the header row. To be positioned on top, the header should be placed at the starting X coordinate of the container. In this case, if the title is not selected, then its initial coordinate is shifted to the right by two pixels. The situation is similar for a different location of the header bar.
The method in the loop looks for a match between the coordinates of the object and the coordinate of the container, depending on the location of the headers bar, and returns the pointer to the found object. If none of the headers are found, the method returns NULL.
The method that scrolls the header bar to the left:
//+------------------------------------------------------------------+ //| Scroll the header bar to the left | //+------------------------------------------------------------------+ void CTabControl::ScrollHeadersRowToLeft(void) { //--- If there are multiline headers, leave if(this.Multiline()) return; //--- Declare the variables and get the index of the selected tab int shift=0; int correct_size=0; int selected=this.SelectedTabPageNum(); //--- Get the first visible header CTabHeader *first=this.GetFirstVisibleHeader(); if(first==NULL) return; //--- If the first visible header is selected, set the size adjustment value if(first.PageNumber()==selected) correct_size=4; //--- Get the pointer to the very last header CTabHeader *last=this.GetLastHeader(); if(last==NULL) return; //--- If the last heading is fully visible, leave since the shift of all headers to the left is completed if(last.RightEdge()<=this.RightEdgeWorkspace()) return; //--- Get the shift size shift=first.Width()-correct_size; //--- In the loop by all headers for(int i=0;i<this.TabPages();i++) { //--- get the next header CTabHeader *header=this.GetTabHeader(i); if(header==NULL) continue; //--- and, if the header is successfully shifted to the left by 'shift' value, if(header.Move(header.CoordX()-shift,header.CoordY())) { //--- save its new relative coordinates header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); //--- If the title has gone beyond the left edge, int x=(i==selected ? 0 : 2); if(header.CoordX()-x<this.CoordXWorkspace()) { //--- crop and hide it header.Crop(); header.Hide(); //--- Get the selected header CTabHeader *header_selected=this.GetTabHeader(selected); if(header_selected==NULL) continue; //--- Get the tab field corresponding to the selected header CTabField *field_selected=header_selected.GetFieldObj(); if(field_selected==NULL) continue; //--- Draw the field frame field_selected.DrawFrame(); field_selected.Update(); } //--- If the header fits the visible area of the control, else { //--- display and redraw it header.Show(); header.Redraw(false); //--- Get the tab field corresponding to the header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- If this is a selected header, if(i==selected) { //--- Draw the field frame field.DrawFrame(); field.Update(); } } } } //--- Get the selected header CTabHeader *obj=this.GetTabHeader(selected); //--- If the header is placed in the visible part of the control, bring it to the foreground if(obj!=NULL && obj.CoordX()>=this.CoordXWorkspace() && obj.RightEdge()<=this.RightEdgeWorkspace()) obj.BringToTop(); //--- Redraw the chart to display changes immediately ::ChartRedraw(this.ChartID()); } //+------------------------------------------------------------------+
he method scrolling the header bar to the right:
//+------------------------------------------------------------------+ //| Scroll the header bar to the right | //+------------------------------------------------------------------+ void CTabControl::ScrollHeadersRowToRight(void) { //--- If there are multiline headers, leave if(this.Multiline()) return; //--- Declare the variables and get the index of the selected tab int shift=0; int correct_size=0; int selected=this.SelectedTabPageNum(); //--- Get the first visible header CTabHeader *first=this.GetFirstVisibleHeader(); if(first==NULL) return; //--- Get the header located before the first visible one CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1); //--- If there is no such header, leave since the shift of all headers to the right is completed if(prev==NULL) return; //--- If the header is selected, specify the size adjustment value if(prev.PageNumber()==selected) correct_size=4; //--- Get the shift size shift=prev.Width()-correct_size; //--- In the loop by all headers for(int i=0;i<this.TabPages();i++) { //--- get the next header CTabHeader *header=this.GetTabHeader(i); if(header==NULL) continue; //--- and, if the header is successfully shifted to the right by 'shift' value, if(header.Move(header.CoordX()+shift,header.CoordY())) { //--- save its new relative coordinates header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); //--- If the title goes beyond the left edge, int x=(i==selected ? 0 : 2); if(header.CoordX()-x<this.CoordXWorkspace()) { //--- crop and hide it header.Crop(); header.Hide(); } //--- If the header fits the visible area of the control, else { //--- display and redraw it header.Show(); header.Redraw(false); //--- Get the tab field corresponding to the header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- If this is a selected header, if(i==selected) { //--- Draw the field frame field.DrawFrame(); field.Update(); } } } } //--- Get the selected header CTabHeader *obj=this.GetTabHeader(selected); //--- If the header is placed in the visible part of the control, bring it to the foreground if(obj!=NULL && obj.CoordX()>=this.CoordXWorkspace() && obj.RightEdge()<=this.RightEdgeWorkspace()) obj.BringToTop(); //--- Redraw the chart to display changes immediately ::ChartRedraw(this.ChartID()); } //+------------------------------------------------------------------+
The method that scrolls the header row up when the headers are on the left:
//+------------------------------------------------------------------+ //| Scroll the header row up when the headers are on the left | //+------------------------------------------------------------------+ void CTabControl::ScrollHeadersRowLeftToUp(void) { //--- If there are multiline headers, leave if(this.Multiline()) return; //--- Declare the variables and get the index of the selected tab int shift=0; int correct_size=0; int selected=this.SelectedTabPageNum(); //--- Get the first visible header CTabHeader *first=this.GetFirstVisibleHeader(); if(first==NULL) return; //--- Get the header located before the first visible one CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1); //--- If there is no such header, leave since the shift of all headers upwards is completed if(prev==NULL) return; //--- If the header is selected, specify the size adjustment value if(prev.PageNumber()==selected) correct_size=4; //--- Get the shift size shift=prev.Height()-correct_size; //--- In the loop by all headers for(int i=0;i<this.TabPages();i++) { //--- get the next header CTabHeader *header=this.GetTabHeader(i); if(header==NULL) continue; //--- and, if the header is successfully shifted upwards by 'shift' value, if(header.Move(header.CoordX(),header.CoordY()-shift)) { //--- save its new relative coordinates header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); //--- If the header goes beyond the lower edge, int x=(i==selected ? 0 : 2); if(header.BottomEdge()+x>this.BottomEdgeWorkspace()) { //--- crop and hide it header.Crop(); header.Hide(); } //--- If the header fits the visible area of the control, else { //--- display and redraw it header.Show(); header.Redraw(false); //--- Get the tab field corresponding to the header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- If this is a selected header, if(i==selected) { //--- Draw the field frame field.DrawFrame(); field.Update(); } } } } //--- Get the selected header CTabHeader *obj=this.GetTabHeader(selected); //--- If the header is placed in the visible part of the control, bring it to the foreground if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace()) obj.BringToTop(); //--- Redraw the chart to display changes immediately ::ChartRedraw(this.ChartID()); } //+------------------------------------------------------------------+
The method that scrolls the header row down when the headers are on the left:
//+------------------------------------------------------------------+ //| Scroll the header row down when the headers are on the left | //+------------------------------------------------------------------+ void CTabControl::ScrollHeadersRowLeftToDown(void) { //--- If there are multiline headers, leave if(this.Multiline()) return; //--- Declare the variables and get the index of the selected tab int shift=0; int correct_size=0; int selected=this.SelectedTabPageNum(); //--- Get the first visible header CTabHeader *first=this.GetFirstVisibleHeader(); if(first==NULL) return; //--- If the first visible header is selected, set the size adjustment value if(first.PageNumber()==selected) correct_size=4; //--- Get the pointer to the very last header CTabHeader *last=this.GetLastHeader(); if(last==NULL) return; //--- If the last heading is fully visible, leave since the shift of all headers downwards is completed if(last.CoordY()>=this.CoordYWorkspace()) return; //--- Get the shift size shift=first.Height()-correct_size; //--- In the loop by all headers for(int i=0;i<this.TabPages();i++) { //--- get the next header CTabHeader *header=this.GetTabHeader(i); if(header==NULL) continue; //--- and, if the header is successfully shifted downwards by 'shift' value, if(header.Move(header.CoordX(),header.CoordY()+shift)) { //--- save its new relative coordinates header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); //--- If the header has gone beyond the lower edge, int x=(i==selected ? 0 : 2); if(header.BottomEdge()-x>this.BottomEdgeWorkspace()) { //--- crop and hide it header.Crop(); header.Hide(); //--- Get the selected header CTabHeader *header_selected=this.GetTabHeader(selected); if(header_selected==NULL) continue; //--- Get the tab field corresponding to the selected header CTabField *field_selected=header_selected.GetFieldObj(); if(field_selected==NULL) continue; //--- Draw the field frame field_selected.DrawFrame(); field_selected.Update(); } //--- If the header fits the visible area of the control, else { //--- display and redraw it header.Show(); header.Redraw(false); //--- Get the tab field corresponding to the header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- If this is a selected header, if(i==selected) { //--- Draw the field frame field.DrawFrame(); field.Update(); } } } } //--- Get the selected header CTabHeader *obj=this.GetTabHeader(selected); //--- If the header is placed in the visible part of the control, bring it to the foreground if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace()) obj.BringToTop(); //--- Redraw the chart to display changes immediately ::ChartRedraw(this.ChartID()); } //+------------------------------------------------------------------+
The method that scrolls the header row up when the headers are on the right:
//+------------------------------------------------------------------+ //| Scroll the header row up when the headers are on the right | //+------------------------------------------------------------------+ void CTabControl::ScrollHeadersRowRightToUp(void) { //--- If there are multiline headers, leave if(this.Multiline()) return; //--- Declare the variables and get the index of the selected tab int shift=0; int correct_size=0; int selected=this.SelectedTabPageNum(); //--- Get the first visible header CTabHeader *first=this.GetFirstVisibleHeader(); if(first==NULL) return; //--- If the first visible header is selected, set the size adjustment value if(first.PageNumber()==selected) correct_size=4; //--- Get the pointer to the very last header CTabHeader *last=this.GetLastHeader(); if(last==NULL) return; //--- If the last heading is fully visible, leave since the shift of all headers upwards is completed if(last.BottomEdge()<=this.BottomEdgeWorkspace()) return; //--- Get the shift size shift=first.Height()-correct_size; //--- In the loop by all headers for(int i=0;i<this.TabPages();i++) { //--- get the next header CTabHeader *header=this.GetTabHeader(i); if(header==NULL) continue; //--- and, if the header is successfully shifted upwards by 'shift' value, if(header.Move(header.CoordX(),header.CoordY()-shift)) { //--- save its new relative coordinates header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); //--- If the header has gone beyond the upper edge, int x=(i==selected ? 0 : 2); if(header.CoordY()-x<this.CoordYWorkspace()) { //--- crop and hide it header.Crop(); header.Hide(); //--- Get the selected header CTabHeader *header_selected=this.GetTabHeader(selected); if(header_selected==NULL) continue; //--- Get the tab field corresponding to the selected header CTabField *field_selected=header_selected.GetFieldObj(); if(field_selected==NULL) continue; //--- Draw the field frame field_selected.DrawFrame(); field_selected.Update(); } //--- If the header fits the visible area of the control, else { //--- display and redraw it header.Show(); header.Redraw(false); //--- Get the tab field corresponding to the header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- If this is a selected header, if(i==selected) { //--- Draw the field frame field.DrawFrame(); field.Update(); } } } } //--- Get the selected header CTabHeader *obj=this.GetTabHeader(selected); //--- If the header is placed in the visible part of the control, bring it to the foreground if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace()) obj.BringToTop(); //--- Redraw the chart to display changes immediately ::ChartRedraw(this.ChartID()); } //+------------------------------------------------------------------+
The method that scrolls the header row down when the headers are on the right:
//+------------------------------------------------------------------+ //| Scroll the header row down when the headers are on the right | //+------------------------------------------------------------------+ void CTabControl::ScrollHeadersRowRightToDown(void) { //--- If there are multiline headers, leave if(this.Multiline()) return; //--- Declare the variables and get the index of the selected tab int shift=0; int correct_size=0; int selected=this.SelectedTabPageNum(); //--- Get the first visible header CTabHeader *first=this.GetFirstVisibleHeader(); if(first==NULL) return; //--- Get the header located before the first visible one CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1); //--- If there is no such header, leave since the shift of all headers downwards is completed if(prev==NULL) return; //--- If the header is selected, specify the size adjustment value if(prev.PageNumber()==selected) correct_size=4; //--- Get the shift size shift=prev.Height()-correct_size; //--- In the loop by all headers for(int i=0;i<this.TabPages();i++) { //--- get the next header CTabHeader *header=this.GetTabHeader(i); if(header==NULL) continue; //--- and, if the header is successfully shifted downwards by 'shift' value, if(header.Move(header.CoordX(),header.CoordY()+shift)) { //--- save its new relative coordinates header.SetCoordXRelative(header.CoordX()-this.CoordX()); header.SetCoordYRelative(header.CoordY()-this.CoordY()); //--- If the title goes beyond the upper edge int x=(i==selected ? 0 : 2); if(header.CoordY()-x<this.CoordYWorkspace()) { //--- crop and hide it header.Crop(); header.Hide(); } //--- If the header fits the visible area of the control, else { //--- display and redraw it header.Show(); header.Redraw(false); //--- Get the tab field corresponding to the header CTabField *field=header.GetFieldObj(); if(field==NULL) continue; //--- If this is a selected header, if(i==selected) { //--- Draw the field frame field.DrawFrame(); field.Update(); } } } } //--- Get the selected header CTabHeader *obj=this.GetTabHeader(selected); //--- If the header is placed in the visible part of the control, bring it to the foreground if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace()) obj.BringToTop(); //--- Redraw the chart to display changes immediately ::ChartRedraw(this.ChartID()); } //+------------------------------------------------------------------+
The logic of all methods for scrolling header lines is fully described in the method code. All of them are identical to each other and differ only slightly with regards to shift calculation and visibility scope. I hope that the methods do not need additional explanations. If you have any questions, feel free to ask them in the comments below.
The event handler:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CTabControl::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Adjust subwindow Y shift CGCnvElement::OnChartEvent(id,lparam,dparam,sparam); //--- If the tab is selected if(id==WF_CONTROL_EVENT_TAB_SELECT) { //--- Get the header of the selected tab CTabHeader *header=this.GetTabHeader(this.SelectedTabPageNum()); if(header==NULL) return; //--- Depending on the location of the header row switch(this.Alignment()) { //--- Headers at the top/bottom case CANV_ELEMENT_ALIGNMENT_TOP : case CANV_ELEMENT_ALIGNMENT_BOTTOM : //--- If the header is cropped, shift the header row to the left if(header.RightEdge()>this.RightEdgeWorkspace()) this.ScrollHeadersRowToLeft(); break; //--- Headers on the left case CANV_ELEMENT_ALIGNMENT_LEFT : //--- If the header is cropped, shift the header row downwards if(header.CoordY()<this.CoordYWorkspace()) this.ScrollHeadersRowLeftToDown(); break; //--- Headers on the right case CANV_ELEMENT_ALIGNMENT_RIGHT : //--- If the header is cropped, shift the header row upwards Print(DFUN,"header.BottomEdge=",header.BottomEdge(),", this.BottomEdgeWorkspace=",this.BottomEdgeWorkspace()); if(header.BottomEdge()>this.BottomEdgeWorkspace()) this.ScrollHeadersRowRightToUp(); break; default: break; } } //--- When clicking on any header row scroll button if(id>=WF_CONTROL_EVENT_CLICK_SCROLL_LEFT && id<=WF_CONTROL_EVENT_CLICK_SCROLL_DOWN) { //--- Get the header of the last tab CTabHeader *header=this.GetTabHeader(this.GetListHeaders().Total()-1); if(header==NULL) return; int hidden=0; //--- When clicking on the left arrow header row scroll button if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT) this.ScrollHeadersRowToRight(); //--- When clicking on the right arrow header row scroll button if(id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT) this.ScrollHeadersRowToLeft(); //--- When clicking on the down arrow header row scroll button if(id==WF_CONTROL_EVENT_CLICK_SCROLL_DOWN) { //--- Depending on the location of the header row switch(this.Alignment()) { //--- scroll the headings upwards using the appropriate method case CANV_ELEMENT_ALIGNMENT_LEFT : this.ScrollHeadersRowLeftToUp(); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : this.ScrollHeadersRowRightToUp(); break; default: break; } } //--- When clicking on the up arrow header row scroll button if(id==WF_CONTROL_EVENT_CLICK_SCROLL_UP) { //--- Depending on the location of the header row switch(this.Alignment()) { //--- scroll the headings downwards using the appropriate method case CANV_ELEMENT_ALIGNMENT_LEFT : this.ScrollHeadersRowLeftToDown(); break; case CANV_ELEMENT_ALIGNMENT_RIGHT : this.ScrollHeadersRowRightToDown(); break; default: break; } } } } //+------------------------------------------------------------------+
Now we handle each event (either selecting a partially hidden tab header, or clicking on the button for scrolling the header bar) using the above methods. Generally, depending on the location of the header bar and the button or header click event, we call the appropriate method to scroll the header bar.
In the collection class of graphical elements in \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, namely in its event handler, we now need to correctly handle the events received from the WinForms objects. To achieve this, we need to get three names from the 'sparam' string parameter, find the base object and retrieve the one that generated the event from it. If the found object belongs to TabControl, call TabControl event handler by sending the event ID to it.
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { CGStdGraphObj *obj_std=NULL; // Pointer to the standard graphical object CGCnvElement *obj_cnv=NULL; // Pointer to the graphical element object on canvas ushort idx=ushort(id-CHARTEVENT_CUSTOM); //--- Processing WinForms control events if(idx>WF_CONTROL_EVENT_NO_EVENT && idx<WF_CONTROL_EVENTS_NEXT_CODE) { //--- Declare the array of names and enter the names of three objects, set in 'sparam' and separated by ";", into it string array[]; if(::StringSplit(sparam,::StringGetCharacter(";",0),array)!=3) { CMessage::ToLog(MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES); return; } //--- Get the main object by name CWinFormBase *main=this.GetCanvElement(array[0]); if(main==NULL) return; //--- Get the base object, inside which the event has occurred, from the main object by name CWinFormBase *base=main.GetElementByName(array[1]); CWinFormBase *base_elm=NULL; //--- If there is no element with the same name, then this is the base object of the event element bound to the base one - look for it in the list if(base==NULL) { //--- Get the list of all elements bound to the main object with the type set in the 'dparam' parameter CArrayObj *list_obj=CSelect::ByGraphCanvElementProperty(main.GetListElements(),CANV_ELEMENT_PROP_TYPE,(long)dparam,EQUAL); if(list_obj==NULL || list_obj.Total()==0) return; //--- In the loop by the obtained list for(int i=0;i<list_obj.Total();i++) { //--- get the next object base_elm=list_obj.At(i); if(base_elm==NULL) continue; //--- If the base object is found, get the bound object from it by name from array[1] base=base_elm.GetElementByName(array[1]); if(base!=NULL) break; } } //--- If failed to find the object here, exit if(base==NULL) return; //--- From the found base object, get the object the event occurred from by name CWinFormBase *object=base.GetElementByName(array[2]); if(object==NULL) return; //+------------------------------------------------------------------+ //| Clicking the control | //+------------------------------------------------------------------+ if(idx==WF_CONTROL_EVENT_CLICK) { //--- If TabControl type is set in dparam if((ENUM_GRAPH_ELEMENT_TYPE)dparam==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL) { //--- Set the event type depending on the element type that generated the event int event_id= (object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP ? WF_CONTROL_EVENT_CLICK_SCROLL_UP : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN : WF_CONTROL_EVENT_NO_EVENT); //--- If the base control is received, call its event handler if(base_elm!=NULL) base_elm.OnChartEvent(event_id,lparam,dparam,sparam); } } //+------------------------------------------------------------------+ //| Selecting the TabControl tab | //+------------------------------------------------------------------+ if(idx==WF_CONTROL_EVENT_TAB_SELECT) { if(base!=NULL) base.OnChartEvent(idx,lparam,dparam,sparam); } } //--- Handle the events of renaming and clicking a standard graphical object if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CLICK || idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG || idx==CHARTEVENT_OBJECT_CLICK) { //--- Calculate the chart ID //---... //---...
The entire logic is described in the code and requires no additional explanations.
Now all is ready for the test.
Test
To perform the test, I will use the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part119\ as TestDoEasy119.mq5.
The only difference from the previous version is that I have implemented 15 tabs in TabControl:
//--- Create TabControl pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,InpTabControlX,InpTabControlY,pnl.Width()-30,pnl.Height()-40,clrNONE,255,true,false); CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0); if(tc!=NULL) { tc.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode); tc.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment); tc.SetMultiline(InpTabCtrlMultiline); tc.SetHeaderPadding(6,0); tc.CreateTabPages(15,0,56,20,TextByLanguage("Вкладка","TabPage")); //--- Create a text label with a tab description on each tab for(int j=0;j<tc.TabPages();j++) { tc.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,60,20,80,20,clrDodgerBlue,255,true,false); CLabel *label=tc.GetTabElement(j,0); if(label==NULL) continue; label.SetText("TabPage"+string(j+1)); } }
It was possible to leave 11 tabs but I have increased the number of tabs to test the performance and search for some "bugs". So this number is just the result of debugging and troubleshooting when shifting the selected header out of the container on both sides.
Compile the EA and launch it on the chart:
As we can see, everything works as intended.
There are two shortcomings though: if you hover over the tab header area that is hidden, the header reacts by changing color, as if it were visible in this area. This is the reason why the active area of the control does not change its size when the visible area is resized. To fix this, I will need to calculate and resize the active area in accordance with the visible one.
The second shortcoming is that if you move the selected header outside the container and move the panel, then two pixels of the hidden header will be displayed. This has to do with sizing the tab for the scope calculation, as the selected header increases in size on each side by two pixels. To fix this, I need to find a way to get the size of the adjacent header inside the tab header object, according to which the size of the visibility area is calculated.
I will deal with this in subsequent articles along with the development of a new WinForms object.
What's next?
In the next article, I will start developing the SplitContainer WinForms object.
*Previous articles within the series:
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
DoEasy. Controls (Part 16): TabControl WinForms object — several rows of tab headers, stretching headers to fit the container
DoEasy. Controls (Part 17): Cropping invisible object parts, auxiliary arrow buttons WinForms objects
DoEasy. Controls (Part 18): Functionality for scrolling tabs in TabControl
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/11490
![Neural networks made easy (Part 26): Reinforcement Learning](https://c.mql5.com/2/48/Networks_easy_26.png)
![DoEasy. Controls (Part 18): Functionality for scrolling tabs in TabControl](https://c.mql5.com/2/48/MQL5-avatar-doeasy-library-2__6.png)
![Neural networks made easy (Part 27): Deep Q-Learning (DQN)](https://c.mql5.com/2/48/Neural_networks_made_easy.png)
![Neural networks made easy (Part 25): Practicing Transfer Learning](https://c.mql5.com/2/48/Neural_networks_made_easy_025.png)
![MQL5 - Language of trade strategies built-in the MetaTrader 5 client terminal](https://c.mql5.com/i/registerlandings/logo-2.png)
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use