![DoEasy. Controles (Parte 19): Rolagem de guias no elemento TabControl, eventos de objetos WinForms](https://c.mql5.com/2/49/doeasy_019_600x314.jpg)
DoEasy. Controles (Parte 19): Rolagem de guias no elemento TabControl, eventos de objetos WinForms
Conteúdo
Ideia
No último artigo, provamos a rolagem de uma fileira de cabeçalhos de guias enquanto selecionávamos um cabeçalho parcialmente oculto, o que causava que este último se deslocasse para a esquerda, ficando totalmente visível. Hoje, já partindo da funcionalidade criada, elaboraremos métodos para deslocar a fileira de cabeçalhos da esquerda para a direita e de cima para baixo. Devemos considerar que, para rolar os cabeçalhos para cima e para baixo, importa onde a fileira de cabeçalhos está localizada, se à esquerda ou à direita. Todos esses métodos serão chamados ao selecionar um cabeçalho parcialmente oculto para exibi-lo por completo e ao pressionar os botões de rolagem (para cima, para baixo, para a esquerda e para a direita). Para entender qual botão e em qual controle foi pressionado, usaremos o modelo de evento, assim quando o botão for pressionado, será enviado um evento que a biblioteca irá interceptar e processar, enviando, para o manipulador de eventos do elemento ao qual o botão pressionado é vinculado, um evento para processamento adicional dentro do controle. E usaremos esse modelo para trabalhar em outros controles que contêm controles.
No momento, enviamos os nomes dos elementos gráficos — o principal e o base — para o manipulador de eventos no parâmetro sparam. O elemento principal é aquele ao qual está anexado o objeto no qual ocorreu o evento, e ele é aqui considerado o elemento base. Mas, se tal controle base for composto, isto é, mais alguns controles estão anexados a ele (botões, por exemplo) e um evento já ocorreu neles, então não conseguiremos encontrar o elemento base, pois não está vinculado diretamente ao objeto principal. Para contornar essa situação, vamos fazer o seguinte por enquanto: no parâmetro sparam, passaremos os nomes do objeto principal, o base (como antes), mais o nome do objeto em que ocorreu o evento.
Assim, teremos dados de entrada para o objeto principal, para o base, dentro do qual ocorreu um evento em algum dos elementos anexados, e teremos o nome desse elemento no qual o evento ocorreu. Para saber exatamente que tipo de controle foi aquele em que clicamos usando o botão (um caso especial), enviaremos o tipo deste objeto base no parâmetro dparam. Assim, conhecendo o tipo do objeto base, obteremos uma lista de todos os controles com o tipo registrado em dparam no objeto principal e, a seguir, em um loop por todos esses objetos, procuraremos o objeto anexado a ele com o nome passado em sparam, em que houve um evento (clique no controle).
No momento, este esquema não parece muito confiável em termos de versatilidade para lidar com objetos mais complexos. Mas nesta fase, ele é suficiente para continuarmos o desenvolvimento da biblioteca. Já quando criemos controles mais complexos com um esquema mais desenvolvido de aninhamento de objetos, então teremos um exemplo prático claro de como identificar corretamente na biblioteca e universalmente os eventos em tais objetos (lembre-se do princípio de "do simples ao complexo").
A biblioteca agora permite ocultar e exibir controles. Para exibir ou ocultar elementos, basta ocultar o elemento principal ou base e todos os elementos anexados a ele serão ocultados ou exibidos respectivamente. Mas se pensarmos um pouco sobre isso e imaginarmos que deve haver objetos dentro do controle que são mostrados ou exibidos independentemente do objeto principal, ou seja, sua visibilidade é definida dentro do próprio controle, e se tal objeto estiver escondido mas seu pai for mostrado, então este objeto não deve ser mostrado. Em tais situações, precisamos controlar a visibilidade destes objetos a partir de seu controle base, e para a obtenção de tal possibilidade, precisamos introduzir mais uma propriedade de elemento gráfico, isto é, o sinalizador de exibição. Então, se o objeto principal tiver sido escondido e depois exibido, o controle para o qual seu sinalizador de exibição for reiniciado permanecerá oculto até que seja explicitamente exibido desde seu controle básico.
Mas chega de teoria, vamos ao que interessa...
Modificando as classes da biblioteca
Como estamos fazendo tudo pensando no futuro, para os botões de controle de rolagem não criaremos nossos próprios identificadores de eventos, uma vez que iremos criá-los enquanto focamos em outros novos controles que também usarão botões (barra de rolagem, listas suspensas, etc.). Isso quer dizer que criaremos alguns eventos de clique genéricos no controle de rolagem ou de janelas suspensas.
No arquivo \MQL5\Include\DoEasy\Defines.mqh, adicionei novas constantes de enumeração na lista de possíveis eventos para controles WinForms:
//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+
Aqui, por precaução, criamos um identificador de evento para cancelar o clique no controle (em alguns casos é possível processar tal evento) e adicionamos eventos gerais para controles que possuem botões que gerem a aparência do controle ou servem para interagir com ele.
Adicionamos uma nova propriedade à enumeração de propriedades inteiras do elemento gráfico na tela e aumentamos o número total de propriedades de 96 para 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 //+------------------------------------------------------------------+
O sinalizador de exibição de um controle não oculto indica que, se o controle não estiver oculto, mas esse sinalizador estiver desmarcado (false), esse controle não será exibido. O que significa que se o controle principal for exibido, então seus descendentes, para os quais este sinalizador está desmarcado, ainda permanecerão ocultos até que sejam forçados a serem exibidos definindo este sinalizador como true para eles e chamando o método Show().
E adicionamos estas novas propriedades à lista de possíveis critérios para ordenar elementos gráficos na tela:
//+------------------------------------------------------------------+ //| 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 }; //+------------------------------------------------------------------+
Agora poderemos selecionar, classificar e filtrar listas de objetos-elementos gráficos por esta nova propriedade.
No arquivo \MQL5\Include\DoEasy\Data.mqh, adicionamos os índices das novas mensagens da 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
e os textos das mensagens correspondentes aos índices recém-adicionados:
{"Запрос за пределами массива","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 modificar a classe do objeto-elemento gráfico no arquivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.
Vamos adicionar a nova propriedade recém-adicionada à estrutura do objeto :
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 as propriedades do objeto são gravadas na estrutura do objeto para salvá-lo em um arquivo e, em seguida, restaurar o objeto a partir do arquivo.
No bloco com métodos para acesso simplificado às propriedades do objeto, vamos adicionar dois novos métodos para configurar e obter a propriedade de exibição do objeto:
//+------------------------------------------------------------------+ //| 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); }
Os métodos simplesmente gravam o sinalizador passado na propriedade do objeto e retornam o valor escrito na propriedade do objeto.
Em ambos os construtores de classe, escrevemos o valor padrão na nova propriedade:
//+------------------------------------------------------------------+ //| 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()); } } //+------------------------------------------------------------------+
Por padrão, um objeto é definido como visível e exibido quando a visibilidade do objeto principal ou do base é ativada. Para definir a visibilidade do objeto manualmente, o valor do sinalizador deve ser definido como false. Neste caso, se o objeto base ou principal foi oculto e depois exibido, então este objeto atual permanecerá em um estado oculto e, para exibi-lo, nós precisaremos definir a propriedade Displayed como true e chamar o método Show() deste objeto.
No método que cria a estrutura do objeto, escrevemos o valor da propriedade do objeto no campo correspondente da estrutura:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
No método que cria um objeto a partir de uma estrutura, definimos o valor da nova propriedade do objeto a partir do campo correspondente da estrutura:
//+------------------------------------------------------------------+ //| 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 } //+------------------------------------------------------------------+
Na classe do objeto WinForms base no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, no método que retorna a descrição da propriedade inteira do elemento, escreveremos um bloco de código para retornar a descrição da nova propriedade do 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)) ) : "" ); } //+------------------------------------------------------------------+
Se esta propriedade for passada para o método, então, dependendo do sinalizador only_prop, uma mensagem de texto será criada — apenas o nome da propriedade (only_prop = true) ou junto com o valor definido para a propriedade (only_prop = false) .
Todos os controles, de uma forma ou de outra, usarão a funcionalidade de evento, tanto para uso interno quanto para notificar o programa sobre eventos que ocorrem em sua GUI. A classe principal para interação com o usuário é a classe de objeto-forma, ela implementa a funcionalidade de interação do mouse, e a classe base de todos os objetos da biblioteca WinForms é herdada dela. Nela, criaremos um método de envio de mensagens de elementos gráficos.
No arquivo de classe do objeto-forma \MQL5\Include\DoEasy\Objects\Graph\Form.mqh, em sua seção protegida, declararemos um método para envio de mensagens.
O método será virtual caso algum objeto filho precise sobrescrevê-lo:
//--- '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:
No método que mostra a forma, escreveremos a verificação do sinalizador de exibição do objeto (verificação da nova propriedade do 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(); } //+------------------------------------------------------------------+
Ao chamar este método para qualquer objeto do tipo CForm ou superior, o sinalizador de exibição do objeto será verificado primeiro e, se o sinalizador não estiver definido (o controle de visibilidade manual ativado), abandonamos imediatamente o método.
Fora do corpo da classe, vamos escrever a implementação do método que envia a mensagem sobre o 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; } //+------------------------------------------------------------------+
Aqui obtemos ponteiros para os objetos principal e base e obtemos seus nomes. Nesse caso, se o ponteiro para o objeto principal for NULL, provavelmente esse é o objeto principal. Para fazer isso, verificamos se é assim e, em caso afirmativo, usamos o nome do objeto atual. Se, por algum motivo, o ponteiro não for recebido, para o nome usamos a string "Lost name of object", para que possamos controlá-lo.
Em seguida, precisamos descobrir o tipo do objeto base ao qual o objeto base deste objeto está anexado (ou seja, temos que obter seu objeto base a partir do objeto base, bem como seu tipo a partir dele próprio) e gravar todos os dados recebidos nas variáveis enviadas na mensagem do evento. Em lparam enviaremos o identificador do objeto atual, em dparam enviaresmos o tipo do objeto base ao qual está anexado o objeto base do objeto atual, e em sparam enviaremos uma string que contém os nomes de três objetos separados pelo delimitador ";" - esses objetos são o principal, o base e o atual. Ao receber um evento, podemos usar esses dados para determinar exatamente de qual objeto veio a mensagem do evento.
No momento, essa lógica é suficiente para determinar o objeto que gerou o evento, mas vamos alterá-la mais tarde, pois não permite rastrear todo o aninhamento de objetos ao criar controles mais complexos com uma hierarquia mais profunda de aninhamento.
Agora vamos adicionar o envio de mensagens de eventos aos manipuladores de eventos dos objetos WinForms.
No arquivo de classe do objeto-botão \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, no manipulador do evento "Cursor dentro da área ativa, botão (esquerdo) do mouse liberado" escrevemos o envio de mensagens sobre o 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); } //+------------------------------------------------------------------+
No arquivo \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); } //+------------------------------------------------------------------+
No arquivo \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); } //+------------------------------------------------------------------+
Já após a publicação do último artigo, notei que dois arquivos de classe dos objetos-botões que têm as setas esquerda-direita e para cima-para baixo pararam ser compilados sozinhos. Eles só podem ser compilados como parte da biblioteca, isto é, ao compilar o arquivo principal da biblioteca Engine.mqh, mas já sozinhos não é possível. Isso traz confusão. E para corrigir essa situação, temos que alterar a lista de arquivos de inclusão nos arquivos desses objetos.
Anteriormente, tínhamos anexado o arquivo do objeto-painel:
//+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\Containers\Panel.mqh" //+------------------------------------------------------------------+
Agora vamos anexar apenas os arquivos que devem ser usados por essas classes.
Para o arquivo do objeto-botão com as setas para cima-baixo \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 o arquivo do objeto-botão com as setas para esquerda-direita \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" //+------------------------------------------------------------------+
Agora ambos os arquivos serão compilados normalmente de forma independente e como parte da biblioteca.
Modificamos a classe do objeto-cabeçalho da guia do controle TabControl no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.
No manipulador de eventos "Cursor dentro da área ativa, botão (esquerdo) do mouse liberado" removemos o bloco de código para criação do 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); } } //+------------------------------------------------------------------+
Agora temos um método para criar e enviar um evento, vamos usá-lo:
//+------------------------------------------------------------------+ //| '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); } //+------------------------------------------------------------------+
Quando rolamos a fileira de cabeçalhos, por exemplo, para a esquerda, o cabeçalho mais à esquerda se move para fora do controle e o que está à direita dele toma seu lugar. Como a coordenada inicial dos cabeçalhos é deslocada dois pixels para a direita a partir da borda esquerda do contêiner, dois pixels do cabeçalho que vai além da borda esquerda permanecem visíveis nesta mesma área, por isso ele é recortado seguindo as bordas da área do contêiner dentro da qual os elementos devem estar visíveis.
Para ocultar essa fina parte visível do cabeçalho que saiu da borda esquerda, precisamos ajustar ligeiramente as dimensões da área do contêiner dentro da qual os objetos anexados são exibidos. Nesse caso, também precisamos levar em consideração o fato de o cabeçalho estar selecionado ou não, pois o cabeçalho selecionado aumenta de tamanho em dois pixels de cada lado. E para que, ao ser selecionado, dois dos seus pixels não sejam cortados, devemos levar isso em consideração. Ou seja, precisamos redimensionar dinamicamente a área do contêiner onde os objetos estão visíveis, dependendo de qual objeto está na borda. Se selecionado, não alteramos o tamanho e, se não selecionado, reduzimos dois pixels do mesmo.
No método que recorta a imagem, delimitada pela área retangular visível a ser calculada, adicionamos um ajuste do tamanho da área visível do contêiner e adicionamos o valor resultante às bordas da área visível:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Agora, quando os cabeçalhos forem além do contêiner, sua seção estreita, com dois pixels de tamanho, não será exibida.
No arquivo de classe do objeto WinForms TabControl \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh, em sua seção privada, declararemos novos 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:
e removemos o 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
Tornamos esse método público no último artigo, e lembremos que ele deslocava a fileira de cabeçalhos para a esquerda quando um cabeçalho parcialmente oculto era clicado. Agora os métodos declarados acima irão encarregar-se disso. Além disso, eles processarão o clique em um cabeçalho parcialmente oculto e o clique no botão que controla a rolagem da fileira de cabeçalhos.
Cada um dos métodos declarados é projetado para rolar a fileira de cabeçalhos em sua própria direção:
- Quando os cabeçalhos estão na parte superior ou inferior, existem dois métodos de rolagem para a esquerda e rolagem para a direita.
- Quando os cabeçalhos estão à esquerda, há dois métodos para rolagem à esquerda e rolagem à direita.
- Quando os cabeçalhos estão à direita, tem dois métodos de rolagem para a esquerda e rolagem para a direita.
No método que cria o número especificado de guias, ao criar objetos-botões com setas esquerda-direita e cima-baixo, precisamos especificar os objetos principal e base para cada uma desses objetos e para cada objeto-botão com seta que faz parte desses objetos, caso contrário, não conseguiremos encontrar esses objetos ao criar a mensagem sobre o evento ao clicar o respectivo botão:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Depois de criar os objetos-botões com as setas esquerda-direita e cima-baixo, obtemos um ponteiro para o objeto criado e definimos os objetos principal e base para ele. Então, do objeto resultante, obtemos seus objetos-botões com setas, e para cada um deles especificamos os objetos tanto principal como nosso básico.
No método que mostra o controle, vamos adicionar uma verificação de sinalizador de exibição de 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(); } //+------------------------------------------------------------------+
Se a exibição manual do objeto estiver habilitada, abandonamos o método.
Método que retorna um ponteiro para o primeiro cabeçalho visível:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
O primeiro cabeçalho visível é quer seja o cabeçalho à esquerda quando os cabeçalhos estão na parte superior/inferior ou o cabeçalho abaixo quando os cabeçalhos estão à esquerda ou o cabeçalho na parte superior quando os cabeçalhos estão à direita do controle. Para encontrar o cabeçalho extremo, precisamos fazer um loop sobre todos os cabeçalhos do objeto para verificar se suas coordenadas correspondem à localização da fileira de cabeçalhos. Para ser posicionado no topo, o cabeçalho deve estar localizado na coordenada X inicial do contêiner. Nesse caso, se o cabeçalho não for selecionado, sua coordenada inicial será deslocada dois pixels para a direita. E será feito da mesma forma quando a fileira de cabeçalhos tiver uma localização diferente.
O método no loop procura uma correspondência entre as coordenadas do objeto e as coordenadas do contêiner, dependendo da localização da fileira de cabeçalhos, e retorna um ponteiro para o objeto encontrado. Se nenhum dos cabeçalhos for encontrado, o método retornará NULL.
Método que rola a fileira de cabeçalhos para a esquerda:
//+------------------------------------------------------------------+ //| 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 rola a fileira de cabeçalhos para a direita:
//+------------------------------------------------------------------+ //| 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 rola para cima a fileira de cabeçalhos quando eles estão à esquerda:
//+------------------------------------------------------------------+ //| 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 rola para baixo a fileira de cabeçalhos quando eles estão à esquerda:
//+------------------------------------------------------------------+ //| 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 rola para cima a fileira de cabeçalhos quando eles estão à direita:
//+------------------------------------------------------------------+ //| 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 rola para baixo a fileira de cabeçalhos quando eles estão à direita:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
A lógica de todos os métodos para rolar as linhas de cabeçalho está totalmente descrita no código dos métodos. Todos eles são idênticos entre si e diferem apenas ligeiramente em relação aos cálculos de deslocamento e área visível. Espero que os métodos não precisem de explicações adicionais. Em qualquer caso, todas as perguntas podem ser colocadas na discussão do artigo.
Manipulador 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; } } } } //+------------------------------------------------------------------+
Agora processamos cada evento, quer seja ele a seleção um cabeçalho de guia parcialmente oculto ou o clique no botão para rolar a fileira de cabeçalhos, usando os métodos mencionados acima. Em geral, dependendo da localização da fileira de cabeçalhos e do evento de clique no botão/cabeçalho, chamamos o método apropriado para rolar a fileira de cabeçalhos.
Na classe-coleção de elementos gráficos no arquivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, em seu manipulador de eventos, agora precisamos processar corretamente os eventos recebidos dos objetos WinForms. Em outras palavras, devemos obter três nomes a partir do parâmetro de string sparam e encontrar o objeto base, e temos que encontrar a partir deste último aquele que gerou o evento. Depois de encontrá-lo, e se este objeto pertencer ao controle TabControl, então chamamos o manipulador de eventos do controle TabControl, enviando o identificador do evento para ele.
//+------------------------------------------------------------------+ //| 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 //---... //---...
Aqui também toda a lógica está completamente descrita no código, e espero que não precise de explicações adicionais.
Agora está tudo pronto para testar o que fizemos aqui hoje.
Teste
Para testar, vamos pegar o Expert Advisor do artigo anterior e salvá-lo na nova pasta \MQL5\Experts\TestDoEasy\Part119\ com o novo nome TestDoEasy119.mq5.
A única diferença entre este Expert Advisor e a versão anterior é que iremos criar 15 guias no 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)); } }
Dava para deixar como estava, quer dizer, 11 guias, mas para testar a performance e procurar alguns "bugs" aumentei o número de abas. É por isso que esse número é apenas o resultado da depuração e solução de problemas ao mover o cabeçalho selecionado para fora do contêiner em ambos os lados.
Compilamos o Expert Advisor e o iniciamos no gráfico:
Como podemos ver, tudo o que queríamos fazer hoje funciona como planejado.
Existem duas falhas: se passarmos o mouse sobre a área do cabeçalho da guia que está oculta, o cabeçalho reage mudando de cor, como se estivesse visível neste local. Esta é a razão pela qual a área ativa de um elemento não muda de tamanho quando a área visível é redimensionada. Para corrigi-lo, precisaremos calcular e redimensionar a área ativa de acordo com a visível.
O segundo defeito é que se movermos o cabeçalho selecionado para fora do contêiner e mover o painel, dois pixels do cabeçalho oculto serão exibidos. Isso tem a ver com o dimensionamento da guia para o cálculo da área visível, pois o tamanho de cada lado - do cabeçalho selecionado - aumenta dois pixels. Para corrigir isso, precisamos pensar em como, dentro do objeto-cabeçalho da guia, obter o tamanho do cabeçalho adjacente de acordo com o qual o tamanho da área de visibilidade é calculado.
Trataremos disso em artigos posteriores junto com o desenvolvimento de um novo objeto WinForms.
O que virá a seguir?
No próximo artigo, começaremos a desenvolver o objeto WinForms SplitContainer.
*Artigos desta série:
DoEasy. Controles (Parte 13): Otimizando a interação de objetos WinForms com o mouse, dando início ao desenvolvimento do objeto WinForms TabControl
DoEasy. Controles (Parte 14): Novo algoritmo para nomear elementos gráficos. Continuando o trabalho no objeto WinForms TabControl
DoEasy. Controles (Parte 15): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, métodos de manuseio de guias
DoEasy. Controles (Parte 16): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, modo esticamento de cabeçalhos consoante o tamanho do contêiner
DoEasy. Controles (Parte 17): Recorte de seções invisíveis de objetos, objetos-botões WinForms auxiliares com setas
DoEasy. Controles (Parte 18): Preparando a funcionalidade para rolagem de guias no TabControl
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/11490
![Redes neurais de maneira fácil (Parte 30): Algoritmos genéticos](https://c.mql5.com/2/49/Neural_networks_made_easy_024.png)
![Aprendendo a construindo um Expert Advisor que opera de forma automática (Parte 10): Automação (II)](https://c.mql5.com/2/50/aprendendo_construindo_010_avatar.png)
![DoEasy. Controles (Parte 20): Objeto WinForms SplitContainer](https://c.mql5.com/2/49/MQL5-avatar-doeasy-library-2__1.png)
![Redes neurais de maneira fácil (Parte 29): Algoritmo ator-crítico de vantagem (Advantage actor-critic)](https://c.mql5.com/2/48/Neural_networks_made_easy_022__1.png)
![MQL5 - Linguagem para estratégias de negociação inseridas no terminal do cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso