Interfaces gráficas IV: Modo de ventanas múltiples y sistema de prioridades (Capítulo 2)
Anatoli Kazharski | 28 abril, 2016
Índice
- Introducción
- Modo de ventanas múltiples
- Prueba del modo de ventanas múltiples
- Mejorando el sistema de prioridades para el clic izquierdo del ratón
- Conclusión
Introducción
El primer artículo de la serie nos cuenta con más detalles para qué sirve esta librería: Interfaces gráficas I: Preparación de la estructura de la librería (Capítulo 1). Al final de cada artículo de la serie se muestra la lista completa de los capítulos con los enlaces. Además, se puede descargar la versión completa de la librería en la fase actual del desarrollo del proyecto. Es necesario colocar los ficheros en los mismos directorios, tal como están ubicados en el archivo.
En el capítulo anterior de la cuarta parte de la serie, hemos analizado los elementos “Barra de estado” y “Descripción emergente” que pertenecen a los componentes informativos de la interfaz gráfica. En este artículo vamos a ampliar la implementación de la librería hasta conseguir la posibilidad de crear las interfaces de ventanas múltiples para nuestras aplicaciones MQL. Aparte de eso, desarrollaremos el sistema de las prioridades para el clic izquierdo del ratón en los objetos gráficos, puesto que si no lo hacemos, podemos enfrentarnos a los problemas cuando los controles no responden inesperadamente a las acciones del usuario.
Modo de ventanas múltiples
Vamos a considerar el modo de ventanas múltiples de la interfaz gráfica de nuestra librería. Hasta ahora, en la enumeración ENUM_WINDOW_TYPE estaban previstos dos identificadores para la ventana principal (W_MAIN) y la ventana de diálogo (W_DIALOG). Se utilizaba sólo un modo- el modo de una ventana. A continuación, introduciremos las adiciones, después de las cuales para activar el modo de ventanas múltiples será suficiente sólo crear y añadir a la base el número necesario de los formularios para los controles.
En la clase principal del procesamiento de eventos CWndEvents, hay que crear un campo para guardar el índice de la ventana activa en este momento:
class CWndEvents : public CWndContainer { protected: //--- Índice de la ventana activa int m_active_window_index; };
Vamos a ver cómo va a identificarse el índice de la ventana activa. Por ejemplo, el usuario ha asignado la apertura de la ventana de diálogo (W_DIALOG) a un botón. Cuando se pulsa el botón, se genera el evento personalizado ON_CLICK_BUTTON. Se puede seguir este evento en el procesador de eventos CProgram::OnEvent() de la clase personalizada. Aquí mismo utilizamos el método CWindow::Show() del formulario a mostrar. En la implementación actual de la librería no es suficiente, por eso vamos a introducir las adiciones necesarias.
Desde el método CWindow::Show() hay que enviar el evento personalizado que va a avisar que ha sido abierta una ventana y hay que cambiar los valores de los parámetros del sistema de la interfaz gráfica. Para eso, hace falta un identificador separado. Vamos a llamarlo ON_OPEN_DIALOG_BOX y colocarlo en el archivo Defines.mqh donde se encuentran los demás identificadores de la librería:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #define ON_OPEN_DIALOG_BOX (11) // Evento de la apertura de la ventana de diálogo
Hay que añadir una línea al final del método CWindow::Show(), tal como se muestra en el código de abajo (la versión reducida del método). Para la identificación nítida del iniciador del mensaje, aparte del identificador del evento, hay que enviar el identificador del elemento y el nombre del programa.
//+------------------------------------------------------------------+ //| Muestra la ventana | //+------------------------------------------------------------------+ void CWindow::Show(void) { //--- Hacer que todos los objetos sean visibles //--- Estado de visibilidad //--- Puesta a cero del foco //--- Enviamos el mensaje sobre ello ::EventChartCustom(m_chart_id,ON_OPEN_DIALOG_BOX,(long)CElement::Id(),0,m_program_name); }
Este evento va a procesarse en la clase CWndEvents. Antes de implementar el método en el que va a ejecutarse el procesamiento, hay que crear tres métodos más en la clase CWindow. Dos métodos van a utilizarse para guardar y obtener el índice del formulario del que se abre la ventana de diálogo, el tercer método servirá para gestionar el estado del formulario.
Hay que guardar el índice de la ventana anterior activa ya que a la vez pueden estar abiertas varias ventanas. Por eso cuando se cierra una ventana de diálogo, es importante saber cuál de ellas es necesario devolver al estado activo.
class CWindow : public CElement { private: //--- Índice de la ventana anterior activa int m_prev_active_window_index; //--- public: //--- (1) Guardar y (2) obtener el índice de la ventana anterior activa void PrevActiveWindowIndex(const int index) { m_prev_active_window_index=index; } int PrevActiveWindowIndex(void) const { return(m_prev_active_window_index); } };
En cuanto a la gestión del estado del formulario, los formularios desactivados tendrán el color del encabezado diferente que puede ser cambiado por el usuario. Además, el color de los elementos no va a cambiarse al situar el cursor sobre ellos porque el formulario estará bloqueado. Aparte de eso, en el momento de la desactivación va a generarse un evento de usuario cuyo objetivo será avisar sobre el hecho de que el formulario está bloqueado y hay que anular los focos y los colores de todos los elementos. Es que cuando el formulario está bloqueado, el foco sobre los elementos no se monitorea. Cuando se abre la ventana de diálogo, el color del control con el que ha sido abierta seguirá siendo el mismo como si el foco del ratón permanezca estando sobre él.
Para este mensaje, en el archivo Defines.mqh se crea el identificador ON_RESET_WINDOW_COLORS:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #define ON_RESET_WINDOW_COLORS (13) // Resetear los colores de todos los elementos del formulario
Aquí tenemos el método para gestionar el estado del formulario:
class CWindow : public CElement { public: //--- Establecer el estado de la ventana void State(const bool flag); }; //+------------------------------------------------------------------+ //| Establece el estado de la ventana | //+------------------------------------------------------------------+ void CWindow::State(const bool flag) { //--- Si hay que bloquear la ventana if(!flag) { //--- Establecemos el estatus m_is_locked=true; //--- Establecemos el color del encabezado m_caption_bg.BackColor(m_caption_bg_color_off); //--- Señal para resetear el color. Los demás elementos también serán reseteados. ::EventChartCustom(m_chart_id,ON_RESET_WINDOW_COLORS,(long)CElement::Id(),0,""); } //--- Si hay que desbloquear la ventana else { //--- Establecemos el estatus m_is_locked=false; //--- Establecemos el color del encabezado m_caption_bg.BackColor(m_caption_bg_color); //--- Resetear el foco CElement::MouseFocus(false); } }
Ahora volvemos al procesamiento del evento ON_OPEN_DIALOG_BOX. En la clase principal del procesamiento de eventos de la interfaz gráfica (CWndEvents) creamos el método CWndEvents::OnOpenDialogBox(). Vamos a llamar a este método en el método común de todos los eventos de usuario CWndEvents::ChartEventCustom().
El método CWndEvents::OnOpenDialogBox() se empieza con dos comprobaciones: para el identificador del evento y para el nombre del programa. Si han sido superadas con éxito, recorremos en el ciclo por todas las ventanas para averiguar qué ventana ha generado el evento. Podemos averiguarlo por el identificador del control que figura en este mensaje (lparam). Los formularios cuyos identificadores no coinciden serán bloqueados junto con todos los elementos adjuntos. Las prioridades de todos los objetos se ponen a cero a través del método ResetZorders() y no van a reaccionar al clic izquierdo del ratón. Cuando llegamos al formulario cuyos identificadores han coincidido, guardamos dentro el índice de la ventana actual activa como el índice de la “ventana anterior activa”. Activamos este formulario y restauramos la prioridad del clic izquierdo del ratón para todos sus objetos. Guardamos el índice de esta ventana como la “ventana activa en este momento”. Luego, hacemos que todos los elementos de este formulario sean visibles y recuperamos sus prioridades para el clic izquierdo del ratón, omitiendo el elemento del formulario (ya que está visible) y los elementos desplegables.
Si la ventana de diálogo se abre en el momento cuando se ve la descripción emergente, hay que ocultarla. No va a desaparecer por sí misma, porque el formulario al que pertenece ya está bloqueado. Precisamente para estas ocasiones hemos creado antes el array privado para las descripciones emergentes. En la clase principal del procesamiento de eventos (CWndEvents), en cualquier momento se puede obtener el acceso a los métodos de cualquier elemento de la base.
class CWndEvents : public CWndContainer { private: //--- Apertura de la ventana de diálogo bool OnOpenDialogBox(void); }; //+------------------------------------------------------------------+ //| Evento CHARTEVENT_CUSTOM | //+------------------------------------------------------------------+ void CWndEvents::ChartEventCustom(void) { //--- Si hay la señal para minimizar el formulario //--- Si hay la señal para maximizar el formulario //--- Si hay señal para ocultar los menús contextuales desde el elemento iniciador //--- Si hay señal para cerrar todos los menús contextuales //--- Si hay la señal para abrir la ventana de diálogo if(OnOpenDialogBox()) return; } //+------------------------------------------------------------------+ //| Evento ON_OPEN_DIALOG_BOX | //+------------------------------------------------------------------+ bool CWndEvents::OnOpenDialogBox(void) { //--- Si hay la señal para abrir la ventana de diálogo if(m_id!=CHARTEVENT_CUSTOM+ON_OPEN_DIALOG_BOX) return(false); //--- Salir si es la señal de otro programa if(m_sparam!=m_program_name) return(true); //--- Recorremos el array de ventanas int window_total=CWndContainer::WindowsTotal(); for(int w=0; w<window_total; w++) { //--- Si los identificadores coinciden if(m_windows[w].Id()==m_lparam) { //--- Guarde en este formulario el índice de la ventana desde la cual ha sido llamado m_windows[w].PrevActiveWindowIndex(m_active_window_index); //--- Activamos el formulario m_windows[w].State(true); //--- Restauramos para los objetos del formulario las prioridad del clic izquierdo del ratón m_windows[w].SetZorders(); //--- Guardamos el índice de la ventana activada m_active_window_index=w; //--- Hacer que todos los elementos de la ventana activada sean visibles int elements_total=CWndContainer::ElementsTotal(w); for(int e=0; e<elements_total; e++) { //--- Saltamos los formularios y los objetos deplegables if(m_wnd[w].m_elements[e].ClassName()=="CWindow" || m_wnd[w].m_elements[e].IsDropdown()) continue; //--- Hacer que el elemento sea visible m_wnd[w].m_elements[e].Show(); //--- Restaurar para el elemento la prioridad del clic izquierdo del ratón m_wnd[w].m_elements[e].SetZorders(); } Ocultar las descripciones emergentes int tooltips_total=CWndContainer::TooltipsTotal(m_windows[w].PrevActiveWindowIndex()); for(int t=0; t<tooltips_total; t++) m_wnd[m_windows[w].PrevActiveWindowIndex()].m_tooltips[t].FadeOutTooltip(); } //--- Otros formularios estarán bloqueados hasta que no se cierra la ventana activada else { //--- Bloqueamos el formulario m_windows[w].State(false); //--- Resetemos las prioridades de los elementos del formulario para el clic izquierdo int elements_total=CWndContainer::ElementsTotal(w); for(int e=0; e<elements_total; e++) m_wnd[w].m_elements[e].ResetZorders(); } } //--- return(true); }
Ahora vamos a aclarar el evento con el identificador ON_RESET_WINDOW_COLORS que hemos creado antes en este artículo. Antes de escribir el método para el procesamiento de este evento, hay que añadir otro método virtual estándar a la clase base de todos los controles CElement que va a utilizarse para resetear los colores. Lo llamaremos CElement::ResetColors():
class CElement { public: //--- Resetear color del elemento virtual void ResetColors(void) {} };
En todas las clases derivadas hay que crear sus métodos ResetColors() con las particularidades propias a cada elemento. El código de abajo contiene el ejemplo para el control “Botón con imagen” (CIconButton). Puede ver el método ResetColors() para los demás controles en los archivos adjuntos al artículo.
class CIconButton : public CElement { public: //--- Resetear color del elemento void ResetColors(void); }; //+------------------------------------------------------------------+ //| Resetea el color | //+------------------------------------------------------------------+ void CIconButton::ResetColors(void) { //--- Salir si es el modo de dos estados y el botón está pulsado if(m_two_state && m_button_state) return; //--- Resetear color m_button.BackColor(m_back_color); //--- Poner a cero el foco m_button.MouseFocus(false); CElement::MouseFocus(false); }
Por tanto, teniendo el método virtual en la clase base de los controles y su propia versión en las derivadas, se puede resetear en el ciclo el color de todos los elementos desde el manejador de eventos de la clase principal de la librería (CWndEvents).
Para el procesamiento del evento ON_RESET_WINDOW_COLORS escribiremos el método CWndEvents::OnResetWindowColors(). Aquí todo es muy simple. Buscamos el formulario recién desactivado por el identificador del control que ha llegado en el mensaje. Si lo encontramos, guardamos su índice. Luego, si el índice ha sido guardado, reseteamos los colores de todos los elementos de este formulario. Más detalles en el código de abajo:
class CWndEvents : public CWndContainer { private: //--- Resetear los colores del formulario y sus elementos bool OnResetWindowColors(void); }; //+------------------------------------------------------------------+ //| Evento CHARTEVENT_CUSTOM | //+------------------------------------------------------------------+ void CWndEvents::ChartEventCustom(void) { //--- Si hay la señal para minimizar el formulario //--- Si hay la señal para maximizar el formulario //--- Si hay señal para ocultar los menús contextuales desde el elemento iniciador //--- Si hay señal para cerrar todos los menús contextuales //--- Si hay la señal para abrir la ventana de diálogo //--- Si hay señal para resetear el color de los elementos en el formulario especificado if(OnResetWindowColors()) return; } //+------------------------------------------------------------------+ //| Evento ON_RESET_WINDOW_COLORS | //+------------------------------------------------------------------+ bool CWndEvents::OnResetWindowColors(void) { //--- Si hay la señal para resetear el color de la ventana if(m_id!=CHARTEVENT_CUSTOM+ON_RESET_WINDOW_COLORS) return(false); //--- Para identificar el índice del formulario del que ha llegado el mensaje int index=WRONG_VALUE; //--- Recorremos el array de ventanas int window_total=CWndContainer::WindowsTotal(); for(int w=0; w<window_total; w++) { //--- Si los identificadores coinciden if(m_windows[w].Id()==m_lparam) { //--- Guardamos el índice index=w; //--- Reseteamos el color del formulario m_windows[w].ResetColors(); break; } } //--- Salimos si el índice no está definido if(index==WRONG_VALUE) return(true); //--- Reseteamos los colores de todos los elementos del formulario int pConnection for(int e=0; e<elements_total; e++) m_wnd[index].m_elements[e].ResetColors(); //--- Redibujar el gráfico m_chart.Redraw(); return(true); }
Pues, con el proceso de la apertura de las ventanas todo está aclarado. Ahora hay que implementar los métodos para el cierre y la recuperación de la ventana anterior activa. Para el procesamiento de este evento hay que crear el identificador ON_CLOSE_DIALOG_BOX en el archivo Defines.mqh:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #define ON_CLOSE_DIALOG_BOX (12) // Evento del cierre de la ventana de diálogo
Ahora para el cierre del formulario, y para el cierre del programa, en la clase CWindow se utiliza el método CWindow::CloseWindow(). En este método, la sección para el cierre de las ventanas de diálogo (W_DIALOG) todavía no está implementada. Vamos a escribir el método adicional que va a generar el evento para el cierre de las ventanas de diálogo. Aparte del (1) identificador, el mensaje va a contener (2) el identificador del elemento, (3) el índice de la ventana anterior activa y (4) el texto del encabezado. Esté método va a llamarse CWindow::CloseDialogBox(). A continuación, vamos a usarlo también en los controles complejos donde el cierre de la ventana va a realizarse mediante otros elementos, distintos del botón de cierre.
class CWindow : public CElement { public: //--- Cierre de la ventana de diálogo void CloseDialogBox(void); }; //+------------------------------------------------------------------+ //| Cierre de la ventana de diálogo | //+------------------------------------------------------------------+ void CWindow::CloseDialogBox(void) { //--- Estado de visibilidad CElement::IsVisible(false); //--- Enviamos el mensaje sobre ello ::EventChartCustom(m_chart_id,ON_CLOSE_DIALOG_BOX,CElement::Id(),m_prev_active_window_index,m_caption_text); }
En la clase CWindow la llamada al método CWindow::CloseDialogBox() debe realizarse en el método CWindow::CloseWindow(), tal como se muestra en la versión reducida del código de abajo. Los archivos adjuntos contienen su versión completa.
//+------------------------------------------------------------------+ //| Cierre del cuadro de diálogo o la ventana del programa | //+------------------------------------------------------------------+ bool CWindow::CloseWindow(const string pressed_object) { //--- Si es clic fuera del botón del cierre de la ventana if(pressed_object!=m_button_close.Name()) return(false); //--- Si es laa ventana principal if(m_window_type==W_MAIN) { //--- ... } //--- Si es cuadro de diálogo else if(m_window_type==W_DIALOG) { //--- La cerramos CloseDialogBox(); } //--- return(false); }
Después del enví del mensaje con el identificador ON_CLOSE_DIALOG_BOX, hay que seguir y procesarlo en el manejador de la clase CWndEvents. Para eso escribiremos el método CWndEvents::OnCloseDialogBox(). Ahí, en el ciclo recorremos todas las ventanas en la base buscando la ventana cuyo identificador del elemento va a coincidir con el identificador en el mensaje. Si la encontramos, hay que desactivarla. Luego la ocultamos junto con todos los elementos adjuntos y activamos el formulario por el índice pasado en el mensaje. Guardamos el índice de la ventana actual activa y recuperamos las prioridades de los elementos para el clic izquierdo.
class CWndEvents : public CWndContainer { private: //--- Cierre de la ventana de diálogo bool OnCloseDialogBox(void); }; //+------------------------------------------------------------------+ //| Evento CHARTEVENT_CUSTOM | //+------------------------------------------------------------------+ void CWndEvents::ChartEventCustom(void) { //--- Si hay la señal para minimizar el formulario //--- Si hay la señal para maximizar el formulario //--- Si hay señal para ocultar los menús contextuales desde el elemento iniciador //--- Si hay señal para cerrar todos los menús contextuales //--- Si hay la señal para abrir la ventana de diálogo //--- Si hay la señal para cerrar la ventana de diálogo if(OnCloseDialogBox()) return; //--- Si hay señal para resetear el color de los elementos en el formulario especificado } //+------------------------------------------------------------------+ //| Evento ON_CLOSE_DIALOG_BOX | //+------------------------------------------------------------------+ bool CWndEvents::OnCloseDialogBox(void) { //--- Si hay la señal para cerrar la ventana de diálogo if(m_id!=CHARTEVENT_CUSTOM+ON_CLOSE_DIALOG_BOX) return(false); //--- Recorremos el array de ventanas int window_total=CWndContainer::WindowsTotal(); for(int w=0; w<window_total; w++) { //--- Si los identificadores coinciden if(m_windows[w].Id()==m_lparam) { //--- Bloqueamos el formulario m_windows[w].State(false); //--- Ocultamos el formulario int elements_total=CWndContainer::ElementsTotal(w); for(int e=0; e<elements_total; e++) m_wnd[w].m_elements[e].Hide(); //--- Activamos el formulario anterior m_windows[int(m_dparam)].State(true); //--- Redibujar el gráfico m_chart.Redraw(); break; } } //--- Establecer el índice de la ventana anterior m_active_window_index=int(m_dparam); //--- Recuperación de las prioridades del clic izquierdo del ratón para la ventana activada int elements_total=CWndContainer::ElementsTotal(m_active_window_index); for(int e=0; e<elements_total; e++) m_wnd[m_active_window_index].m_elements[e].SetZorders(); //--- return(true); }
Todo está listo para probar el modo de ventanas múltiples.
Prueba del modo de ventanas múltiples
En el EA que ha sido usado para probar los elementos informativos de la interfaz, vamos a crear dos instancias de la clase CWindow Como resultado, tendremos tres formularios en la interfaz gráfica del EA. El primer formulario es principal (W_MAIN), otros dos van a servir de las ventanas de diálogo (W_DIALOG). Adjuntaremos la primera ventana de diálogo a uno de los botones del formulario principal. Ahí crearemos tres botones y adjuntaremos la segunda ventana de diálogo a uno de ellos. Al final, conseguiremos la situación cuando los tres formularios estarán abiertos al mismo tiempo, y sólo uno de ellos estará activo (disponible).
En el siguiente código se muestra qué es lo que hay que añadir a la clase de usuario de la aplicación (CProgram) en esta fase del desarrollo:
class CProgram : public CWndEvents { private: //--- Formulario 2 CWindow m_window2; //--- Botones con imágenes CIconButton m_icon_button6; CIconButton m_icon_button7; CIconButton m_icon_button8; //--- Formulario 3 CWindow m_window3; //--- private: //--- Formulario 2 bool CreateWindow2(const string text); //--- Botones con imágenes #define ICONBUTTON6_GAP_X (7) #define ICONBUTTON6_GAP_Y (25) bool CreateIconButton6(const string text); #define ICONBUTTON7_GAP_X (7) #define ICONBUTTON7_GAP_Y (50) bool CreateIconButton7(const string text); #define ICONBUTTON8_GAP_X (7) #define ICONBUTTON8_GAP_Y (75) bool CreateIconButton8(const string text); //--- Formulario 3 bool CreateWindow3(const string text); };
Colocamos la llamada a estos métodos en el método principal de la creación de la interfaz gráfica de la aplicación. En el código de abajo se muestra la versión reducida de este método.
//+------------------------------------------------------------------+ //| Crea el panel de trading | //+------------------------------------------------------------------+ bool CProgram::CreateTradePanel(void) { //--- Creación del formulario 1 para los controles //--- Creación de controles: // Menú principal //--- Menús contextuales //--- Creación de la barra de estado //--- Botones con imagen //--- Creación del formulario 2 para los controles if(!CreateWindow2("Icon Button 1")) return(false); //--- Botones con imagen if(!CreateIconButton6("Icon Button 6...")) return(false); if(!CreateIconButton7("Icon Button 7")) return(false); if(!CreateIconButton8("Icon Button 8")) return(false); //--- Creación del formulario 3 para los controles if(!CreateWindow3("Icon Button 6")) return(false); //--- Descripciones emergentes //--- Redibujar el gráfico m_chart.Redraw(); return(true); }
Nosotros vamos a considerar el método sólo para la primera ventana de diálogo (el segundo formulario). Usted ya sabe que hay que usar el método CWndContainer::AddWindow() para añadir el formulario a la base. Luego, fíjese (véase el código de abajo) cómo se definen las coordenadas para el formulario. Por defecto, cuando el programa se carga en el gráfico tienen los valores cero, por eso van a establecerse las que considere convenientes. En este caso, como ejemplo, son las siguientes: x=1, y=20. Después de eso, se puede mover el formulario, y luego cambiar el período de tiempo o el símbolo del gráfico. Con está técnica, como se muestra en el código de abajo, el formulario se queda donde ha estado la última vez. Si necesita que el formulario se coloque en la posición inicial como durante el primer inicio del programa en el gráfico, simplemente quite estas condiciones. En nuestro ejemplo, los tres formularios de la interfaz gráfica van a tener estas condiciones.
Hagamos que se pueda mover los formularios por el gráfico. El tipo de la ventana tiene que ser establecido obligatoriamente como de diálogo (W_DIALOG). De lo contrario, la interfaz no va a comportarse de forma correcta. Se puede redefinir el icono de la ventana usando el método CWindow::IconFile(). Las ventanas de diálogo pueden tener el mismo icono que tiene el elemento a través del cual se abre esta ventana.
//+------------------------------------------------------------------+ //| Crea el formulario 2 para los controles | //+------------------------------------------------------------------+ bool CProgram::CreateWindow2(const string caption_text) { //--- Añadimos el puntero de la ventana al array de ventanas CWndContainer::AddWindow(m_window2); //--- Coordenadas int x=(m_window2.X()>0) ? m_window2.X() : 1; int y=(m_window2.Y()>0) ? m_window2.Y() : 20; //--- Propiedades return(true); m_window2.WindowType(W_DIALOG); m_window2.XSize(160); m_window2.YSize(160); m_window2.IconFile("Images\\EasyAndFastGUI\\Icons\\bmp16\\coins.bmp"); m_window2.CaptionBgColor(clrCornflowerBlue); m_window2.CaptionBgColorHover(C'150,190,240'); //--- Creación del formulario if(!m_window2.CreateWindow(m_chart_id,m_subwin,caption_text,x,y)) return(false); //--- return(true); }
Voy a recordar algunos momentos respecto a la manera de adjuntar los controles a una determinada ventana de diálogo. Como ejemplo, veremos uno de los métodos de los botones destinados para este formulario. Aquí hay que destacar sólo dos momentos.
Debe recordar que:
- Al control hay que pasarle el puntero del formulario al que debe adjuntarse.
- Cuando guarda el puntero al control en la base, indique el índice del formulario al que se adjunta el control. En este caso, es 1.
//+------------------------------------------------------------------+ //| Crea el botón con imagen 6 | //+------------------------------------------------------------------+ bool CProgram::CreateIconButton6(const string button_text) { //--- Guardamos el puntero a la ventana m_icon_button6.WindowPointer(m_window2); //--- Coordenadas int x=m_window2.X()+ICONBUTTON6_GAP_X; int y=m_window2.Y()+ICONBUTTON6_GAP_Y; //--- Establecemos las propiedades antes de la creación m_icon_button6.TwoState(false); m_icon_button6.ButtonXSize(146); m_icon_button6.ButtonYSize(22); m_icon_button6.LabelColor(clrBlack); m_icon_button6.LabelColorPressed(clrBlack); m_icon_button6.BorderColorOff(clrWhite); m_icon_button6.BackColor(clrLightGray); m_icon_button6.BackColorHover(C'193,218,255'); m_icon_button6.BackColorPressed(C'153,178,215'); m_icon_button6.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp"); m_icon_button6.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp16\\script_colorless.bmp"); //--- Creamos el control if(!m_icon_button6.CreateIconButton(m_chart_id,m_subwin,button_text,x,y)) return(false); //--- Añadimos el puntero al control a la base CWndContainer::AddToElementsArray(1,m_icon_button6); return(true); }
El desarrollador de la aplicación debe encargarse del control de la visualización de una u otra ventana. En el manejador de eventos de la clase personalizada (CProgram), hay que monitorear el clic en un control y mostrar la ventana necesaria. La llamada a la primera ventana de diálogo se vinculará al botón en la ventana principal del EA (el segundo formulario), la llamada a la segunda ventana se vinculará al botón en la primera ventana de diálogo (tercer formulario).
//+------------------------------------------------------------------+ //| Manejador de eventos | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Evento del clic en el botón if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { //--- Si el texto coincide if(sparam==m_icon_button1.Text()) { //--- Mostrar la ventana 2 m_window2.Show(); } //--- Si el texto coincide if(sparam==m_icon_button6.Text()) { //--- Mostrar la ventana 3 m_window3.Show(); } } }
En la captura de abajo se muestra el resultado que se debe obtener. Fíjese en los puntos suspensivos en los nombres de los botones «Icon Button 1...» y «Icon Button 6...». Normalmente, de esta manera al usuario se la da a entender que al hacer clic en este control, se abre una ventana de diálogo.
Fig. 1. Prueba del modo de ventanas múltiples.
Si ahora, en el momento cuando están abiertos varios formularios, Usted cambia el símbolo o período del gráfico, se enfrentará con un problema. Las ventanas de diálogo desaparecerán, como debe ser, pero la ventana principal no obtendrá el control. Por esta razón, el formulario no va a reaccionar a las acciones del usuario. Este problema se soluciona muy fácil. Recordamos que en el método de la deinicialización de la clase personalizada CProgram::OnDeinitEvent() se invoca el método CWndEvents::Destroy(). Ahí se elimina la interfaz gráfica de la aplicación. Hay que pasar el control a la ventana principal precisamente en el momento de la eliminación de la interfaz gráfica. Por eso hay que introducir pequeñas adiciones en el método CWndEvents::Destroy():
- Establecer el índice de la ventana principal como “activo”.
- Activar la ventana principal y desactivar las demás.
En el listado del código de abajo se muestra la versión actual del método CWndEvents::Destroy():
//+------------------------------------------------------------------+ //| Eliminación de todos los objetos | //+------------------------------------------------------------------+ void CWndEvents::Destroy(void) { //--- Establecemos el índice de la ventana principal m_active_window_index=0; //--- Obtenemos el número de ventanas int window_total=CWndContainer::WindowsTotal(); //--- Recorremos el array de ventanas for(int w=0; w<window_total; w++) { //--- Activamos la ventana principal if(m_windows[w].WindowType()==W_MAIN) m_windows[w].State(true); //--- Bloqueamos las ventanas de diálogo else m_windows[w].State(false); } //--- Liberamos los arrays de controles for(int w=0; w<window_total; w++) { int elements_total=CWndContainer::ElementsTotal(w); for(int e=0; e<elements_total; e++) { //--- Si el puntero no es válido, ir al siguiente if(::CheckPointer(m_wnd[w].m_elements[e])==POINTER_INVALID) continue; //--- Eliminar los objetos del control m_wnd[w].m_elements[e].Delete(); } //--- Liberar los arrays de controles ::ArrayFree(m_wnd[w].m_objects); ::ArrayFree(m_wnd[w].m_elements); ::ArrayFree(m_wnd[w].m_context_menus); } //--- Liberar los arrays de formularios ::ArrayFree(m_wnd); ::ArrayFree(m_windows); }
Hemos implementado la primera versión del modo de ventanas múltiples. Como podemos ver, no ha sido tan complicado como podía parecer a la primera vista.
Mejorando el sistema de prioridades para el clic izquierdo del ratón
Hasta este momento, el control de las prioridades del clic izquierdo en los controles de la interfaz se realizaba por los eventos con los identificadores ON_OPEN_DIALOG_BOX y ON_CLOSE_DIALOG_BOX. La razón de eso era que durante el desarrollo del siguiente control desplegable, el usuario asignaba personalmente el valor de la prioridad para cada objeto de este control. Se tomaban en cuenta las prioridades de otros elementos que pueden encontrarse debajo de ellos. Pero cuando se crean los controles complejos, eso se convierte en un sistema demasiado “embarazoso” en el que se puede confundirse fácilmente. Para hacer las cosas más claras y sencillas, vamos a crear dos identificadores más para estos eventos:
- ON_ZERO_PRIORITIES – anulación de prioridades.
- ON_SET_PRIORITIES – recuperación de prioridades.
Vamos a añadirlos al archivo Defines.mqh:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #define ON_ZERO_PRIORITIES (14) // Poner a cero las prioridades para el clic izquierdo del ratón #define ON_SET_PRIORITIES (15) // Recuperar las prioridades para el clic izquierdo del ratón
La generación de eventos con estos identificadores debe ubicarse en las clases de los elementos que son (pueden ser) desplegables. En esta fase del desarrollo de la librería, en el presente conjunto de la interfaz, este control es “Menú contextual”. Por eso ahora en la clase CContextMenu en los métodos Show() y Hide() hay que añadir el código, tal como se muestra en el código de abajo (versiones reducidas de los métodos):
//+------------------------------------------------------------------+ //| Muestra el menú contextual | //+------------------------------------------------------------------+ void CContextMenu::Show(void) { //--- Salir si el control ya está visible //--- Mostrar los objetos del menú contextual //--- Mostrar los elementos del menú //--- Conceder el estatus del control visible //--- Estado del menú contextual //--- Anotar el estado en el nodo anterior //--- Bloquear el formulario //--- Enviar la señal para anular las prioridades para el clic izquierdo del ratón ::EventChartCustom(m_chart_id,ON_ZERO_PRIORITIES,CElement::Id(),0.0,""); } //+------------------------------------------------------------------+ //| Oculta el menú contextual | //+------------------------------------------------------------------+ void CContextMenu::Hide(void) { //--- Salir si el control está ocultado //--- Ocultar los objetos del menú contextual //--- Ocultar los elementos del menú //--- Poner a cero el foco //--- Estado del menú contextual //--- Anotar el estado en el nodo anterior //--- Enviar la señal para recuperar las prioridades para el clic izquierdo del ratón ::EventChartCustom(m_chart_id,ON_SET_PRIORITIES,0,0.0,""); }
Vamos a recibir estos mensajes en la clase principal del procesamiento de todos los mensajes (CWndEvents). Para estos fines, escribiremos un método separado de procesamiento para cada identificador. Estos métodos van a invocarse en el método principal del procesamiento de eventos de usuario CWndEvents::ChartEventCustom().
class CWndEvents : public CWndContainer { private: //--- Resetear las prioridades para el clic izquierdo del ratón bool OnZeroPriorities(void); //--- Recuperar las prioridades para el clic izquierdo del ratón bool OnSetPriorities(void); }; //+------------------------------------------------------------------+ //| Evento CHARTEVENT_CUSTOM | //+------------------------------------------------------------------+ void CWndEvents::ChartEventCustom(void) { //--- Si hay la señal para minimizar el formulario //--- Si hay la señal para maximizar el formulario //--- Si hay señal para ocultar los menús contextuales desde el elemento iniciador //--- Si hay señal para cerrar todos los menús contextuales //--- Si hay la señal para abrir la ventana de diálogo //--- Si hay la señal para cerrar la ventana de diálogo //--- Si hay señal para resetear el color de los elementos en el formulario especificado //--- Si hay señal para resetear las prioridades para el clic izquierdo del ratón if(OnZeroPriorities()) return; //--- Si hay señal para recuperar las prioridades para el clic izquierdo del ratón if(OnSetPriorities()) return; }
En el método CWndEvents::OnZeroPriorities() repasamos en el ciclo todos los controles de la ventana activa y anulamos las prioridades para todos a excepción del control cuyo identificador se encuentra en el mensaje (parámetro lparam), y salvo los elementos del menú y menús contextuales. La razón de la excepción de los elementos del menú y menús contextuales consiste en el hecho de que pueden estar abiertos varios menús contextuales al mismo tiempo (uno de otro).
//+------------------------------------------------------------------+ //| Evento ON_ZERO_PRIORITIES | //+------------------------------------------------------------------+ bool CWndEvents::OnZeroPriorities(void) { //--- Si hay señal para anular las prioridades para el clic izquierdo del ratón if(m_id!=CHARTEVENT_CUSTOM+ON_ZERO_PRIORITIES) return(false); //--- int elements_total=CWndContainer::ElementsTotal(m_active_window_index); for(int e=0; e<elements_total; e++) { //--- Anular las prioridades de todos los elementos excepto el elemento cuyo id ha sido pasado en el evento y ... if(m_lparam!=m_wnd[m_active_window_index].m_elements[e].Id()) { //--- ... excepto los menús contextuales if(m_wnd[m_active_window_index].m_elements[e].ClassName()=="CMenuItem" || m_wnd[m_active_window_index].m_elements[e].ClassName()=="CContextMenu") continue; //--- m_wnd[m_active_window_index].m_elements[e].ResetZorders(); } } //--- return(true); }
Si ha llegado el mensaje con el identificador del evento ON_SET_PRIORITIES, simplemente hay que recuperar las prioridades del clic izquierdo para todos los elementos de la ventana activa.
//+------------------------------------------------------------------+ //| Evento ON_SET_PRIORITIES | //+------------------------------------------------------------------+ bool CWndEvents::OnSetPriorities(void) { //--- Si hay señal para recuperar las prioridades para el clic izquierdo del ratón if(m_id!=CHARTEVENT_CUSTOM+ON_SET_PRIORITIES) return(false); //--- int elements_total=CWndContainer::ElementsTotal(m_active_window_index); for(int e=0; e<elements_total; e++) m_wnd[m_active_window_index].m_elements[e].SetZorders(); //--- return(true); }
Conclusión
En esta fase del desarrollo de la librería para la creación de las interfaces gráficas, su esquema tiene el siguiente aspecto
Fig. 2. Estructura de la librería en la fase actual del desarrollo.
Pues, aquí terminamos la cuarta parte de la serie sobre las interfaces gráficas. En el primer capítulo de esta parte, hemos implementado los elementos informativos, tales como la “Barra de estado” y la “Descripción emergente”. En el segundo capítulo, hemos analizado los temas sobre el modo de ventanas múltiples y el sistema de prioridades para el clic izquierdo del ratón.
Más abajo puede descargar el material de la primera parte de la serie para poder probar cómo funciona todo eso. Si le surgen algunas preguntas sobre el uso del material de estos archivos, puede dirigirse a la descripción detallada del proceso de desarrollo de la librería en uno de los artículos listados más abajo, o bien hacer su pregunta en los comentarios para el artículo.
Lista de artículos (capítulos) de la cuarta parte: