DoEasy. Elementos de control (Parte 19): Scrolling de pestañas en el elemento TabControl, eventos de objetos WinForms
Contenido
Concepto
En el último artículo pusimos a prueba el scrolling de la línea de encabezados de las pestañas al seleccionar un encabezado parcialmente oculto: este se desplazó hacia la izquierda, haciéndose totalmente visible. Hoy, basándonos en la funcionalidad creada, vamos a crear métodos para desplazar la línea de encabezados de izquierda a derecha y de arriba a abajo. En esta caso, además, el scrolling de los encabezados hacia arriba y hacia abajo dependerá de dónde se ubica la línea de encabezados, a la izquierda o a la derecha. Todos estos métodos se llamarán tanto al seleccionar un encabezado parcialmente oculto para su visualización completa como al pulsar los botones de scrolling (arriba, abajo, izquierda y derecha). Para entender qué botón y en qué control pulsamos, utilizaremos el modelo de eventos: al pulsar un botón, se enviará un evento que la biblioteca captará y procesará, enviando el evento al manejador de eventos del elemento al que está vinculado el botón pulsado, para su posterior procesamiento dentro del control. Utilizaremos este modelo para trabajar en otros controles que tendrán elementos de control.
Por el momento, los nombres de los elementos gráficos -principal y básico- se enviarán al manejador de eventos en el parámetro sparam. El elemento principal será aquel al que está vinculado el objeto en el que se ha producido el evento, y aquí se considerará el elemento básico. Pero si este control básico es compuesto -algunos otros controles están vinculados a él (botones, por ejemplo)-, y un evento ya ha ocurrido en ellos, entonces no podremos encontrar el control básico porque no está directamente adjunto al objeto principal. Para evitar esto por ahora, haremos lo siguiente: transmitiremos en el parámetro sparam los nombres del objeto principal, el objeto básico (como estaba antes), más el nombre del objeto en el que se ha producido el evento.
De esta forma, tendremos datos de entrada sobre el objeto principal, sobre el objeto básico dentro del cual se ha producido un evento en alguno de los elementos vinculados a él, y el nombre de ese elemento en el que se ha producido el evento. Para saber exactamente de qué control se trataba al pulsar el botón (un caso especial), transmitiremos el tipo de este objeto subyacente en el parámetro dparam. Así, conociendo el tipo del objeto básico, obtendremos una lista con todos los controles con el tipo escrito en dparam en el objeto principal, y luego haremos un ciclo a través de todos esos objetos para encontrar el objeto vinculado con el nombre transmitido en último lugar a sparam, en el que se produjo el evento (pulsación en el control).
Por el momento, este esquema no parece muy sólido en cuanto a su versatilidad para trabajar con objetos más complejos, pero en este momento, un plan de este tipo bastará para continuar el desarrollo de la biblioteca. Y cuando creemos controles más complejos con un esquema más desarrollado de anidamiento de objetos entre sí, entonces tendremos un ejemplo práctico claro de cómo identificar correcta y universalmente los eventos en tales objetos en la biblioteca (recuerde el principio universal "de lo simple a lo complejo").
La biblioteca ahora permite ocultar y mostrar controles. Para mostrar u ocultar dichos elementos, solo tendremos que ocultar el elemento principal o básico, y todos los elementos adjuntos se ocultarán o mostrarán en consecuencia. Pero si lo pensamos un poco e imaginamos que debe haber objetos dentro del control que se muestren o visualicen independientemente del objeto principal (es decir, su visibilidad se establece dentro del propio control), y si tal objeto está oculto pero su elemento padre está visualizado, entonces tal objeto no deberá mostrarse. En tales situaciones, necesitaremos controlar la visibilidad de estos objetos desde su control subyacente y, para ello, deberemos introducir otra propiedad del elemento gráfico: su bandera de visualización. Entonces, cuando el objeto principal se haya ocultado y luego mostrado, el control para el que se ha reiniciado su bandera de visualización permanecerá oculto hasta que se muestre explícitamente desde su control básico.
Bien, basta de teoría, vayamos al grano...
Mejorando las clases de la biblioteca
Como hacemos esto pensando en el futuro, no vamos a crear nuestros propios identificadores de eventos para los controles de scrolling: los crearemos con vistas a futuros nuevos controles que también usarán botones (barra de desplazamiento, listas desplegables, etc.). Es decir, crearemos algunos eventos de clic genéricos en el control que gestiona el scrolling o la aparición de ventanas emergentes.
En el archivo \MQL5\Include\DoEasy\Defines.mqh, añadiremos a la lista de posibles eventos de los controles WinForms las nuevas constantes de enumeración:
//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+
Aquí hemos creado un identificador de evento para el evento de cancelación de clic (en algunos casos es posible procesar este evento) por si acaso, y hemos añadido eventos comunes para los controles que tienen botones que gestionan la apariencia del control o interactúan con él.
En la enumeración de propiedades enteras del elemento gráfico, en el lienzo, añadimos la nueva propiedad y aumentamos el número total de propiedades de 96 a 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 //+------------------------------------------------------------------+
La bandera para mostrar un control que no está oculto indica que si el control no está oculto, pero esta bandera está desactivada(false), entonces este control no se mostrará. Es decir, si mostramos el control principal, sus descendientes, para los que esta bandera está desactivada, seguirán ocultos hasta que se muestren forzosamente estableciendo esta bandera en true y llamando al método Show().
Asimismo, añadiremos las nuevas propiedades a la lista de posibles criterios para clasificar los elementos gráficos en el lienzo:
//+------------------------------------------------------------------+ //| 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 }; //+------------------------------------------------------------------+
Ahora podremos seleccionar, clasificar y filtrar listas de elementos gráficos utilizando esta nueva propiedad.
En el archivo \MQL5\Include\DoEasy\Data.mqh, añadiremos los índices de los nuevos mensajes a la biblioteca:
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
y los textos de los mensajes correspondientes a los nuevos índices añadidos:
{"Запрос за пределами массива","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"},
Vamos a finalizar la clase del elemento gráfico en el archivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.
Para ello, añadiremos a la nueva estructura del objeto la nueva propiedad nuevamente añadida:
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
Todas las propiedades del objeto se escriben en la estructura del objeto para guardar este en un archivo y luego recuperar el objeto del mismo.
En el bloque con los métodos simplificados de acceso a las propiedades de los objetos, escribiremos dos nuevos métodos para establecer y recuperar la propiedad de representación de objetos:
//+------------------------------------------------------------------+ //| 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); }
Los métodos simplemente escriben la bandera transmitida en la propiedad del objeto y retornan el valor escrito en la propiedad del objeto.
En ambos constructores de la clase, escribiremos un valor por defecto en la nueva propiedad:
//+------------------------------------------------------------------+ //| 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()); } } //+------------------------------------------------------------------+
De forma predeterminada, el objeto está configurado para ser visible y se muestra al activarse la visibilidad del objeto básico o principal. Para establecer para el objeto el modo de control manual de su visibilidad, el valor de la bandera deberá ser false. En este caso, si el objeto básico o maestro se ha ocultado y luego mostrado, este objeto actual seguirá oculto, y para mostrarlo, necesitaremos establecer la propiedad Displayed en true y llamar nosotros mismos al método Show() de ese objeto.
En el método que crea la estructura del objeto, escribiremos el valor de la propiedad del objeto en el campo correspondiente de la estructura:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
En el método que crea un objeto a partir de una estructura, estableceremos el valor de la nueva propiedad del objeto a partir del campo correspondiente de la estructura:
//+------------------------------------------------------------------+ //| 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 } //+------------------------------------------------------------------+
En la clase objeto básico WinForms, en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, en el método que retorna la descripción de una propiedad entera del elemento, añadiremos el bloque de código para retornar la descripción de una nueva propiedad del objeto:
//+------------------------------------------------------------------+ //| 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)) ) : "" ); } //+------------------------------------------------------------------+
Si esta propiedad es transmitida al método, entonces dependiendo de la bandera only_prop, se generará un mensaje de texto, ya sea solo el nombre de la propiedad (only_prop = true), o bien junto con el valor establecido a la propiedad (only_prop = false).
Todos los controles utilizarán la funcionalidad de los eventos de una forma u otra, tanto para su propio uso interno como para notificar al programa de eventos en su GUI. La clase principal para la interacción con el usuario es la clase objeto de formulario, que implementa la funcionalidad de interacción con el ratón, y también hereda la clase básica de todos los objetos de la biblioteca WinForms. Precisamente en ella crearemos un método para enviar mensajes de elementos gráficos.
En el archivo de clase objeto \MQL5\Include\DoEasy\Objects\Graph\Form.mqh, en su sección protegida, declararemos el método para enviar mensajes.
El método será virtual, en caso de que necesite ser sobrescrito para cualquier objeto heredero:
//--- '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:
En el método que muestra el formulario, escribiremos una comprobación para la bandera de visualización del objeto (comprobación de una nueva propiedad del elemento gráfico):
//+------------------------------------------------------------------+ //| 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(); } //+------------------------------------------------------------------+
Al llamar a este método para cualquier objeto de tipo CForm o superior, primero comprobaremos la bandera de visualización del objeto y, si la bandera no está activada (el control de visibilidad manual está activado), saldremos inmediatamente del método.
Fuera del cuerpo de la clase, escribiremos una implementación del método que envía el mensaje de evento:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Aquí obtendremos los punteros a los objetos principal y básico y también obtendremos sus nombres. Si el puntero al objeto principal es NULL, probablemente sea el objeto principal. Para ello, comprobaremos si es así y, en caso afirmativo, usaremos el nombre del objeto actual. Si el puntero no se obtiene por alguna razón, utilizaremos la cadena "Lost name of object" para el nombre, de forma que pueda ser monitoreado.
Luego deberemos averiguar el tipo de objeto básico al que está vinculado el objeto básico de este objeto (es decir, obtendremos del objeto básico su objeto básico, y de este obtendremos su tipo), y escribiremos todos los datos obtenidos en las variables enviadas en el mensaje de evento. En lparam enviaremos el identificador del objeto actual, en dparam el tipo de objeto básico al que está vinculado el objeto básico del objeto actual, y en sparam enviaremos una línea con los nombres de los tres objetos separados por ";" - el objeto principal, el objeto básico y el objeto actual. Al recibir un evento, podremos usar estos datos para determinar exactamente de qué objeto procede el mensaje de evento.
Por ahora, esta lógica bastará para identificar el objeto que ha generado el evento, pero la modificaremos más adelante, ya que no nos permite monitorear el anidamiento completo de los objetos entre sí al crear controles más complejos con una jerarquía más profunda de anidamiento mutuo.
Ahora vamos a añadir a los manejadores de eventos de los objetos WinForms el envío de mensajes sobre eventos.
En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, en el manejador del evento "Cursor dentro del área activa, botón (izquierdo) del ratón pulsado", escribiremos un mensaje sobre el evento:
//+------------------------------------------------------------------+ //| '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); } //+------------------------------------------------------------------+
En el archivo \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); } //+------------------------------------------------------------------+
En el archivo \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); } //+------------------------------------------------------------------+
Ya después de publicar el último artículo, reparamos en que los dos archivos de clase para las flechas izquierda-derecha y arriba-abajo dejaron de compilarse solos. Solo se pueden compilar como parte de una biblioteca, al compilar el archivo principal de la biblioteca Engine.mqh, pero no por sí solos, y esto no está bien. Para rectificar dicho defecto, deberemos cambiar la lista de archivos de inclusión en los archivos de estos objetos.
Antes, teníamos un archivo de objeto de panel conectado:
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\Containers\Panel.mqh" //+------------------------------------------------------------------+
Ahora añadiremos solo aquellos archivos que deben ser utilizados por estas clases.
Para el archivo del objeto de botón con flechas arriba-abajo \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" //+------------------------------------------------------------------+
Para el archivo del objeto de botón con flechas izquierda-derecha \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" //+------------------------------------------------------------------+
Ambos archivos se compilarán ahora con normalidad, ya sea por sí mismos o como parte de una biblioteca.
Vamos a mejorar la clase del objeto de encabezado de la pestaña del control TabControl en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.
Del manejador del evento "Cursor dentro del área activa, botón (izquierdo) del ratón pulsado", eliminaremos el bloque de código para crear el evento:
//--- 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); } } //+------------------------------------------------------------------+
Ahora tenemos un método para crear y enviar el evento, que es lo que vamos a utilizar:
//+------------------------------------------------------------------+ //| '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); } //+------------------------------------------------------------------+
Al desplazar la línea de encabezados hacia la izquierda, por ejemplo, la línea de encabezados de la izquierda saldrá del control y será sustituida por la de su derecha. Como la coordenada inicial para la ubicación del encabezado está dos píxeles a la derecha del borde izquierdo del contenedor, el encabezado que se encuentre más allá del borde izquierdo permanecerá visible en la misma zona de dos píxeles, pues se recortará por los bordes del área del contenedor dentro de la cual los elementos deberían ser visibles.
Para ocultar esta parte delgada y visible del encabezado, deberemos ajustar ligeramente el tamaño de la zona contenedora donde se muestran los objetos adjuntos. También habrá que tener en cuenta si el encabezado está seleccionado o no, ya que un encabezado seleccionado aumenta de tamaño dos píxeles a cada lado, y para que no se recorte dos píxeles al seleccionarlo, deberemos tenerlo en cuenta. Es decir, necesitaremos cambiar dinámicamente el tamaño de la zona contenedora en la que los objetos resultan visibles, dependiendo de qué objeto esté en el borde. Si está seleccionado, no lo redimensionaremos, y si no está seleccionado, reduciremos en dos píxeles.
En el método que recorta la imagen contorneada por la zona de visibilidad rectangular calculada, añadiremos un ajuste al tamaño de la zona de visibilidad del contenedor y añadiremos el valor resultante a los bordes de la zona visible:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Ahora, si los encabezados se salen del contenedor, no se mostrará su sección estrecha de dos píxeles.
En el archivo de la clase del objeto WinForms TabControl \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh, en su sección privada, declararemos los nuevos métodos:
//--- 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:
y eliminaremos el método público
//--- 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
Ya hicimos este método público en un artículo anterior, y se encargaba de desplazar la línea de encabezados a la izquierda al pulsar sobre un encabezado parcialmente oculto. A partir de ahora, esto se gestionará usando los métodos declarados anteriormente, además, estos procesarán tanto el clic sobre un encabezado parcialmente oculto como el clic sobre el botón de control de scrolling de la línea de encabezados.
Cada uno de los métodos declarados ha sido diseñado para el scrolling de la línea de encabezados en una dirección diferente:
- Cuando los encabezados se encuentran arriba o abajo, hay dos métodos para el scrolling a la izquierda y a la derecha.
- Cuando los encabezados se encuentran a la izquierda, hay dos métodos para el scrolling a la izquierda y el scrolling a la derecha.
- Cuando los encabezados se encuentran a la derecha, hay dos métodos para el scrolling a la izquierda y el scrolling a la derecha.
En el método que crea el número indicado de pestañas, al crear los objetos botón con flechas izquierda-derecha y arriba-abajo, necesitaremos indicar a estos objetos y a cada objeto de botón con flechas dentro de estos objetos sus objetos principal y básico, de lo contrario, no podremos encontrar estos objetos al crear el mensaje de evento al pulsar sobre el botón:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Después de crear los objetos de botón con flechas izquierda-derecha y arriba-abajo, obtendremos el puntero al objeto creado y estableceremos los objetos principal y básico para él. A continuación, obtendremos sus objetos de botón con flechas del objeto resultante y especificaremos el objeto principal y el objeto básico para cada uno de ellos.
En el método que muestra un control, añadiremos la comprobación de la bandera de visualización del objeto:
//+------------------------------------------------------------------+ //| 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(); } //+------------------------------------------------------------------+
Si el modo de visualización manual está activado para el objeto, saldremos del método.
Método que retorna un puntero al primer encabezado visible:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
El primer encabezado visible es el de la izquierda cuando los encabezados están en la parte superior/inferior, o en la parte inferior cuando los encabezados están a la izquierda, o en la parte superior cuando los encabezados están a la derecha del control. Para encontrar el encabezado más externo, deberemos iterar por todos los encabezados de los objetos y comprobar sus coordenadas con la ubicación de la fila de encabezados. Para la ubicación superior, el encabezado deberá encontrarse en la coordenada inicial X del contenedor. Si el encabezado no está seleccionado, su coordenada inicial se desplazará dos píxeles a la derecha. Lo mismo ocurrirá con una disposición diferente de la fila de encabezados.
El método buscará en un ciclo una coincidencia entre las coordenadas del objeto y la coordenada del contenedor en función de la ubicación de la fila de encabezados, y retornará el puntero al objeto encontrado. Si no se encuentra ninguno de los encabezados, el método retornará NULL.
Método que desplaza la línea de encabezados hacia la izquierda:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
Método que desplaza la línea de encabezados hacia la derecha:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
Método que desplaza hacia arriba la línea de encabezados cuando estos se ubican a la izquierda:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
Método que desplaza hacia abajo la línea de encabezados cuando estos se ubican a la izquierda:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
Método que desplaza hacia arriba la línea de encabezados cuando estos se ubican a la derecha:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
Método que desplaza hacia abajo la línea de encabezados cuando estos se ubican a la derecha:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
La lógica de todos los métodos para el scrolling de las filas de encabezados se explica completamente en el código del método. Todas son idénticas entre sí y solo se distinguen ligeramente en los cálculos de desplazamiento y las zonas de visibilidad. Espero que los métodos no necesiten más explicaciones. En cualquier caso, podrá plantear cualquier duda en los comentarios al artículo.
Manejador de eventos:
//+------------------------------------------------------------------+ //| 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; } } } } //+------------------------------------------------------------------+
Ahora, utilizando los métodos descritos anteriormente, procesaremos cada evento, ya sea la selección de un encabezado de pestaña parcialmente oculto o el clic sobre el botón de scrolling de la fila de encabezados. En general, dependiendo de la ubicación de la fila de encabezados y del evento pulsación de un botón o encabezado, llamaremos al método correspondiente para el scrolling por la fila de encabezados.
En la clase de colección de elementos gráficos, en el archivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, en su manejador de eventos, ahora deberemos procesar correctamente los eventos obtenidos de los objetos WinForms. A saber, los tres nombres del parámetro de cadena sparam, así como encontrar el objeto básico, y a partir de él, el que ha generado el evento. Una vez encontrado, y si este objeto pertenece al control TabControl, llamaremos al manejador de eventos del control TabControl, enviándole el identificador del evento.
//+------------------------------------------------------------------+ //| 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 //---... //---...
Aquí también podemos ver que toda la lógica está completamente explicada en el código, por lo que esperamos que no necesite más explicaciones.
Ahora ya está todo listo para poner a prueba lo que hemos hecho hoy.
Simulación
Para la prueba, tomaremos el asesor del artículo anterior y lo guardaremos en la nueva carpeta \MQL5\Experts\TestDoEasy\Part119\ con el nuevo nombre TestDoEasy119.mq5.
La única diferencia entre el asesor y la versión anterior es que crearemos 15 pestañas en el control 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)); } }
Podríamos haberlo dejado como estaba, con 11 pestañas, pero hemos aumentado el número de pestañas para probar el rendimiento y encontrar algunos 'bugs'. Así que este número es simplemente el resultado de la depuración y la solución de problemas cuando el encabezado resaltado se desplaza fuera del contenedor en ambos lados.
Vamos a compilar el asesor y ejecutarlo en el gráfico:
Como podemos ver, todo lo que queríamos hacer hoy está funcionando según lo previsto.
Eso sí, hay dos pequeñas faltas: si pasamos el cursor por encima de una zona del encabezado de una pestaña que esté oculta, el encabezado reaccionará cambiando de color como si estuviera visible en ese lugar. Esta es la razón por la que la zona activa del elemento no cambia de tamaño cuando se redimensiona la zona visible. Para corregirlo, deberemos calcular la zona activa y redimensionarla para que coincida con la zona visible.
La segunda falta es que si desplazamos el encabezado seleccionado fuera del contenedor y desplazamos el panel, se mostrarán los dos píxeles del encabezado oculto. Esto tiene que ver con la especificación del tamaño de la pestaña para calcular la zona de visibilidad, ya que el encabezado seleccionado aumentará de tamaño en dos píxeles a cada lado. Para solucionarlo, dentro del objeto de encabezado de la pestaña, deberemos averiguar cómo obtener el tamaño del encabezado adyacente que se utiliza para calcular el tamaño de la zona de visibilidad.
Llegaremos a esto artículos posteriores, junto con el desarrollo del nuevo objeto WinForms.
¿Qué es lo próximo?
En el próximo artículo, comenzaremos a desarrollar el objeto WinForms SplitContainer.
*Artículos de esta serie:
DoEasy. Elementos de control (Parte 13): Optimizando la interacción de los objetos WinForms con el ratón. Comenzamos el desarrollo del objeto WinForms TabControl
DoEasy. Elementos de control (Parte 14): Nuevo algoritmo de denominación de los elementos gráficos. Continuamos trabajando con el objeto WinForms TabControl
DoEasy. Elementos de control (Parte 15): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, métodos de trabajo con pestañas
DoEasy. Elementos de control (Parte 16): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, modo de expansión de encabezados para ajustarse al tamaño del contenedor
DoEasy. Elementos de control (Parte 17): Recortando partes invisibles de objetos, objetos WinForms auxiliares de botón con flechas
DoEasy. Elementos de control (Parte 18): Preparamos la funcionalidad para el scrolling de las pestañas en TabControl
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/11490
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso