Interfaces gráficas II: Control "Menú principal" (Capítulo 4)
Índice
- Introducción
- Desarrollando la clase para crear el menú principal
- Prueba de colocación del menú principal
- Bloqueo de los controles inactivos
- Métodos para la interacción con el menú principal
- Prueba final del menú principal
- 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 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.
Es el artículo final de la segunda parte de la serie sobre las interfaces gráficas. Aquí vamos a considerar la creación del control “Menú principal”. Se demostrará el proceso de su desarrollo y la configuración de los manejadores de las clases de la librería para una correcta reacción a las acciones del usuario. Además, hablaremos de los modos de conexión de los menús contextuales a los elementos del menú principal. A parte de eso, trataremos la cuestión del bloqueo de los controles que no forman parte de los controles activos en el momento actual.
Desarrollando la clase para crear el menú principal
En tres capítulos anteriores hemos desarrollado las clases para la creación de todos los controles que se utilizan para construir el menú principal del programa. Pues, tenemos las siguientes clases:
- CMenuItem – elemento del menú.
- CSeparateLine – línea separadora.
- CContextMenu – menú contextual.
En el directorio que contiene los archivos de todos los controles, vamos a crear el archivo MenuBar.mqh en la carpeta Controls. En este archivo vamos a incluir el archivo con la clase base, el archivo con la clase del formulario y los archivos de todos los controles integrantes de los que se compone:
//+------------------------------------------------------------------+ //| MenuBar.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "Element.mqh" #include "Window.mqh" #include "MenuItem.mqh" #include "ContextMenu.mqh"
El fondo y los elementos del menú son los objetos básicos del menú principal. Mientras que los menús contextuales van a adjuntarse a los elementos del menú principal a través de los punteros.
Fig. 1. Objetos básicos del menú principal.
A continuación, se muestra el código del formulario inicial de la clase CMenuBar con las instancias de las clases necesarias, con los punteros y los métodos virtuales estándar para cada control:
//+------------------------------------------------------------------+ //| Clase para crear el menú principal | //+------------------------------------------------------------------+ class CMenuBar : public CElement { private: //--- Puntero al formulario al que está adjuntado el control CWindow *m_wnd; //--- Objetos para crear el elemento del menú CRectLabel m_area; CMenuItem m_items[]; //--- Array de punteros a los menús contextuales CContextMenu *m_contextmenus[]; //--- public: CMenuBar(void); ~CMenuBar(void); //--- Guarda el puntero del formulario void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } //--- Manejador de eventos del gráfico virtual void OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam); //--- Desplazamiento del control virtual void Moving(const int x,const int y); //--- (1) Mostrar, (2) ocultar, (3) resetear, (4) eliminar virtual void Show(void); virtual void Hide(void); virtual void Reset(void); virtual void Delete(void); //--- (1) Establecer, (2) poner a cero las prioridades para el clic izquierdo del ratón virtual void SetZorders(void); virtual void ResetZorders(void); //--- };
Igual que en el caso de cualquier otro control de la interfaz, es necesario tener la posibilidad de configurar la apariencia del menú principal. Para eso vamos a crear los campos y métodos especiales que permiten establecer:
- propiedades del fondo del menú principal;
- propiedades comunes de los elementos del menú.
Además, vamos a necesitar los métodos para determinar y obtener el estado actual del menú principal. Hay que inicializar todos los campos dentro del constructor usando los valores predefinidos. Por defecto, el alto del fondo del menú principal será igual a 22 píxeles. El alto de los elementos del menú va a calcularse automáticamente respecto al alto del fondo del menú principal. Pero si hace falta, Usted puede cambiar este valor antes de colocar el control sobre el gráfico usando el método de la clase base CElement::YSize(). El ancho del fondo del menú principal suele ser igual al ancho del formulario al que se adjunta. Por eso al colocar el fondo sobre el gráfico, el cálculo del valor de este parámetro también se realiza automáticamente.
class CMenuBar : public CElement { private: //--- Propiedades del fondo int m_area_zorder; color m_area_color; color m_area_color_hover; color m_area_border_color; //--- Propiedades comunes de los elementos del menú int m_item_y_size; color m_item_color; color m_item_color_hover; color m_item_border_color; int m_label_x_gap; int m_label_y_gap; color m_label_color; color m_label_color_hover; //--- Estado del menú principal bool m_menubar_state; //--- public: //--- Color del (1) fondo y (2) marco del fondo del menú principal void MenuBackColor(const color clr) { m_area_color=clr; } void MenuBorderColor(const color clr) { m_area_border_color=clr; } //--- (1) color del fondo, (2) color del fondo al situar el cursor y (3) color del marco de los elementos del menú principal void ItemBackColor(const color clr) { m_item_color=clr; } void ItemBackColorHover(const color clr) { m_item_color_hover=clr; } void ItemBorderColor(const color clr) { m_item_border_color=clr; } //--- Sangrías de la etiqueta de texto desde el punto extremo del fondo del elemento void LabelXGap(const int x_gap) { m_label_x_gap=x_gap; } void LabelYGap(const int y_gap) { m_label_y_gap=y_gap; } //--- Color del texto (1) normal y (2) en el foco void LabelColor(const color clr) { m_label_color=clr; } void LabelColorHover(const color clr) { m_label_color_hover=clr; } //--- Estado del menú principal void State(const bool state); bool State(void) const { return(m_menubar_state); } //--- }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CMenuBar::CMenuBar(void) : m_menubar_state(false), m_area_zorder(0), m_area_color(C'240,240,240'), m_area_border_color(clrSilver), m_item_color(C'240,240,240'), m_item_color_hover(C'51,153,255'), m_item_border_color(C'240,240,240'), m_label_x_gap(15), m_label_y_gap(3), m_label_color(clrBlack), m_label_color_hover(clrWhite) { //--- Guardamos el nombre de la clase del control en la clase base CElement::ClassName(CLASS_NAME); //--- Alto del menú principal por defecto m_y_size=22; } //+------------------------------------------------------------------+ //| Determinar el estado del menú principal | //+------------------------------------------------------------------+ void CMenuBar::State(const bool state) { if(state) m_menubar_state=true; else { m_menubar_state=false; //--- Recorrer todos los elementos del menú principal para // determinar el estatus de los menús contextuales desactivados int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) m_items[i].ContextMenuState(false); //--- Desbloquear el formulario m_wnd.IsLocked(false); } }
Vamos a necesitar los arrays para establecer las propiedades únicas para cada elemento. A las propiedades únicas les pertenecen:
- ancho del elemento;
- texto a mostrar.
Estas propiedades van a establecerse antes de la colocación del menú principal sobre el gráfico, es decir durante su formación en el momento de adición de cada elemento. Para eso vamos a utilizar el método CMenuBar::AddItem(), parecido al que ha sido creado antes en la clase CContextMenu. La diferencia entre ellos consiste sólo en los parámetros pasados (establecidos).
Para vincular los menús contextuales a cada elemento del menú principal, vamos a crear el método CMenuBar::AddContextMenuPointer(). En él hay que pasar el índice del elemento del menú principal y el objeto del menú contextual cuyo puntero va a guardarse en el array m_contextmenus[].
class CMenuBar : public CElement { private: //--- Arrays de las propiedades únicas de los elementos del menú: // (1) Ancho, (2) texto int m_width[]; string m_label_text[]; //--- public: //--- Añade un elemento del menú con propiedades especificadas antes de la creación del menú principal void AddItem(const int width,const string text); //--- Adjunta el menú contextual pasado al elemento especificado del menú principal void AddContextMenuPointer(const int index,CContextMenu &object); //--- }; //+------------------------------------------------------------------+ //| Añade un elemento del menú | //+------------------------------------------------------------------+ void CMenuBar::AddItem(const int width,const string text) { //--- Aumentamos el tamaño del array a un control int array_size=::ArraySize(m_items); ::ArrayResize(m_items,array_size+1); ::ArrayResize(m_contextmenus,array_size+1); ::ArrayResize(m_width,array_size+1); ::ArrayResize(m_label_text,array_size+1); //--- Guardamos los valores de parámetros pasados m_width[array_size] =width; m_label_text[array_size] =text; } //+------------------------------------------------------------------+ //| Añade el puntero del menú contextual | //+------------------------------------------------------------------+ void CMenuBar::AddContextMenuPointer(const int index,CContextMenu &object) { //--- Comprobar la superación del rango int size=::ArraySize(m_contextmenus); if(size<1 || index<0 || index>=size) return; //--- Guardar el puntero m_contextmenus[index]=::GetPointer(object); }
También vamos a necesitar los métodos para obtener el puntero del elemento del menú principal y el puntero de uno de los menús contextuales adjuntos. En cada uno de estos métodos va a realizarse la comprobación del tamaño del array y la corrección del índice en caso de salir fuera del rango del array. Aparte de eso, el repaso cíclico de los elementos del menú principal va a utilizarse con mucha frecuencia para unas u otras tareas, por eso necesitamos los métodos para obtener los tamaños de los arrays.
class CMenuBar : public CElement { public: //--- (1) Obtener el puntero del elemento del menú especificado, (2) obtener el puntero del menú contextual especificado CMenuItem *ItemPointerByIndex(const int index); CContextMenu *ContextMenuPointerByIndex(const int index); //--- Número de (1) elementos y (2) menús contextuales int ItemsTotal(void) const { return(::ArraySize(m_items)); } int ContextMenusTotal(void) const { return(::ArraySize(m_contextmenus)); } //--- }; //+------------------------------------------------------------------+ //| Devuelve el puntero del elemento del menú según el índice | //+------------------------------------------------------------------+ CMenuItem *CMenuBar::ItemPointerByIndex(const int index) { int array_size=::ArraySize(m_items); //--- Si el menú principal no tiene elementos, avisar sobre ello if(array_size<1) { ::Print(__FUNCTION__," > ¡La llamada a este método debe realizarse " " cuando en el menú principal hay por lo menos un elemento!"); } //--- Corrección en caso de superar el rango int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index; //--- Devolver el puntero return(::GetPointer(m_items[i])); } //+------------------------------------------------------------------+ //| Devuelve el puntero del menú contextual según el índice | //+------------------------------------------------------------------+ CContextMenu *CMenuBar::ContextMenuPointerByIndex(const int index) { int array_size=::ArraySize(m_contextmenus); //--- Si el menú principal no tiene elementos, avisar sobre ello if(array_size<1) { ::Print(__FUNCTION__," > ¡La llamada a este método debe realizarse " " cuando en el menú principal hay por lo menos un elemento!"); } //--- Corrección en caso de superar el rango int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index; //--- Devolver el puntero return(::GetPointer(m_contextmenus[i])); }
El proceso de creación del menú principal no tiene ningunas diferencias particulares de la creación del menú contextual en la clase CContextMenu. Su implementación es incluso un poco más simple ya que el menú principal no necesita el puntero al nodo anterior. Tampoco tiene las líneas separadoras. Por esa razón no vamos a mostrar aquí el código de estos métodos para ahorrar el espacio del artículo. Se puede verlos en el archivo adjunto MenuBar.mqh.
Antes, para que los elementos del menú contextual entren en la base de todos los controles en la clase CWndContainer, ha sido escrito el código especial AddContextMenuElements() que se invoca en el método principal de la adición de los controles a la base CWndContainer::AddToElementsArray(). Hay que escribir el mismo método para el control “menú principal”, de lo contrario los elementos del menú no van a desplazarse junto con el formulario y no van a cambiar su color al apuntarlos con el cursor.
A continuación se muestra la lista de acciones cuando es necesario que los controles integrantes de algún control principal también entren en la base, y el puntero del control principal entre en el array personal:
- Incluir el archivo con la clase del control en el archivo WndContainer.mqh.
- Incluir el array del control en la estructura WindowElements.
- Añadir el método para obtener el número de menús principales en el array personal.
- Declarar e implementar el método privado para guardar los punteros a los controles que forman parte de otro control (principal).
- Colocar la llamada del método nuevo en el método común que sirve para el uso en la clase de la aplicación como adición del puntero del control principal a la base.
Abajo se muestra el código reducido del archivo WndContainer.mqh en el que se muestra sólo lo que es necesario añadir en él:
//+------------------------------------------------------------------+ //| WndContainer.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "MenuBar.mqh" //+------------------------------------------------------------------+ //| Clase para almacenar todos los objetos de la interfaz | //+------------------------------------------------------------------+ class CWndContainer { protected: //--- Estructura de los arrays de controles struct WindowElements { //Array de los menús principales CMenuBar *m_menu_bars[]; }; //--- public: //Cantidad de los menús principales int MenuBarsTotal(const int window_index); //--- private: //--- Guarda los punteros a los controles del menú principal en la base bool AddMenuBarElements(const int window_index,CElement &object); }; //+------------------------------------------------------------------+ //| Devuelve el número de menús principales según el índice especificado de la ventana | //+------------------------------------------------------------------+ int CWndContainer::MenuBarsTotal(const int window_index) { if(window_index>=::ArraySize(m_wnd)) { ::Print(PREVENTING_OUT_OF_RANGE); return(WRONG_VALUE); } //--- return(::ArraySize(m_wnd[window_index].m_menu_bars)); } //+------------------------------------------------------------------+ //| Añade el puntero al array de controles | //+------------------------------------------------------------------+ void CWndContainer::AddToElementsArray(const int window_index,CElement &object) { //--- Si en la base no hay formularios para los controles //--- Si se solicita el formulario no existente //--- Añadimos al array común de controles //--- Añadir los objetos del control al array común de objetos //--- Recordamos id del último control en todos los formularios //--- Aumentamos el contador de los identificadores de controles //--- Guarda los punteros a los objetos del menú contextual en la base //--- Guarda los punteros a los objetos del menú principal en la base if(AddMenuBarElements(window_index,object)) return; } //+------------------------------------------------------------------+ //| Guarda los punteros a los objetos del menú principal en la base | //+------------------------------------------------------------------+ bool CWndContainer::AddMenuBarElements(const int window_index,CElement &object) { //--- Salimos si no es menú principal if(object.ClassName()!="CMenuBar") return(false); //--- Obtenemos el puntero al menú principal CMenuBar *mb=::GetPointer(object); //--- Guardamos los punteros a sus objetos en la base int items_total=mb.ItemsTotal(); for(int i=0; i<items_total; i++) { //--- Aumentar el array de controles int size=::ArraySize(m_wnd[window_index].m_elements); ::ArrayResize(m_wnd[window_index].m_elements,size+1); //--- Obtener el puntero al elemento del menú CMenuItem *mi=mb.ItemPointerByIndex(i); //--- Guardamos el puntero en el array m_wnd[window_index].m_elements[size]=mi; //--- Añadimos los punteros a todos los objetos del elemento del menú al array común AddToObjectsArray(window_index,mi); } //--- Añadimos el puntero al array personal AddToRefArray(mb,m_wnd[window_index].m_menu_bars); return(true); }
Prueba de colocación del menú principal
En esta fase ya se puede probar la colocación del menú principal. Vamos a crear este control de tres elementos. Establecemos el ancho y el texto a mostrar para cada uno de ellos. Las demás propiedades serán predefinidas.
Inserte el código para la creación del menú principal en la clase de la aplicación CProgram, como se muestra a continuación. Corrija las coordenadas del resto de controles que han sido añadidos al formulario antes de eso. Y finalmente, coloque la llamada al método nuevo CProgram::CreateMenuBar() en el método principal de la creación de la interfaz gráfica.
class CProgram : public CWndEvents { private: //--- Menú principal CMenuBar m_menubar; //--- private: //--- #define MENUBAR_GAP_X (1) #define MENUBAR_GAP_Y (20) bool CreateMenuBar(void); //--- #define MENU_ITEM1_GAP_X (6) #define MENU_ITEM1_GAP_Y (45) //--- #define SEP_LINE_GAP_X (6) #define SEP_LINE_GAP_Y (75) //--- }; //+------------------------------------------------------------------+ //| Crea el panel de trading | //+------------------------------------------------------------------+ bool CProgram::CreateTradePanel(void) { //--- Creación del formulario para los controles //--- Creación de controles: // Menú principal if(!CreateMenuBar()) return(false); //--- Elemento del menú //--- Línea separadora //--- Redibujar el gráfico return(true); } //+------------------------------------------------------------------+ //| Crea el menú principal | //+------------------------------------------------------------------+ bool CProgram::CreateMenuBar(void) { //--- Tres elementos en el menú principal #define MENUBAR_TOTAL 3 //--- Guardamos el puntero a la ventana m_menubar.WindowPointer(m_window); //--- Coordenadas int x=m_window.X()+MENUBAR_GAP_X; int y=m_window.Y()+MENUBAR_GAP_Y; //--- Arrays con propiedades únicos de los elementos int width[MENUBAR_TOTAL] ={50,55,53}; string text[MENUBAR_TOTAL] ={"File","View","Help"}; //--- Añadir elementos al menú principal for(int i=0; i<MENUBAR_TOTAL; i++) m_menubar.AddItem(width[i],text[i]); //--- Creamos el control if(!m_menubar.CreateMenuBar(m_chart_id,m_subwin,x,y)) return(false); //--- Añadimos el objeto al array común de los grupos de objetos CWndContainer::AddToElementsArray(0,m_menubar); return(true); }
Compile los archivos e inicie el EA en el gráfico. El resultado tiene que ser el mismo que se muestra en la captura de pantalla de abajo. El menú principal va a desplazarse junto con el formulario y sus elementos van a cambiar su color al apuntarlos con el cursor.
Fig. 2. Prueba del control “Menú principal”.
Bloqueo de los controles inactivos
Antes de crear los menús contextuales y adjuntarlos a los elementos del menú principal, hay que integrar una funcionalidad en la librería que va a bloquear el formulario y otros controles cuando algún control está activado. ¿Para qué es necesario hacerlo? Aquí bajo los controles activados se entienden aquellos que se hacen visibles (se invocan) a través de otros controles y se ocultan cuando ya no son necesarios. Por ejemplo, pueden ser las listas desplegables, menús contextuales, calendarios, etc. Intente activar algún menú contextual o lista desplegable en el terminal comercial MetaTrader. Nótese: cuando el control de este tipo está activado, todos los demás controles del terminal se hacen no disponibles. Eso se expresa, por ejemplo, en que no cambian su color cuando situamos el cursor sobre ellos. El objetivo de este bloqueo consiste en evitar la situación cuando un control que en este momento está tapado temporalmente con la lista desplegable reaccione al cursor.
Para implementar el mismo comportamiento, sólo hay que bloquear el formulario al que pertenece el control activado. Cualquier otro control de este formulario tiene el acceso a él mediante el puntero, por eso en cualquier momento se puede averiguar en qué estado se encuentra el formulario en este momento. Es muy fácil: si el formulario esta bloqueado, no hace falta llamar al método del cambio del color del control.
A la clase de la creación del formulario CWindow se añade un campo especial y los métodos para determinar y obtener el estado (bloqueado/desbloqueado) del formulario (véase el código). En el constructor, el campo m_is_locked tiene que inicializarse con el valor false: es decir, por defecto el formulario debe estar desbloqueado. Aquí mismo se puede añadir la condición según la cual el formulario y sus controles tienen que cambiar de color sólo cuando éste no está bloqueado.
class CWindow : public CElement { private: //--- Estatus de la ventana bloqueada bool m_is_locked; //--- public: //--- Estatus de la ventana bloqueada bool IsLocked(void) const { return(m_is_locked); } void IsLocked(const bool flag) { m_is_locked=flag; } //--- }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CWindow::CWindow(void) : m_is_locked(false) { } //+------------------------------------------------------------------+ //| Temporizador | //+------------------------------------------------------------------+ void CWindow::OnEventTimer(void) { //--- Si la ventana no está bloqueada if(!m_is_locked) { //--- Cambio del color de los objetos del formulario ChangeObjectsColor(); } }
En cuanto a los demás controles, en este momento tenemos sólo un control que puede ser pulsado -el elemento del menú con el que se puede probar esta funcionalidad. El cambio del color del elemento al ser apuntado con el ratón va a depender también del estado del formulario al que está adjuntado. Por eso hay que colocar la misma comprobación dentro de su clase CMenuItem, como se muestra abajo:
//+------------------------------------------------------------------+ //| Temporizador | //+------------------------------------------------------------------+ void CMenuItem::OnEventTimer(void) { //--- Si la ventana está disponible if(!m_wnd.IsLocked()) { //--- Si el estatus del menú contextual desactivado if(!m_context_menu_state) //--- Cambio del color de los objetos del formulario ChangeObjectsColor(); } }
El bloque del formulario debe realizarse cuando el menú contextual se hace visible. O sea, en el método CContextMenu::Show().
//+------------------------------------------------------------------+ //| Muestra el menú contextual | //+------------------------------------------------------------------+ void CContextMenu::Show(void) { //--- Salir si el control ya es 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 m_wnd.IsLocked(true); }
A primera vista parece que para el desbloqueo sería suficiente llamar el método CWindow::IsLocked() en el método CContextMenu::Hide(). Pero esta opción no conviene porque varios menús contextuales pueden estar abiertos al mismo tiempo. No todos ellos se cierran simultáneamente. Vamos a recordar cuándo los menús contextuales abiertos se cierran a la vez. Para eso deben cumplirse ciertas condiciones. Por ejemplo, en el método CContextMenu::CheckHideContextMenus(), cuando después de comprobar todas las condiciones se envía la señal para el cierre de todos los menús contextuales. El segundo caso: en el método CContextMenu::ReceiveMessageFromMenuItem(), cuando se procesa el evento ON_CLICK_MENU_ITEM.
Vamos a añadir el desbloqueo del formulario en estos métodos. Abajo se muestran las versiones reducidas de los métodos, pero los comentarios ayudan a comprender dónde hay que insertar el código marcado en amarillo.
//+------------------------------------------------------------------+ //| Comprobación de las condiciones para cerrar todos los menús contextuales | //+------------------------------------------------------------------+ void CContextMenu::CheckHideContextMenus(void) { //--- Salir si el cursor se encuentra dentro del área del menú contextual o en el área del nodo anterior //--- Si el cursor se encuentra fuera del área de estos controles, ... // ... hay que comprobar si hay menús contextuales abiertos que han sido activados después de éste //--- Para eso recorremos en el ciclo la lista de este menú contextual ... // ... para detectar la presencia del elemento que contiene un menú contextual //--- Desbloquear el formulario m_wnd.IsLocked(false); //--- Enviar la señal para cerrar todos los menús contextuales } //+------------------------------------------------------------------+ //| Recepción del mensaje del elemento del menú para el procesamiento | //+------------------------------------------------------------------+ void CContextMenu::ReceiveMessageFromMenuItem(const int id_item,const int index_item,const string message_item) { //--- Si hay indicio del mensaje de este programa y id del control coincide //--- Ocultar el menú contextual //--- Desbloquear el formulario m_wnd.IsLocked(false); }
Si en esta fase compila los archivos y inicia el EA probado anteriormente, inmediatamente después de abrir el menú contextual verá que todo funciona un poco diferente de lo esperado. Absolutamente todos los elementos del menú estarán bloqueados en cuanto al cambio del color, incluso los que se encuentran en el menú contextual activado. Desde luego, eso no puede ser así. Para estos casos, los menús contextuales deben de tener su propio método del cambio del color de sus elementos del menú. Vamos a crear este método en la clase CContextMenu y colocar su llamada en el temporizador, como se muestra en el código de abajo.
class CContextMenu : public CElement { public: //--- Cambia el color de los elementos del menú al situar el cursor sobre ellos void ChangeObjectsColor(void); }; //+------------------------------------------------------------------+ //| Temporizador | //+------------------------------------------------------------------+ void CContextMenu::OnEventTimer(void) { //--- Cambio del color de los elementos del menú al situar el cursor sobre ellos ChangeObjectsColor(); } //+------------------------------------------------------------------+ //| Cambio del color del objeto al situar el cursor sobre él | //+------------------------------------------------------------------+ void CContextMenu::ChangeObjectsColor(void) { //--- Salir si el menú contextual está desactivado if(!m_context_menu_state) return; //--- Recorrer todos los elementos del menú int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) { //--- Cambiar el color del elemento del menú m_items[i].ChangeObjectsColor(); } }
Ahora todo va a funcionar tal como se ha planteado:
Fig. 3. Prueba del bloqueo del formulario y todos los controles, excepto el que está activado en este momento.
Métodos para la interacción con el menú principal
Seguiremos desarrollando la clase CMenuBar para crear el menú principal. La parte restante se refiere a la gestión de este control. Vamos a estudiar con más detalles cómo funciona el menú principal con el ejemplo de otros programas. Después del inicio del programa, el menú principal queda desactivado por defecto. En este estado, al situar el cursor sobre los elementos del menú principal, ellos simplemente se resaltan con el color. Al hacer clic en uno de los elementos, el menú principal se activa. Se abre el menú contextual del elemento pulsado. En este estado del menú principal, cuando desplazamos el cursor a lo largo de sus elementos, los menús contextuales van a cambiarse automáticamente dependiendo del elemento sobre el que se encuentra el cursor en este momento.
Antes de implementar este comportamiento en nuestra librería, vamos a crear tres menús contextuales y adjuntarlos a los elementos del menú principal. Para ahorrar el espacio del artículo, aquí vamos a mostrar la versión reducida sólo de uno de ellos. Pero Usted puede ver la versión hecha de los archivos del EA al final del artículo.
El código de abajo contiene sólo las líneas importantes. Fíjese cómo al menú contextual se le pasa el puntero al nodo anterior, y cómo se guarda el puntero al menú contextual en el menú principal. Cuando se establecen las propiedades de los menús contextuales, para el menú principal hay que establecer el modo del cálculo de las coordenadas desde la parte inferior del elemento. Por lo demás, la creación del menú contextual no se diferencia en nada de lo que hemos hablado antes.
//+------------------------------------------------------------------+ //| Crea el menú contextual | //+------------------------------------------------------------------+ bool CProgram::CreateMBContextMenu1(void) { //--- Tres elementos en el menú contextual //--- Guardamos el puntero a la ventana m_mb_contextmenu1.WindowPointer(m_window); //--- Guardamos el puntero al nodo anterior m_mb_contextmenu1.PrevNodePointer(m_menubar.ItemPointerByIndex(0)); //--- Adjuntar el menú contextual al elemento especificado del menú m_menubar.AddContextMenuPointer(0,m_mb_contextmenu1); //--- Arrays de los nombres de los elementos //--- Array de los iconos para el modo disponible //--- Array de los iconos para el modo bloqueado //--- Arrays de los tipos de los elementos del menú //--- Establecemos las propiedades antes de la creación m_mb_contextmenu1.FixSide(FIX_BOTTOM); //--- Añadimos los elementos en el menú contextual //--- Línea separadora tras el segundo elemento //--- Desactivar el segundo elemento //--- Crear el menú contextual if(!m_mb_contextmenu1.CreateContextMenu(m_chart_id,m_subwin)) return(false); //--- Añadimos el objeto al array común de los grupos de objetos CWndContainer::AddToElementsArray(0,m_mb_contextmenu1); return(true); }
Luego configuramos los manejadores de eventos en la clase del menú principal СMenuBar. Empezamos con el procesamiento del clic en los elementos del menú. Para eso, en las clases CMenuItem y CContextMenu, vamos a necesitar el método OnClickMenuItem(), así como los métodos especiales para la extracción del índice e identificador del elemento del menú desde el nombre del objeto pulsaddo.
Los métodos para la identificación son los mismos como en la clase CContextMenu. Sin embargo, para la clase СMenuBar el procesamiento del clic en el elemento del menú tiene sus particularidades. Después de la verificación del identificador, se realiza la comprobación de la corrección del puntero al menú contextual según el índice obtenido desde el nombre del objeto. Si no hay puntero, simplemente enviamos la señal para el cierre de todos los menús contextuales abiertos. Si hay puntero, entonces la señal para el cierre de todos los menús contextuales se envía sólo si este clic en el elemento ha sido hecho para cerrar el menú contextual actual.
class CMenuBar : public CElement { private: //--- Procesamiento del clic en un elemento del menú bool OnClickMenuItem(const string clicked_object); //--- Obtener el (1) identificador e (2) índice desde el nombre del elemento del menú int IdFromObjectName(const string object_name); int IndexFromObjectName(const string object_name); //--- }; //+------------------------------------------------------------------+ //| Manejador de eventos | //+------------------------------------------------------------------+ void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Procesamiento del evento del clic izquierdo en el elemento del menú principal if(id==CHARTEVENT_OBJECT_CLICK) { if(OnClickMenuItem(sparam)) return; } } //+------------------------------------------------------------------+ //| Clic en el elemento del menú principal | //+------------------------------------------------------------------+ bool CMenuBar::OnClickMenuItem(const string clicked_object) { //--- Salimos si el clic ha sido hecho fuera del elemento del menú if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",0)<0) return(false); //--- Obtenemos el identificador e índice desde el nombre del objeto int id =IdFromObjectName(clicked_object); int index =IndexFromObjectName(clicked_object); //--- Salir si el identificador no coincide if(id!=CElement::Id()) return(false); //--- Si hay puntero al menú contextual if(CheckPointer(m_contextmenus[index])!=POINTER_INVALID) { //--- Estado del menú principal depende de la visibilidad del menú contextual m_menubar_state=(m_contextmenus[index].ContextMenuState())? false : true; //--- Determinar el estado del formulario m_wnd.IsLocked(m_menubar_state); //--- Si el menú principal está desactivado if(!m_menubar_state) //--- Enviar la señal para ocultar todos los menús contextuales ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,""); } //--- Si no hay puntero al menú contextual else { //--- Enviar la señal para cerrar todos los menús contextuales ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,""); } //--- return(true); }
Recordamos que el procesamiento del evento ON_HIDE_CONTEXTMENUS se realiza en la clase CWndEvents. Hay que añadir al método CWndEvents::OnHideContextMenus() un ciclo más, en el que van a desactivarse forzosamente todos los menús principales que se encuentran en la base.
//+------------------------------------------------------------------+ //| Evento ON_HIDE_CONTEXTMENUS | //+------------------------------------------------------------------+ bool CWndEvents::OnHideContextMenus(void) { //--- Si hay señal para cerrar todos los menús contextuales if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS) return(false); //--- Ocultar todos los menús contextuales int cm_total=CWndContainer::ContextMenusTotal(0); for(int i=0; i<cm_total; i++) m_wnd[0].m_context_menus[i].Hide(); //--- Desactivar los menús principales int menu_bars_total=CWndContainer::MenuBarsTotal(0); for(int i=0; i<menu_bars_total; i++) m_wnd[0].m_menu_bars[i].State(false); //--- return(true); }
Si ahora compilamos los archivos e iniciamos el EA en el gráfico, al pulsar en los elementos del menú principal, los menús contextuales van a abrirse, y si volvemos a pulsarlos, van a cerrarse.
Fig. 4. Prueba de la llamada de menús contextuales desde el menú principal.
Luego implementamos los métodos que permiten cambiar los menús contextuales con el desplazamiento del cursor del ratón cuando el menú principal está activado, como en las aplicaciones Windows. Vamos a necesitar el método para resaltar los elementos del menú principal cuando éste está activado, así como un método auxiliar para determinar el elemento activo (en el foco) del menú principal activado.
class CMenuBar : public CElement { public: //--- Cambia el color al apuntar con el cursor void ChangeObjectsColor(void); //--- private: //--- Devuelve el elemento activo del menú principal int ActiveItemIndex(void); //--- }; //+------------------------------------------------------------------+ //| Cambio del color del objeto al situar el cursor sobre él | //+------------------------------------------------------------------+ void CMenuBar::ChangeObjectsColor(void) { int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) m_items[i].ChangeObjectsColor(); } //+------------------------------------------------------------------+ //| Devuelve el índice del elemento activado del menú | //+------------------------------------------------------------------+ int CMenuBar::ActiveItemIndex(void) { int active_item_index=WRONG_VALUE; //--- int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) { //--- Si elemento está en el foco if(m_items[i].MouseFocus()) { //--- Recordamos el índice y paramos el ciclo active_item_index=i; break; } } //--- return(active_item_index); }
Aparte de eso, vamos a crear el método que va a conmutar los menús contextuales, al apuntarles con el cursor cuando el menú principal está activado. Esté método va a llamarse SwitchContextMenuByFocus(). Hay que pasarle el índice del elemento activado del menú principal que permite averiguar qué menú contextual se hace visible. Todos los demás menús contextuales se ocultan. Además, se realiza la verificación comprobando si están abiertos los menús contextuales que se abren desde el elemento del menú principal. Si resulta que es así, se genera el evento personalizado ON_HIDE_BACK_CONTEXTMENUS, que ha sido considerado al detalles en este artículo.
Tras el cierre del menú contextual, aquí también hay que resetear el color del elemento para evitar las situaciones cuando pueden estar resaltados dos elementos del menú principal al mismo tiempo.
class CMenuBar : public CElement { private: //--- Cambia los menús contextuales del menú principal al apuntar el cursor void SwitchContextMenuByFocus(const int active_item_index); //--- }; //+------------------------------------------------------------------+ //| Cambia los menús contextuales del menú principal al apuntar el cursor | //+------------------------------------------------------------------+ void CMenuBar::SwitchContextMenuByFocus(const int active_item_index) { int items_total=ItemsTotal(); for(int i=0; i<items_total; i++) { //--- Ir al siguiente si en este elemento no hay menú contextual if(::CheckPointer(m_contextmenus[i])==POINTER_INVALID) continue; //--- Si hemos llegado al elemento especificado, hacer que su menú contextual sea visible if(i==active_item_index) m_contextmenus[i].Show(); //--- Hay que ocultar los demás menús contextuales else { CContextMenu *cm=m_contextmenus[i]; //--- Ocultar los menús contextuales que están abiertos desde otros menús contextuales. // Recorremos en el ciclo los elementos del menú contextual actual para averiguar si éstos existen. int cm_items_total=cm.ItemsTotal(); for(int c=0; c<cm_items_total; c++) { CMenuItem *mi=cm.ItemPointerByIndex(c); //--- Ir al siguiente si el puntero al elemento no es correcto if(::CheckPointer(mi)==POINTER_INVALID) continue; //--- Ir al siguiente si este elemento no contiene un menú contextual if(mi.TypeMenuItem()!=MI_HAS_CONTEXT_MENU) continue; //--- Si el menú contextual existe y está activado if(mi.ContextMenuState()) { //--- Enviar la señal para cerrar todos los menús contextuales que están abiertos después de éste ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),0,""); break; } } //--- Ocultar el menú contextual del menú principal m_contextmenus[i].Hide(); //--- Resetear el color del elemento del menú m_items[i].ResetColors(); } } }
Ahora sólo queda añadir nuevos métodos creados al manejador de eventos de la clase CMenuBar de la comprobación del evento del desplazamiento del cursor CHARTEVENT_MOUSE_MOVE:
//+------------------------------------------------------------------+ //| Manejador de eventos | //+------------------------------------------------------------------+ void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { if(id==CHARTEVENT_MOUSE_MOVE) { //--- Salir si el menú principal no está activado if(!m_menubar_state) return; //--- Obtener el índice del elemento activado del menú principal int active_item_index=ActiveItemIndex(); if(active_item_index==WRONG_VALUE) return; //--- Cambiar el color si el foco ha cambiado ChangeObjectsColor(); //--- Cambiar el menú contextual según el elemento activado del menú principal SwitchContextMenuByFocus(active_item_index); return; } }
Prueba final del menú principal
Ahora se puede probar todo lo que hemos hecho en este artículo. Vamos a insertar en el formulario unos elementos independientes del menú. Al menú principal le añadimos otro menú contextual interno, y lo adjuntamos al segundo elemento del tercer menú contextual (véase la captura de pantalla).
Compile los archivos e inicie el EA en el gráfico. Para obtener el mismo resultado que se muestra en la captura de pantalla, puede descargar los archivos adjuntos al final del artículo.
Fig. 5. Prueba final del menú principal.
En los archivos adjuntos también hay indicador para las pruebas con la misma interfaz gráfica que tiene el EA en la captura de pantalla de arriba. Además, hay versiones para las pruebas en la plataforma comercial MetaTrader 4.
Conclusión
Con eso terminamos la segunda parte de la serie. Ha salido bastante amplia, pero hemos considerado prácticamente todos los componentes principales de la librería para el desarrollo de interfaces gráficas. En este momento se puede representar la estructura de la librería tal como se muestra en el esquema de abajo. En el último capítulo de la primera parte se puede encontrar la descripción detallada de este esquema.
Fig. 6. Estructura de la librería en la fase actual del desarrollo.
Si han llegado a este momento, puedo alegrarles: lo más complicado se ha quedado atrás. En la primera y la segunda parte de esta serie hemos considerado los temas más complicados para la comprensión respecto al desarrollo de las interfaces gráficas. En los siguientes artículos hablaremos de la creación de varios controles, el material no estará sobrecargado con diferentes clases, y la complejidad de los artículos será mucho menor.
Más abajo se puede descargar los archivos comprimidos con los ficheros de la librería en esta fase del desarrollo, imágenes y ficheros de los programas examinados en el artículo (EA, indicadores y script) para realizar las pruebas en los terminales MetaTrader 4 y MetaTrader 5. 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 segunda parte:
- Interfaces gráficas II: Control “Elemento del menú” (Capítulo 2)
- Interfaces gráficas II: Controles “Línea separadora” y “Menú contextual” (Capítulo 2)
- Interfaces gráficas II: Configuración de los manejadores de eventos de la librería (Capítulo 3)
- Interfaces gráficas II: Control “Menú principal” (Capítulo 4)
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/2207
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso