Interfaces gráficas II: Control "Menú principal" (Capítulo 4)

Anatoli Kazharski | 18 marzo, 2016

Índice



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”.

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.

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.

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.

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.

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: