Interfaces gráficas III: Botones simples y multifuncionales (Capítulo 1)

Anatoli Kazharski | 26 abril, 2016

Índice



Introducción

Antes, en dos partes anteriores de la serie, hemos considerado varios temas relacionados con el desarrollo de la estructura de la librería para la creación de interfaces gráficas, así como las técnicas principales que se utilizan para controlar los objetos.

En la segunda parte de esta serie se ha mostrado un ejemplo detallado de cómo crear un control y vincularlo con el motor de la librería. Este ejemplo era bastante complicado: hay que reconocer que el menú principal y los menús contextuales pertenecen a los controles más complejos.

El presente artículo va a ser considerablemente más simple en comparación con los artículos anteriores. Aquí vamos a considerar el control que se llama “Botón”.

El botón es el control más sencillo de la interfaz gráfica con el que puede interactuar el usuario. Sin embargo, aquí se puede ofrecer varias opciones de su implementación. En este artículo vamos a crear tres clases para la creación de los botones de diferente grado de complejidad (véase la lista de abajo).

  • Botón simple (simple button). Clase CSimpleButton.
  • Botón con imagen (icon button). Clase CIconButton.
  • Botón de división con varias funciones (split button). Clase CSplitButton.

Además de eso, implementaremos otras tres clases más que nos permitirán crear los grupos de botones interconectados:

  • Grupo de botones simples. Clase CButtonsGroup.
  • Grupo de botones con imágenes. Clase CIconButtonsGroup.
  • Grupo de botones de opción (radio buttons). Clase CRadioButtons.

Aparte de eso, vamos a introducir adiciones para ampliar la funcionalidad del menú contextual con otro modo más. Además, añadiremos a la clase del formulario (CWindow) un campo con el método que permitirá definir unívocamente qué control ha bloqueado el formulario en el momento de su activación. Eso permitirá crear la condición cuando el formulario puede desbloquearse sólo por el control que la ha bloqueado.

No vamos a detenernos en los métodos que son propios a todos los controles, porque muchos de ellos ya han sido analizados al detalle en los artículos anteriores. Dichos métodos van a mostrarse en los listados del código del artículo sólo como las declaraciones en el cuerpo de la clase.

 


Desarrollando la clase para crear un botón simple

Empezamos con el botón simple. Antes, en el archivo Objects.mqh hemos preparado la clase para la creación de la primitiva del control tipo CButton. CChartObjectButton es su clase base que puede usarse para crear el objeto gráfico tipo OBJ_BUTTON. En las propiedades de este objeto ya están previstos dos estados: pulsado (on) y suelto (off). Su representación gráfica también tiene dos opciones diferentes, dependiendo de que si la visualización del marco del objeto está activado. En ambos modos, el color del botón pulsado se pone un poco más oscuro que en el estado suelto.

Se puede adjuntar este objeto al gráfico manualmente desde el menú principal: Insertar -> Objetos -> Objetos Gráficos -> Botón. Además, se puede cambiar los parámetros manualmente desde la ventana de los ajustes del objeto gráfico:


Fig. 1. Ventana de los ajustes del objeto gráfico “Botón”.

 

Luego, en la carpeta Controls, que ahora contiene otros archivos con las clases de los controles de nuestra librería, se crea el archivo SimpleButton.mqh, y dentro de él se crea la clase CSimpleButton con los campos y métodos estándar para todos los controles, que ya han sido estudiados detalladamente en los artículos anteriores.

//+------------------------------------------------------------------+
//|                                                 SimpleButton.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+

//| Clase para crear un botón simple                                |

//------------------------------------------------------------------+
class CSimpleButton : public CElement
  {
private:
   //--- Puntero al formulario al que está adjuntado el control
   CWindow          *m_wnd;
   //---
public:
                     CSimpleButton(void);
                    ~CSimpleButton(void);
   //--- Guarda el puntero del formulario
   void              WindowPointer(CWindow &object)          { m_wnd=::GetPointer(object);     }
   //---
public:
   //--- Manejador del evento del gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Temporizador
   virtual void      OnEventTimer(void);
   //--- 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);
  };

Vamos a aclarar qué propiedades podremos establecer para el botón antes de su creación. Son las siguientes:

  • Tamaño.
  • Colores del fondo para diferentes estados y en función de la posición del cursor del ratón.
  • Colores del marco para diferentes estados.
  • Colores del texto.

A veces, se necesita el botón que vuelve a pasar automáticamente al estado inicial (suelto) después de ser pulsado. Pero hay situaciones cuando, después de la pulsación, el botón debe quedarse en el estado pulsado si antes estaba suelto, y soltarse si antes estaba pulsado. Vamos a hacer así que se pueda trabajar con el botón en dos modos a elección del usuario. También vamos a necesitar el método IsPressed() para identificar si ahora el botón está pulsado o suelto.

Además de eso, hace falta la posibilidad de bloquear/desbloquear el botón si el autor de la aplicación necesita usar esta opción. Por ejemplo, el botón se queda bloqueado si en este momento no se cumplen las condiciones del uso de la función incorporada en el botón. En cuanto las condiciones se cumplan, el botón queda disponible. Estos ejemplos serán considerados más tarde en este artículo.

Vamos a añadir los métodos para la creación del botón a la clase. No tienen nada nuevo de lo que hemos estudiado antes, por eso se puede ver el código de los métodos en los archivos adjuntos al artículo.

class CSimpleButton : public CElement
  {
private:
   //--- Objeto para crear el botón
  CButton           m_button;
   //--- Propiedades del botón
   //    (1) Texto, (2) tamaños
   string            m_button_text;
   int               m_button_x_size;
   int               m_button_y_size;
   //--- Color del fondo
   color             m_back_color;
   color             m_back_color_off;
   color             m_back_color_hover;
   color             m_back_color_pressed;
   color             m_back_color_array[];
   //--- Color del marco
   color             m_border_color;
   color             m_border_color_off;
   //--- Color del texto
   color             m_text_color;
   color             m_text_color_off;
   color             m_text_color_pressed;
   //--- Prioridad para el clic izquierdo del ratón
   int               m_button_zorder;
   //--- Modo de dos estados del botón
   bool              m_two_state;
   //--- Disponible/bloqueado
   bool              m_button_state;
   //---
public:
   //--- Métodos para crear un botón simple
   bool              CreateSimpleButton(const long chart_id,const int subwin,const string button_text,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   //---
public:
   //--- (1) establecer el modo del botón,
   //    (2) estado general del botón (disponible/bloqueado)
   void              TwoState(const bool flag)               { m_two_state=flag;               }
   bool              IsPressed(void)                   const { return(m_button.State());       }
   bool              ButtonState(void)                 const { return(m_button_state);         }
   void              ButtonState(const bool state);
   //--- Tamaño del botón
   void              ButtonXSize(const int x_size)           { m_button_x_size=x_size;         }
   void              ButtonYSize(const int y_size)           { m_button_y_size=y_size;         }
   //--- (1) Devuelve el texto del botón, (2) establecer el color del texto del botón
   string            Text(void)                        const { return(m_button.Description()); }
   void              TextColor(const color clr)              { m_text_color=clr;               }
   void              TextColorOff(const color clr)           { m_text_color_off=clr;           }
   void              TextColorPressed(const color clr)       { m_text_color_pressed=clr;       }
   //--- Establecemos el color del fondo del botón
   void              BackColor(const color clr)              { m_back_color=clr;               }
   void              BackColorOff(const color clr)           { m_back_color_off=clr;           }
   void              BackColorHover(const color clr)         { m_back_color_hover=clr;         }
   void              BackColorPressed(const color clr)       { m_back_color_pressed=clr;       }
   //--- Establecemos el color del marco del botón
   void              BorderColor(const color clr)            { m_border_color=clr;             }
   void              BorderColorOff(const color clr)         { m_border_color_off=clr;         }
   //---
  };
//+------------------------------------------------------------------+
//| Cambio del estado del botón                                       |
//+------------------------------------------------------------------+
void CSimpleButton::ButtonState(const bool state)
  {
   m_button_state=state;
   m_button.State(false);
   m_button.Color((state)? m_text_color : m_text_color_off);
   m_button.BackColor((state)? m_back_color : m_back_color_off);
   m_button.BorderColor((state)? m_border_color : m_border_color_off);
  }

A continuación, vamos a considerar la lógica del procesamiento de eventos al pulsar el botón. Hay que añadir otro identificador más ON_CLICK_BUTTON en el archivo Defines.mqh para la generación del evento de usuario. Va a utilizarse en todas las clases destinadas para la creación de los botones.

#define ON_CLICK_BUTTON           (8)  // Clic en el botón

Ahora hay que crear el método CSimpleButton::OnClickButton() para el procesamiento del clic del ratón. Al principio del método hacen falta dos comprobaciones: (1) para el nombre del objeto pulsado y (2) para el estado actual del objeto. El resultado negativo de estas comprobaciones provoca la salida del método. Si las comprobaciones han pasado con éxito, el procesamiento se realiza tomando en cuenta el modo en el que se encuentra el botón. Si el botón se suelta por sí mismo, se realiza la vuelta a su estado inicial y el color correspondiente. Para el modo con dos estados, para cada estado se utilizan dos grupos de colores. Al final del método se envía el mensaje con el identificador ON_CLICK_BUTTON, identificador del elemento, índice del elemento y el nombre del botón. 

class CSimpleButton : public CElement
  {
private:
   //--- Procesamiento del clic en el botón
   bool              OnClickButton(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Manejo de eventos                                                  |
//+------------------------------------------------------------------+
void CSimpleButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Procesamiento del evento del clic izquierdo en el objeto
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickButton(sparam))
         return;
     }
  }
//+------------------------------------------------------------------+
//| Procesamiento de pulsación del botón
//+------------------------------------------------------------------+
bool CSimpleButton::OnClickButton(const string clicked_object)
  {
//--- Comprobación según el nombre del objeto
   if(m_button.Name()!=clicked_object)
      return(false);
//--- Si el botón está bloqueado
   if(!m_button_state)
     {
      m_button.State(false);
      return(false);
     }
//--- Si el modo del botón tiene un estado
   if(!m_two_state)
     {
      m_button.State(false);
      m_button.Color(m_text_color);
      m_button.BackColor(m_back_color);
     }
//--- Si el modo del botón tiene dos estados
   else
     {
      //--- Si el botón está pulsado
      if(m_button.State())
        {
         //--- Cambiamos los colores del botón 
         m_button.State(true);
         m_button.Color(m_text_color_pressed);
         m_button.BackColor(m_back_color_pressed);
         CElement::InitColorArray(m_back_color_pressed,m_back_color_pressed,m_back_color_array);
        }
      //--- Si el botón está suelto
      else
        {
         //--- Cambiamos los colores del botón 
         m_button.State(false);
         m_button.Color(m_text_color);
         m_button.BackColor(m_back_color);
         CElement::InitColorArray(m_back_color,m_back_color_hover,m_back_color_array);
        }
     }
//--- Enviar la señal sobre ello
   ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),CElement::Index(),m_button.Description());
   return(true);
  }

Ya podemos probar ahora mismo la colocación de botones sobre el gráfico con su vinculación al formulario, así como la recepción de mensajes en el manejador de la clase de usuario de la aplicación. Haremos la copia del programa de prueba (EA) del artículo anterior. Dejaremos ahí sólo el menú principal con los menús contextuales adjuntados a sus elementos. El archivo con la clase CSimpleButton tiene que incluirse en el archivo WndContainer.mqh para poder usarla en la clase personalizada. 

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "SimpleButton.mqh"

Después de eso, se puede declarar las instancias de la clase CSimpleButton y los métodos para la creación de los botones en la clase personalizada de la aplicación CProgram. Como ejemplo vamos a crear tres botones. Dos de ellos serán que se sueltan por sí mismos después de ser pulsados, y el tercer botón tendrá la posibilidad de fijación, es decir tendrá dos estados (pulsado/suelto).

class CProgram : public CWndEvents
  {
private:
   //--- Botones simples
   CSimpleButton     m_simple_button1;
   CSimpleButton     m_simple_button2;
   CSimpleButton     m_simple_button3;
   //---
private:
#define BUTTON1_GAP_X            (7)
#define BUTTON1_GAP_Y            (50)
   bool              CreateSimpleButton1(const string text);
#define BUTTON2_GAP_X            (128)
#define BUTTON2_GAP_Y            (50)
   bool              CreateSimpleButton2(const string text);
#define BUTTON3_GAP_X            (7)
#define BUTTON3_GAP_Y            (75)
   bool              CreateSimpleButton3(const string text);
  };

Como ejemplo, mostraremos el código sólo de uno de ellos. Fíjese en la línea resaltada en el código donde se activa el modo cuando el botón debe tener dos estados. 

//+------------------------------------------------------------------+
//| Crea el botón simple 3                                           |
//+------------------------------------------------------------------+
bool CProgram::CreateSimpleButton3(string button_text)
  {
//--- Pasa el objeto del panel
   m_simple_button3.WindowPointer(m_window);
//--- Coordenadas
   int x=m_window.X()+BUTTON3_GAP_X;
   int y=m_window.Y()+BUTTON3_GAP_Y;
//--- Establecemos las propiedades antes de la creación
   m_simple_button3.TwoState(true);
   m_simple_button3.ButtonXSize(237);
   m_simple_button3.TextColor(clrBlack);
   m_simple_button3.TextColorPressed(clrBlack);
   m_simple_button3.BackColor(clrLightGray);
   m_simple_button3.BackColorHover(C'193,218,255');
   m_simple_button3.BackColorPressed(C'153,178,215');
//--- Crear el botón
   if(!m_simple_button3.CreateSimpleButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- Añadimos el objeto al array común de los grupos de objetos
   CWndContainer::AddToElementsArray(0,m_simple_button3);
   return(true);
  }

La llamada a todos los métodos para la creación de los elementos de la interfaz gráfica se realiza en el método CProgram::CreateTradePanel():

//+------------------------------------------------------------------+
//| Crea el panel de trading                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creación del formulario para los controles
//--- Creación de controles:
//    Menú principal
//--- Menús contextuales
//--- Botones simples
   if(!CreateSimpleButton1("Simple Button 1"))
      return(false);
   if(!CreateSimpleButton2("Simple Button 2"))
      return(false);
   if(!CreateSimpleButton3("Simple Button 3"))
      return(false);
 //--- Redibujar el gráfico
   m_chart.Redraw();
   return(true);
  }

El mensaje con el identificador del evento ON_CLICK_BUTTON lo vamos a recibir en el manejador de eventos CProgram::OnEvent() tal como se muestra en el código de abajo. Como ejemplo, vamos a implementar el bloque del código en el que va a comprobarse el nombre del botón. Si resulta que que ha sido pulsado el tercer botón, entonces en función de su estado actual va a depender el estado del segundo botón. Es decir, si el tercer botón está pulsado, entonces el segundo botón va a bloquearse, y viceversa.

//+------------------------------------------------------------------+
//| 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)
     {
      ::Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //---
      if(mi.TypeMenuItem()!=MI_HAS_CONTEXT_MENU)
        {
         if(m_simple_button3.IsPressed())
            m_simple_button1.ButtonState(false);
         else
            m_simple_button1.ButtonState(true);
        }
     }
  }

En las capturas de abajo se muestra el resultado que se debe obtener después de la compilación y la descarga del programa en el gráfico. En la captura izquierda, el botón Simple Button 3 está suelto, y el botón Simple Button 1 está disponible. En la captura derecha, el botón Simple Button 3 está pulsado, y el botón Simple Button 1 está bloqueado. 

 Fig. 2. Prueba de colocación del control “Botón” sobre el gráfico. Se muestran diferentes estados de botones.

Fig. 2. Prueba de colocación del control “Botón” sobre el gráfico. Se muestran diferentes estados de botones. 

 

Pero en esta implementación falta un pequeño detalle que transmita determinada realidad durante la interacción con el botón. Hay que hacer que el color del botón se cambie justo después de hacer clic en él. Se puede comprobar si está pulsado el botón izquierdo en este momento a través del evento del desplazamiento del cursor CHARTEVENT_MOUSE_MOVE, por eso vamos a añadir el código correspondiente en el manejador de eventos CSimpleButton::OnEvent(). 

En casos cuando (1) el elemento está ocultado, (2) el formulario está bloqueado, (3) el botón izquierdo del ratón está suelto y (4) el botón está bloqueado, entonces el programa sale del manejador. Pero si todas estas comprobaciones han sido superadas, entonces dependiendo del foco y el estado actual del botón se establece el color correspondiente.

//+------------------------------------------------------------------+
//| Manejo de eventos                                                  |
//+------------------------------------------------------------------+
void CSimpleButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Procesamiento del evento del desplazamiento del cursor
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Salir si el control está ocultado
      if(!CElement::IsVisible())
         return;
      //--- Definimos el foco
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      //--- Salir si el formulario está bloqueado
      if(m_wnd.IsLocked())
         return;
      //--- Salir si el botón del ratón está suelto
      if(sparam=="0")
         return;
      //--- Salir si el botón está bloqueado
      if(!m_button_state)
         return;
      //--- Si no hay foco
      if(!CElement::MouseFocus())
        {
         //--- Si el botón está suelto
         if(!m_button.State())
            {
             m_button.Color(m_text_color);
             m_button.BackColor(m_back_color);
            }
         //---
         return;
        }
      //--- Si hay foco
      else
        {
         m_button.Color(m_text_color_pressed);
         m_button.BackColor(m_back_color_pressed);
         return;
        }
      //---
      return;
     }
  }

Ahora todo va a funcionar tal como se ha planteado. Hemos terminado el desarrollo de la clase para crear un botón simple. Puede estudiar su código con más detalles en los archivos adjuntos al artículo. A continuación, pasamos a la clase para el botón con funcionalidad extendida.

 


Desarrollando la clase para crear un botón con imagen

El botón con imagen se compone de tres objetos gráficos primitivos:

  1. Fondo.
  2. Icono.
  3. Etiqueta de texto.


Fig. 3. Partes integrantes del control “Botón con imagen”.

 

La etiqueta de texto es necesaria para el posicionamiento libre del texto del botón. Por ejemplo, el botón puede ser compuesto de tal manera que el texto se encuentre en la parte inferior del botón, y su icono en la parte superior, y viceversa. Más tarde mostraremos estos ejemplos.

La clase para la creación de este control tendrá los mismos campos y métodos que la clase del botón simple CSimpleButton. Aparte de las propiedades referentes al tamaño del botón y su color, vamos a necesitar los campos y los métodos para establecer los márgenes para el icono y la etiqueta de texto respecto a las coordenadas del elemento, así como las imágenes del icono en el estado activo y bloqueado. También vamos a añadir la posibilidad de crear el botón sólo a base de la imagen. En este caso, el botón va a componerse sólo de un objeto tipo OBJ_BITMAP_LABEL.

Luego hay que crear el archivo IconButton.mqh y la clase CIconButton dentro de él. Lo incluimos en el archivo WndContainer.mqh, igual que todos los demás controles. En el código de abajo se muestran sólo aquellos campos y métodos de la clase CIconButton que la hacen diferente de la clase para la creación del botón simple (CSimpleButton).

class CIconButton : public CElement
  {
private:
   //--- Objetos para crear el botón
   CButton           m_button;
   CBmpLabel         m_icon;
   CLabel            m_label;
   //--- Propiedades del botón
   //    Iconos del botón en el estado activo y bloqueado
   string            m_icon_file_on;
   string            m_icon_file_off;
   //--- Márgenes del icono
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //--- Texto y márgenes de la etiqueta de texto
   string            m_label_text;
   int               m_label_x_gap;
   int               m_label_y_gap;
   //--- Modo “Sólo imagen”, cuando el botón se compone sólo del objeto BmpLabel
   bool              m_only_icon;
   //---
public:
   //--- Métodos para crear el botón
   bool              CreateIconButton(const long chart_id,const int subwin,const string button_text,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   //---
public:
   //--- Establecer el modo “Sólo imagen”
   void              OnlyIcon(const bool flag)                { m_only_icon=flag;               }
   //--- Establecer los iconos para el botón en el estado activo y bloqueado
   void              IconFileOn(const string file_path)       { m_icon_file_on=file_path;       }
   void              IconFileOff(const string file_path)      { m_icon_file_off=file_path;      }
   //--- Márgenes del icono
   void              IconXGap(const int x_gap)                { m_icon_x_gap=x_gap;             }
   void              IconYGap(const int y_gap)                { m_icon_y_gap=y_gap;             }
   //--- Márgenes de la etiqueta de texto
   void              LabelXGap(const int x_gap)               { m_label_x_gap=x_gap;            }
   void              LabelYGap(const int y_gap)               { m_label_y_gap=y_gap;            }
  };

No olvidemos inicializar todos los campos con valores predefinidos:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CIconButton::CIconButton(void) : m_icon_x_gap(4),
                                 m_icon_y_gap(3),
                                 m_label_x_gap(25),
                                 m_label_y_gap(4),
                                 m_icon_file_on(""),
                                 m_icon_file_off(""),
                                 m_button_state(true),
                                 m_two_state(false),
                                 m_only_icon(false),
                                 m_button_y_size(18),
                                 m_back_color(clrLightGray),
                                 m_back_color_off(clrLightGray),
                                 m_back_color_hover(clrSilver),
                                 m_back_color_pressed(clrBlack),
                                 m_border_color(clrWhite),
                                 m_border_color_off(clrDarkGray),
                                 m_label_color(clrBlack),
                                 m_label_color_off(clrDarkGray),
                                 m_label_color_hover(clrBlack),
                                 m_label_color_pressed(clrBlack)
  {
//--- Guardamos el nombre de la clase del control en la clase base  
   CElement::ClassName(CLASS_NAME);
 //--- Establecemos las prioridades para el clic izquierdo del ratón
   m_button_zorder =1;
   m_zorder        =0;
  }

En los métodos de la creación de todos los objetos del botón habrá la comprobación para el modo “Sólo imagen”. Si se establece que el botón tiene que componerse sólo del icono, el programa sale al principio del método de la creación del fondo y etiqueta de texto.

//+------------------------------------------------------------------+
//| Creamos el fondo del botón                                               |
//+------------------------------------------------------------------+
bool CIconButton::CreateButton(void)
  {
//--- Salimos si está activado el modo “Sólo imagen”
   if(m_only_icon)
      return(true);
//--- ... etc.
  }
//+------------------------------------------------------------------+
//| Crea el texto del botón                                           |
//+------------------------------------------------------------------+
bool CIconButton::CreateLabel(void)
  {
//--- Salimos si está activado el modo “Sólo imagen”
   if(m_only_icon)
      return(true);
//--- ... etc.
  }

En el método de la creación del icono cuando el botón se compone sólo de la imagen habrá otra comprobación donde va a comprobarse la presencia de la imágenes. Si las imágenes no están definidas, la construcción de la interfaz se terminará en esta fase, en el registro aparecerá el mensaje diciendo que la presencia de las imágenes en este modo es obligatoria. En el código de abajo se muestra el método CIconButton::CreateIcon() para la creación del icono del botón:

//+------------------------------------------------------------------+
//| Crea la imagen del botón                                          |
//+------------------------------------------------------------------+
bool CIconButton::CreateIcon(void)
  {
//--- Si el modo “Sólo imagen” está desactivado
   if(!m_only_icon)
     {
      //--- Si el icono para el botón no es necesario, salir
      if(m_icon_file_on=="" || m_icon_file_off=="")
         return(true);
     }
//--- Si está activado el modo “Sólo imagen”
   else
     {
      //--- Si el icono no está definido, mostrar el mensaje y salir
      if(m_icon_file_on=="" || m_icon_file_off=="")
        {
         ::Print(__FUNCTION__," > En el modo \"Only icon\" es obligatorio definir la imagen.");
         return(false);
        }
     }
//--- Formación del nombre del objeto
   string name=CElement::ProgramName()+"_icon_button_bmp_"+(string)CElement::Id();
//--- Coordenadas
   int x =(!m_only_icon)? m_x+m_icon_x_gap : m_x;
   int y =(!m_only_icon)? m_y+m_icon_y_gap : m_y;
//--- Establecemos el icono
   if(!m_icon.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- Establecemos las propiedades
   m_icon.BmpFileOn("::"+m_icon_file_on);
   m_icon.BmpFileOff("::"+m_icon_file_off);
   m_icon.State(true);
   m_icon.Corner(m_corner);
   m_icon.GetInteger(OBJPROP_ANCHOR,m_anchor);
   m_icon.Selectable(false);
   m_icon.Z_Order((!m_only_icon)? m_zorder : m_button_zorder);
   m_icon.Tooltip((!m_only_icon)? "\n" : m_label_text);
//--- Guardamos las coordenadas
   m_icon.X(x);
   m_icon.Y(y);
//--- Guardamos los tamaños
   m_icon.XSize(m_icon.X_Size());
   m_icon.YSize(m_icon.Y_Size());
//--- Margen desde el punto extremo
   m_icon.XGap(x-m_wnd.X());
   m_icon.YGap(y-m_wnd.Y());
//--- Guardamos el puntero del objeto
   CElement::AddToArray(m_icon);
   return(true);
  }

Por todo lo demás, la clase CIconButton no tiene ningunas diferencias particulares de lo que hemos considerado en la clase CSimpleButton. Se puede ver su versión completa en los archivos adjuntos al artículo.

A continuación, vamos a probar los botones tipo CIconButton. Para demostrar sus posibilidades, crearemos cinco botones con diferentes modos, tamaños y estados en el EA de pruebas. Cree cinco instancias de la clase CIconButton en la clase personalizada del EA de pruebas, y declare cinco métodos para la creación de botones, tal como se muestra en el código de abajo. 

class CProgram : public CWndEvents
  {
private:
   //--- Botones con imágenes
   CIconButton       m_icon_button1;
   CIconButton       m_icon_button2;
   CIconButton       m_icon_button3;
   CIconButton       m_icon_button4;
   CIconButton       m_icon_button5;
   //---
private:
   //--- Botones con imágenes
#define ICONBUTTON1_GAP_X        (7)
#define ICONBUTTON1_GAP_Y        (105)
   bool              CreateIconButton1(const string text);
#define ICONBUTTON2_GAP_X        (128)
#define ICONBUTTON2_GAP_Y        (105)
   bool              CreateIconButton2(const string text);
#define ICONBUTTON3_GAP_X        (7)
#define ICONBUTTON3_GAP_Y        (130)
   bool              CreateIconButton3(const string text);
#define ICONBUTTON4_GAP_X        (88)
#define ICONBUTTON4_GAP_Y        (130)
   bool              CreateIconButton4(const string text);
#define ICONBUTTON5_GAP_X        (169)
#define ICONBUTTON5_GAP_Y        (130)
   bool              CreateIconButton5(const string text);
  };

Usaremos la implementación sólo de uno de estos métodos como ejemplo, ya que todos ellos son prácticamente iguales, a excepción de los valores para los parámetros:

//+------------------------------------------------------------------+
//| Crea el botón con la imagen 5                                     |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp64\\gold.bmp"
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp64\\gold_colorless.bmp"
//---
bool CProgram::CreateIconButton5(const string button_text)
  {
//--- Pasa el objeto del panel
   m_icon_button5.WindowPointer(m_window);
//--- Coordenadas
   int x=m_window.X()+ICONBUTTON5_GAP_X;
   int y=m_window.Y()+ICONBUTTON5_GAP_Y;
//--- Establecemos las propiedades antes de la creación
   m_icon_button5.ButtonXSize(76);
   m_icon_button5.ButtonYSize(87);
   m_icon_button5.LabelXGap(6);
   m_icon_button5.LabelYGap(69);
   m_icon_button5.LabelColor(clrBlack);
   m_icon_button5.LabelColorPressed(clrBlack);
   m_icon_button5.BackColor(clrGainsboro);
   m_icon_button5.BackColorHover(C'193,218,255');
   m_icon_button5.BackColorPressed(C'210,210,220');
   m_icon_button5.BorderColor(C'150,170,180');
   m_icon_button5.BorderColorOff(C'178,195,207');
   m_icon_button5.IconXGap(6);
   m_icon_button5.IconYGap(3);
   m_icon_button5.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp64\\gold.bmp");
   m_icon_button5.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp64\\gold_colorless.bmp");
//--- Creamos el control
   if(!m_icon_button5.CreateIconButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- Añadimos el puntero al control a la base
   CWndContainer::AddToElementsArray(0,m_icon_button5);
   return(true);
  }

 No olvide colocar la llamada a estos métodos en el método principal de la creación de la interfaz gráfica del programa. 

Como ejemplo, añadiremos el seguimiento del clic en el botón Icon Button 2 en el manejador de eventos. En la versión adjunta a este artículo, este botón trabaja en dos modos (pulsado/suelto). La disponibilidad de los botones Icon Button 1 y Icon Button 4 depende del estado del botón Icon Button 2. Si este botón esta suelto, los botones dependientes de él están bloqueados, y viceversa. 

//+------------------------------------------------------------------+
//| 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)
     {
      Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //---
      if(mi.TypeMenuItem()!=MI_HAS_CONTEXT_MENU)
        {
         if(m_simple_button3.IsPressed())
            m_simple_button1.ButtonState(false);
         else
            m_simple_button1.ButtonState(true);
        }
      //---
      if(sparam==m_icon_button2.Text())
        {
         if(m_icon_button2.IsPressed())
           {
            m_icon_button1.ButtonState(true);
            m_icon_button4.ButtonState(true);
           }
         else
           {
            m_icon_button1.ButtonState(false);
            m_icon_button4.ButtonState(false);
           }
        }
     }
  }

Después de compilar los archivos y cargar el EA de pruebas en el gráfico, verá el resultado que se muestra abajo:

Fig. 4. Prueba del control “Botón con imagen”.

Fig. 4. Prueba del control “Botón con imagen”.

 

Hemos terminado el desarrollo de la clase CIconButton para crear un botón con posibilidades extendidas. Al final del artículo se puede descargar las imágenes de los botones que figuran en las capturas de arriba. A continuación, hablaremos del desarrollo de la clase para crear un botón de división (split button). 



Desarrollando la clase para crear un botón de división

¿Qué es un botón de división? Es un botón que se compone de dos partes funcionales:

  • La primera parte es el botón que contiene la función principal.
  • La segunda parte es el botón que abre el menú contextual con funciones adicionales.

Este tipo del control se utiliza con bastante frecuencia en las interfaces gráficas de los programas. Normalmente, estos botones se utilizan cuando hay que agrupar de forma compacta varias funciones que pueden pertenecer a la misma categoría por su sentido. 

El botón de división va a componerse de cinco objetos (primitivas gráficas) y un elemento adjunto (menú contextual):

  1. Fondo.
  2. Icono.
  3. Texto.
  4. Fondo del botón adicional.
  5. Indicio del menú contextual.

 


Fig. 5. Partes integrantes del control “Botón de división”.

 

Aquí hemos llegado a que el menú contextual (objeto de la clase CContextMenu) no va a adjuntarse al objeto de la clase CMenuItem. Por eso vamos a necesitar un modo adicional cuando el menú contextual podrá ser parte de cualquier otro elemento o incluso no adjuntarse a ninguno. 

Aparte de eso, necesitaremos un punto de referencia más para el ajuste cualitativo de la interacción entre los controles que se activan por el usuario temporalmente (elementos desplegables). Hay que hacer que sólo el elemento que ha bloqueado el formulario pueda desbloquearlo. Si no lo hacemos, pueden surgir las situaciones de conflicto entre los elementos, porque el estado del formulario puede depender del estado de algún otro control. En el artículo se mostrará el ejemplo que luego podremos testear para comprender mejor en qué tipo de situaciones pueden surgir dichos conflictos. Para implementar esta funcionalidad, en la clase del formulario (CWindow) se añade el campo y el método para almacenar y recibir el control activado, tal como se muestra en el código de abajo:

class CWindow : public CElement
  {
private:
   //--- Identificador del control activado
   int               m_id_activated_element;
   //---
public:
   //--- Métodos para guardar y obtener ID del control activado
   int               IdActivatedElement(void)                          const { return(m_id_activated_element);     }
   void              IdActivatedElement(const int id)                        { m_id_activated_element=id;          }
  };

Ahora, cuando algún control bloquea el formulario durante su activación, hay que guardar el identificador de este control en el formulario. En el lugar donde el formulario se desbloquea, hay que realizar la comprobación buscando el identificador del control que lo ha bloqueado. Pero si antes del desbloqueo existe la comprobación del nombre del objeto pulsado, no hace falta la comprobación del identificador del elemento que ha bloqueado el formulario. 

A continuación, vamos a introducir adiciones en la clase CContextMenu que permitirán activar y procesar el modo del menú contextual libre (véase el código de abajo). Por defecto, se establece el modo del menú contextual con vinculación al nodo anterior

class CContextMenu : public CElement
  {
   //--- Modo del menú contextual libre. Es decir, sin vinculación al nodo anterior.
   bool              m_free_context_menu;
   //---
public:
   //--- Establecer el modo del menú contextual libre.
   void              FreeContextMenu(const bool flag)               { m_free_context_menu=flag;             }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void) : m_free_context_menu(false)
  {
  }

Los identificadores internos para el procesamiento del evento del clic en un elemento del menú van a ser diferentes para el menú contextual vinculado y libre. Esta división hará que el código sea más nítido, reducirá la cantidad de condiciones y permitirá controlar el procesamiento de eventos en diferentes modos de manera más flexible.

Añadimos el nuevo identificador (ON_CLICK_FREEMENU_ITEM) para la generación del evento de parte del menú contextual libre en el archivo Defines.mqh:

#define ON_CLICK_FREEMENU_ITEM    (9)  // Clic en un elemento del menú contextual libre

Hay que añadir las condiciones adicionales con la comprobación del modo del menú contextual libre en los siguientes lugares de la clase CContextMenu. Abajo se muestran las versiones reducidas de los métodos. Se dejan los comentarios.

1. En el manejador de eventos:

//+------------------------------------------------------------------+
//| Manejador de eventos                                               |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Procesamiento del desplazamiento del cursor del ratón
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Salir si el control está ocultado
      //--- Obtenemos el foco
      //--- Salir si es el menú contextual libre
      if(m_free_context_menu)
         return;
      //--- Si el menú contextual está activado y el botón izquierdo ha sido pulsado
       //--- Comprobamos las condiciones para cerrar todos los menús contextuales que han sido abiertos después de éste
      return;
     }
//--- Procesamiento del evento del clic izquierdo en el objeto
//--- Procesamiento del evento ON_CLICK_MENU_ITEM
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM)
     {
      //--- Salir si es el menú contextual libre
      if(m_free_context_menu)
         return;
      //--- Recepción del mensaje del elemento del menú para el procesamiento
      return;
     }
  }

2. //--- En el método de la creación del menú contextual:

//+------------------------------------------------------------------+
//| Crea el menú contextual                                         |
//+------------------------------------------------------------------+
bool CContextMenu::CreateContextMenu(const long chart_id,const int subwin,const int x=0,const int y=0)
  {
//--- Salir si no hay puntero al formulario
//--- Salir si es el menú contextual vinculado
   if(!m_free_context_menu)
     {
      //--- Salir si no hay puntero al nodo anterior
      if(::CheckPointer(m_prev_node)==POINTER_INVALID)
        {
         ::Print(__FUNCTION__," > Antes de crear el menú contextual, hay que pasarle "
                 "el puntero al nodo anterior usando el método CContextMenu::PrevNodePointer(CMenuItem &object).");
         return(false);
        }
     }
//--- Inicialización de variables
//--- Si las coordenadas no están especificadas
//--- Si las coordenadas están especificadas
//--- Márgenes desde el punto extremo
//--- Creando menú contextual
//--- Ocultar control
   return(true);
  }

3. En el método de la creación de la lista de los elementos del menú:

//+------------------------------------------------------------------+
//| Crea la lista de elementos del menú                                      |
//+------------------------------------------------------------------+
bool CContextMenu::CreateItems(void)
  {
   int s =0;     // Para determinar la posición de líneas separadoras
   int x =m_x+1; // Coordenada X
   int y =m_y+1; // Coordenada Y. Va calcularse en ciclo para cada elemento del menú.
//--- Número de líneas separadoras
//---
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Cálculo de la coordenada Y
      //--- Guardamos el puntero del formulario
       //--- Si el menú contextual es con vinculación, añadimos el puntero al nodo anterior
      if(!m_free_context_menu)
         m_items[i].PrevNodePointer(m_prev_node);
      //--- Establecemos las propiedades
      //--- Márgenes desde el punto extremo del panel
      //--- Creación del elemento del menú
      //--- Ir al siguiente si todas las líneas separadoras están colocadas
      //--- Si los índices coinciden, entonces después de este elemento hay que colocar una línea separadora
     }
   return(true);
  }

4. En los métodos de mostrar y ocultar el menú contextual: 

//+------------------------------------------------------------------+
//| 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
   if(!m_free_context_menu)
      m_prev_node.ContextMenuState(true);
//--- Bloquear el formulario
  }
//+------------------------------------------------------------------+
//| 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
//--- Asignar el estatus del control invisible
//--- Estado del menú contextual
//--- Anotar el estado en el nodo anterior
   if(!m_free_context_menu)
      m_prev_node.ContextMenuState(false);
  }

5. En el método del procesamiento del clic en un elemento del menú. En el bloque nuevo, para el modo del menú contextual libre se realiza la comprobación cíclica por el nombre del objeto pulsado. Si este objeto se encuentra, entonces se envía un evento con el identificador ON_CLICK_FREEMENU_ITEM. Luego, hay que seguir este evento en los manejadores de eventos de los controles que tienen un menú contextual como parte integrante (lo mostraremos con el ejemeplo del botón de división). 

//+------------------------------------------------------------------+
//| Procesamiento del clic en un elemento del menú                                  |
//+------------------------------------------------------------------+
bool CContextMenu::OnClickMenuItem(const string clicked_object)
  {
//--- Salimos si este menú contextual tiene el nodo anterior y ya está abierto
   if(!m_free_context_menu && m_context_menu_state)
      return(true);
//--- 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);
//--- Si el menú contextual tiene el nodo anterior
   if(!m_free_context_menu)
     {
      //--- Salimos si el clic no ha sido hecho en el elemento al que este menú contextual está adjuntado
      if(id!=m_prev_node.Id() || index!=m_prev_node.Index())
         return(false);
      //--- Mostrar el menú contextual
      Show();
     }
//--- Si es el menú contextual libre
   else
     {
      //--- Buscamos en el ciclo el elemento del menú en el que hemos pulsado
      int total=ItemsTotal();
      for(int i=0; i<total; i++)
        {
         if(m_items[i].Object(0).Name()!=clicked_object)
            continue;
         //--- Enviamos el mensaje sobre ello
         ::EventChartCustom(m_chart_id,ON_CLICK_FREEMENU_ITEM,CElement::Id(),i,DescriptionByIndex(i));
         break;
        }
     }
//---
   return(true);
  }

Tenemos todo preparado para el desarrollo de la clase del botón de división. Creamos el archivo SplitButton.mqh con la clase CSplitButton y los métodos estándar para todos los controles en la carpeta Controls:

//+------------------------------------------------------------------+
//|                                                  SplitButton.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "ContextMenu.mqh"
//+------------------------------------------------------------------+
//| Clase para crear el botón de división                              |
//+------------------------------------------------------------------+
class CSplitButton : public CElement
  {
private:
   //--- Puntero al formulario al que está adjuntado el control
   CWindow          *m_wnd;
   //---
public:
                     CSplitButton();
                    ~CSplitButton();
   //--- 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);
   //--- Temporizador
   virtual void      OnEventTimer(void);
   //--- 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);
  };

Aparte de las propiedades y métodos que son típicos para los botones de todos los tipos y ya han sido descritos en este artículo, vamos a necesitar los recursos adicionales para la configuración del botón con el menú contextual desplegable:

  • Tamaño. En esta versión se utiliza sólo el ancho. El alto será el mismo que tiene el botón principal.
  • Prioridad para el clic izquierdo del ratón. Para el botón con el menú contextual desplegable, la prioridad tiene que ser más alta que para el botón principal.
  • Imágenes y márgenes para el icono del botón.
  • Estado del menú contextual del botón (visible/ocultado). 
class CSplitButton : public CElement
  {
private:
   //--- Tamaño y prioridad del botón con el menú desplegable para el clic izquierdo del ratón
   int               m_drop_button_x_size;
   int               m_drop_button_zorder;
   //--- Márgenes del icono
   int               m_drop_arrow_x_gap;
   int               m_drop_arrow_y_gap;
   //Iconos del botón con el menú desplegable en el estado activo y bloqueado
   string            m_drop_arrow_file_on;
   string            m_drop_arrow_file_off;
   //--- Estado del menú contextual
   bool              m_drop_menu_state;
   //---
public:
   //--- Tamaño del botón con el menú desplegable
   void              DropButtonXSize(const int x_size)        { m_drop_button_x_size=x_size;        }
   //--- Establecer los iconos para el botón con el menú desplegable en el estado activo y bloqueado
   void              DropArrowFileOn(const string file_path)  { m_drop_arrow_file_on=file_path;     }
   void              DropArrowFileOff(const string file_path) { m_drop_arrow_file_off=file_path;    }
   //--- Márgenes del icono
   void              DropArrowXGap(const int x_gap)           { m_drop_arrow_x_gap=x_gap;           }
   void              DropArrowYGap(const int y_gap)           { m_drop_arrow_y_gap=y_gap;           }
  };

Como ya hemos dicho antes, para la creación de un botón de división hacen falta cinco objetos primitivos y el menú contextual. Vamos a declarar las instancias de las clases necesarias y los métodos para su creación. Además, nos harán falta los métodos para la formación del menú contextual (inserción de los elementos del menú y las líneas separadoras). Y como las propiedades para el menú contextual se establecen por el usuario, necesitamos un método para obtener el puntero al menú contextual del botón.

class CSplitButton : public CElement
  {
private:
   //--- Objetos para crear el botón
   CButton           m_button;
   CBmpLabel         m_icon;
   CLabel            m_label;
   CEdit             m_drop_button;
   CBmpLabel         m_drop_arrow;
   CContextMenu      m_drop_menu;
   //---
public:
   //--- Métodos para crear el botón
   bool              CreateSplitButton(const long chart_id,const string button_text,const int window,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   bool              CreateDropButton(void);
   bool              CreateDropIcon(void);
   bool              CreateDropMenu(void);
   //---
public:
   //--- Obtener el puntero del menú contextual,
   CContextMenu     *GetContextMenuPointer(void)        const { return(::GetPointer(m_drop_menu));  }
   //--- Añade un elemento del menú con propiedades especificadas antes de la creación del menú contextual
   void              AddItem(const string text,const string path_bmp_on,const string path_bmp_off);
   //--- Añade la línea separadora después del elemento especificado antes de crear el menú contextual
   void              AddSeparateLine(const int item_index);
  };

Los métodos para la creación de los objetos del elemento no tienen diferencias importantes de los que hemos considerado antes. El único detalle importante es que en el método de la creación del menú contextual (véase el código de abajo) hay que establecer obligatoriamente el identificador del botón de división como parte del control. De esta manera, luego se puede determinar por el identificador de qué elemento del menú exactamente ha llegado el mensaje ON_CLICK_FREEMENU_ITEM.

//+------------------------------------------------------------------+
//| Crea el menú desplegable                                          |
//+------------------------------------------------------------------+
bool CSplitButton::CreateDropMenu(void)
  {
//--- Pasa el objeto del panel
   m_drop_menu.WindowPointer(m_wnd);
//--- Menú contextual libre
   m_drop_menu.FreeContextMenu(true);
//--- Coordenadas
   int x=m_x;
   int y=m_y+m_y_size;
//--- Establecemos las propiedades
   m_drop_menu.Id(CElement::Id());
   m_drop_menu.XSize((m_drop_menu.XSize()>0)? m_drop_menu.XSize() : m_button_x_size);
//--- Establecemos el menú contextual
   if(!m_drop_menu.CreateContextMenu(m_chart_id,m_subwin,x,y))
      return(false);
//---
   return(true);
  }

Vamos a considerar los métodos para la interacción con el botón de división. Puesto que el botón de este tipo tiene dos partes, hay que crear dos métodos separados para el procesamiento del clic en cada una de ellas. Estos métodos van a llamarse en el manejador de la clase en el bloque del evento CHARTEVENT_OBJECT_CLICK

class CSplitButton : public CElement
  {
private:
   //--- Procesamiento del clic en el botón
   bool              OnClickButton(const string clicked_object);
   //--- Procesamiento del clic en el botón con el menú desplegable
   bool              OnClickDropButton(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Manejador de eventos                                               |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Procesamiento del evento del clic izquierdo en el objeto
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Clic en el botón principal
      if(OnClickButton(sparam))
         return;
       //--- Clic en el botón con el menú desplegable
      if(OnClickDropButton(sparam))
         return;
     }
  }

En el método del manejador del clic en el botón principal CSplitButton::OnClickButton(), primero se comprueba el nombre del objeto. Si éste es el nombre del objeto de esta instancia de la clase, se comprueba el estado del botón. Si el botón está bloqueado, el programa sale del método. El botón principal puede tener sólo un estado. Es decir, tiene que volver en el estado suelto después de haber sido pulsado. Si todas las comprobaciones han sido hechas, entonces hay que (1) ocultar el menú contextual sis estaba visible, (2) establecer el estatus correspondiente del estado y del color para el menú y el botón, (3) desbloquear el formulario y poner a cero el identificador del elemento activador en su memoria. 

Al final del método hay que enviar el mensaje que será recibido en la clase personalizada. El mensaje va a contener (1) el identificador del evento ON_CLICK_BUTTON, (2) identificador del elemento, (3) índice del elemento y (4) la descripción visualizada del botón. 

//+------------------------------------------------------------------+
//| Clic en el botón                                                |
//+------------------------------------------------------------------+
bool CSplitButton::OnClickButton(const string clicked_object)
  {
//--- Salimos si el nombre del objeto no coincide  
   if(clicked_object!=m_button.Name())
      return(false);
//--- Salir si el botón está bloqueado
   if(!m_button_state)
     {
      //--- Saltamos el botón
      m_button.State(false);
      return(false);
     }
//--- Ocultamos el menú
   m_drop_menu.Hide();
   m_drop_menu_state=false;
//--- Saltamos el botón y establecemos el color del foco
   m_button.State(false);
   m_button.BackColor(m_back_color_hover);
   m_drop_button.BackColor(m_back_color_hover);
 //--- Desbloquear el formulario
   m_wnd.IsLocked(false);
   m_wnd.IdActivatedElement(WRONG_VALUE);
//--- Enviamos el mensaje sobre ello
   ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),CElement::Index(),m_label.Description());
   return(true);
  }

En el método CSplitButton::OnClickDropButton(), en el que se procesa el clic en el botón con el menú desplegable, también hay dos comprobaciones al principio: (1) para el nombre y (2) disponibilidad del botón. Luego el programa entra en uno de los bloques del código dependiendo del estado actual de la visibilidad del menú contextual del botón para mostrar u ocultarlo.

//+------------------------------------------------------------------+
//| Clic en el botón con el menú desplegable                              |
//+------------------------------------------------------------------+
bool CSplitButton::OnClickDropButton(const string clicked_object)
  {
//--- Salimos si el nombre del objeto no coincide  
   if(clicked_object!=m_drop_button.Name())
      return(false);
//--- Salir si el botón está bloqueado
   if(!m_button_state)
     {
      //--- Saltamos el botón
      m_button.State(false);
      return(false);
     }
//--- Si el menú está abierto, lo ocultamos
   if(m_drop_menu_state)
     {
      m_drop_menu_state=false;
      m_drop_menu.Hide();
      m_button.BackColor(m_back_color_hover);
      m_drop_button.BackColor(m_back_color_hover);
      //--- Desbloqueamos el formulario y ponemos a cero id del elemento activador
      m_wnd.IsLocked(false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
     }
//--- Si el menú está ocultado, lo abrimos
   else
     {
      m_drop_menu_state=true;
      m_drop_menu.Show();
      m_button.BackColor(m_back_color_hover);
      m_drop_button.BackColor(m_back_color_pressed);
      //--- Bloqueamos el formulario y guardamos  id del elemento activador
      m_wnd.IsLocked(true);
      m_wnd.IdActivatedElement(CElement::Id());
     }
//---
   return(true);
  }

Antes en el artículo, hemos introducido una adición en la clase del menú contextual CContextMenu que permite en el modo libre el envío del evento para el uso interno con el identificador ON_CLICK_FREEMENU_ITEM. Este mensaje va a recibirse en el manejador de la clase del botón de división CSplitButton. Para determinar que el mensaje ha llegado del menú contextual relativo, hay que verificar el identificador del elemento que se encuentra en el parámetro lparam. Si los identificadores coinciden, hay que (1) ocultar el menú, (2) establecer los colores correspondientes al estado del botón y (3) desbloquear el formulario si este elemento ha sido el activador. Después de eso, se envía un mensaje con el identificador ON_CLICK_CONTEXTMENU_ITEM que puede ser recibido en la clase personalizada.

Además, vamos a crear otro método adicional CSplitButton::HideDropDownMenu() para el uso múltiple, cuya tarea consiste en ocultar el menú y desbloquear el formulario con la puesta a cero del identificador del elemento activador.

class CSplitButton : public CElement
  {
private:
   //--- Oculta el menú desplegable
   void              HideDropDownMenu(void);
  };
//+------------------------------------------------------------------+
//| Manejador de eventos                                               |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Procesamiento del evento del clic en un elemento del menú libre
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_FREEMENU_ITEM)
     {
      //--- Salir si los identificadores no coinciden
      if(CElement::Id()!=lparam)
         return;
       //--- Ocultar el menú desplegable
      HideDropDownMenu();
      //--- Enviamos el mensaje
      ::EventChartCustom(m_chart_id,ON_CLICK_CONTEXTMENU_ITEM,lparam,dparam,sparam);
      return;
     }
  }
//+------------------------------------------------------------------+
//| Oculta el menú desplegable                                         |
//+------------------------------------------------------------------+
void CSplitButton::HideDropDownMenu(void)
  {
//--- Ocultar el menú y establecer los indicios correspondientes
   m_drop_menu.Hide();
   m_drop_menu_state=false;
   m_button.BackColor(m_back_color);
   m_drop_button.BackColor(m_back_color);
//--- Desbloqueamos el formulario si los los identificadores del formulario y de este elemento coinciden
   if(m_wnd.IdActivatedElement()==CElement::Id())
     {
      m_wnd.IsLocked(false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
     }
  }

Ahora hay que configurar la reacción del botón de división a la posición del cursor y al estado del botón izquierdo del ratón cuando el cursor está situado encima. Para eso necesitaremos otro método que llamaremos CSplitButton::CheckPressedOverButton(). Este método tiene sólo un parámetro: el estado del botón izquierdo del ratón. Al principio hay dos comprobaciones. Si resulta que (1) el curso está fuera del área del botón o (2) el formulario está bloqueado cuando éste no ha sido el elemento activador, el programa saldrá del método. Si las comprobaciones han sido realizadas, el programa establece los colores correspondientes dependiendo del estado del botón izquierdo del ratón y sobre qué parte del botón se encuentra el cursor.

class CSplitButton : public CElement
  {
private:
   //--- Comprobación del botón izquierdo ratón pulsado sobre el botón de división
   void              CheckPressedOverButton(const bool mouse_state);
  };
//+------------------------------------------------------------------+
//| Comprobación del botón izquierdo ratón pulsado sobre el botón de división         |
//+------------------------------------------------------------------+
void CSplitButton::CheckPressedOverButton(const bool mouse_state)
  {
//--- Salir si está fuera del área del elemento
   if(!CElement::MouseFocus())
      return;
//--- Salir si el formulario está bloqueado, y los identificadores del formulario y de este elemento no coinciden
   if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      return;
//--- El botón del ratón está pulsado
   if(mouse_state)
     {
      //--- En el área del botón del menú
      if(m_drop_button.MouseFocus())
        {
         m_button.BackColor(m_back_color_hover);
         m_drop_button.BackColor(m_back_color_pressed);
        }
      else
        {
         m_button.BackColor(m_back_color_pressed);
         m_drop_button.BackColor(m_back_color_pressed);
        }
     }
//--- El botón del ratón está suelto
   else
     {
      if(m_drop_menu_state)
        {
         m_button.BackColor(m_back_color_hover);
         m_drop_button.BackColor(m_back_color_pressed);
        }
     }
  }

El método CSplitButton::CheckPressedOverButton() se llama en el manejador según el evento del desplazamiento del ratón. Antes de su llamada se comprueba si (1) está ocultado el elemento, (2) el foco, (3) si está disponible el elemento, así como (4) si el cursor se encuentra fuera del área del elemento, hay que ocultar el menú y salir sin llegar a llamar el método CSplitButton::CheckPressedOverButton(). 

//+------------------------------------------------------------------+
//| Manejador de eventos                                               |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Procesamiento del evento del desplazamiento del cursor
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Salir si el control está ocultado
      if(!CElement::IsVisible())
         return;
      //--- Definimos el foco
      int x=(int)lparam;
      int y=(int)dparam;
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      m_drop_button.MouseFocus(x>m_drop_button.X() && x<m_drop_button.X2() && 
                               y>m_drop_button.Y() && y<m_drop_button.Y2());
      //--- Salir si el botón está bloqueado
      if(!m_button_state)
         return;
      //--- Fuera del área del elemento y con el botón del ratón pulsado
      if(!CElement::MouseFocus() && sparam=="1")
        {
         //--- Salir si el foco se encuentra en el menú contextual
         if(m_drop_menu.MouseFocus())
            return;
         //--- Ocultar el menú desplegable
         HideDropDownMenu();
         return;
        }
       //--- Comprobación del botón izquierdo del ratón pulsado sobre el botón de división
      CheckPressedOverButton(bool((int)sparam));
      return;
     }
  }

La clase del control “Botón de división” está lista para la prueba. Pero para que trabaje de forma correcta, hay que empotrarla correctamente en la estructura de la librería. Recuerde que hay que hacer eso cada vez que se cree un control complejo (compuesto). En caso con los botones tipo CSimpleButton y CIconButton no era necesario introducir ningunas adiciones. Con el botón de división la situación es diferente porque, aparte del propio botón, este control incluye también el menú contextual que debe llegar a estar en la base de los punteros a los controles en el array personal correspondiente. El usuario final de la librería, cuyo objetivo es sólo usar su versión final sin intervenir en el código, ni siquiera va a saber cómo funciona todo eso. El objetivo principal del desarrollador de la librería consiste en facilitar al máximo el uso de la librería. Es decir, asegurar que la creación de la interfaz gráfica requiera el número mínimo de acciones.

Incluimos el archivo con la clase CSplitButton en el archivo WndContainer.mqh

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "SplitButton.mqh"
A continuación, hay que declarar e implementar el método para agregar el puntero al menú contextual del botón de división en el array privado que ha sido creado previamente:
//+------------------------------------------------------------------+
//| Clase para almacenar todos los objetos de la interfaz                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
private:
  //--- Guarda los punteros a los elementos del botón de división en la base
   bool              AddSplitButtonElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Guarda los punteros a los objetos del botón de división en la base          |
//+------------------------------------------------------------------+
bool CWndContainer::AddSplitButtonElements(const int window_index,CElement &object)
  {
//--- Salimos si no es el botón de división
   if(object.ClassName()!="CSplitButton")
      return(false);
//--- Obtenemos el puntero al botón de división
   CSplitButton *sb=::GetPointer(object);
 //--- Aumentar el array de elementos
   int size=::ArraySize(m_wnd[window_index].m_elements);
   ::ArrayResize(m_wnd[window_index].m_elements,size+1);
//--- Obtenemos el puntero del menú contextual
   CContextMenu *cm=sb.GetContextMenuPointer();
//--- Guardamos el elemento y objetos en la base
   m_wnd[window_index].m_elements[size]=cm;
   AddToObjectsArray(window_index,cm);
//--- Guardamos los punteros a sus objetos en la base
   int items_total=cm.ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
       //--- Aumentar el array de controles
      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=cm.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 privado
   AddToRefArray(cm,m_wnd[window_index].m_context_menus);
   return(true);
  }

Recordaré que la llamada a los métodos como CWndContainer::AddSplitButtonElements() debe realizarse en el método CWndContainer::AddToElementsArray(), tal como se muestra en la versión reducida de este método en el código de abajo:

//+------------------------------------------------------------------+
//| 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
//--- Guarda los punteros a los objetos del botón de división en la base
   if(AddSplitButtonElements(window_index,object))
      return;
  }

Todo está preparado para probar los botones de división. Vamos a crear cuatro botones de este tipo en el EA de pruebas. Declare las instancias de la clase CSplitButton y los métodos de la creación de botones con los márgenes desde el punto superior izquierdo del formulario. Colocaremos su llamada en el método principal de la creación de la interfaz gráfica del programa.

class CProgram : public CWndEvents
  {
private:
   //--- Botones de división
   CSplitButton      m_split_button1;
   CSplitButton      m_split_button2;
   CSplitButton      m_split_button3;
   CSplitButton      m_split_button4;
   //---
private:
   //--- Botones de división
#define SPLITBUTTON1_GAP_X       (7)
#define SPLITBUTTON1_GAP_Y       (225)
   bool              CreateSplitButton1(const string text);
#define SPLITBUTTON2_GAP_X       (128)
#define SPLITBUTTON2_GAP_Y       (225)
   bool              CreateSplitButton2(const string text);
#define SPLITBUTTON3_GAP_X       (7)
#define SPLITBUTTON3_GAP_Y       (250)
   bool              CreateSplitButton3(const string text);
#define SPLITBUTTON4_GAP_X       (128)
#define SPLITBUTTON4_GAP_Y       (250)
   bool              CreateSplitButton4(const string text);
  };
//+------------------------------------------------------------------+
//| Crea el panel de trading                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creación del formulario para los controles
//--- Creación de controles:
//    Menú principal
//--- Menús contextuales
//--- Botones simples
//--- Botón con imagen
//--- Botones de división
   if(!CreateSplitButton1("Split Button 1"))
      return(false);
   if(!CreateSplitButton2("Split Button 2"))
      return(false);
   if(!CreateSplitButton3("Split Button 3"))
      return(false);
   if(!CreateSplitButton4("Split Button 4"))
      return(false);
 //--- Redibujar el gráfico
   m_chart.Redraw();
   return(true);
  }

Como ejemplo, mostraremos el código de uno de ellos (véase el código de abajo). Atención: para establecer las propiedades del menú contextual, primero hay que obtener el puntero a él a través del método CSplitButton::GetContextMenuPointer().

//+------------------------------------------------------------------+
//| Crea el botón de división 1                                       |
//+------------------------------------------------------------------+
bool CProgram::CreateSplitButton1(const string button_text)
  {
//--- Tres elementos en el menú contextual
#define CONTEXTMENU_ITEMS5 3
//--- Pasar el objeto del panel
   m_split_button1.WindowPointer(m_window);
//--- Coordenadas
   int x=m_window.X()+SPLITBUTTON1_GAP_X;
   int y=m_window.Y()+SPLITBUTTON1_GAP_Y;
//--- Arrays de los nombres de los elementos
   string items_text[]=
     {
      "Item 1",
      "Item 2",
      "Item 3"
     };
//--- Array de los iconos para el modo disponible
   string items_bmp_on[]=
     {
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\coins.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\line_chart.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart.bmp"
     };
//--- Array de los iconos para el modo bloqueado
   string items_bmp_off[]=
     {
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\coins_colorless.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\line_chart_colorless.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart_colorless.bmp"
     };
//--- Establecemos las propiedades antes de la creación
   m_split_button1.ButtonXSize(116);
   m_split_button1.ButtonYSize(22);
   m_split_button1.DropButtonXSize(16);
   m_split_button1.LabelColor(clrBlack);
   m_split_button1.LabelColorPressed(clrBlack);
   m_split_button1.BackColor(clrGainsboro);
   m_split_button1.BackColorHover(C'193,218,255');
   m_split_button1.BackColorPressed(C'190,190,200');
   m_split_button1.BorderColor(C'150,170,180');
   m_split_button1.BorderColorOff(C'178,195,207');
   m_split_button1.BorderColorHover(C'150,170,180');
   m_split_button1.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp");
   m_split_button1.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp16\\script_colorless.bmp");
//--- Obtenemos el puntero al menú contextual del botón
   CContextMenu *cm=m_split_button1.GetContextMenuPointer();
//--- Establecemos las propiedades para el menú contextual
   cm.AreaBackColor(C'240,240,240');
   cm.AreaBorderColor(clrSilver);
   cm.ItemBackColor(C'240,240,240');
   cm.ItemBorderColor(C'240,240,240');
   cm.LabelColor(clrBlack);
   cm.LabelColorHover(clrWhite);
   cm.SeparateLineDarkColor(C'160,160,160');
   cm.SeparateLineLightColor(clrWhite);
//--- Añadimos los elementos en el menú contextual
   for(int i=0; i<CONTEXTMENU_ITEMS5; i++)
      m_split_button1.AddItem(items_text[i],items_bmp_on[i],items_bmp_off[i]);
//--- Línea separadora tras el primer elemento del menú
   m_split_button1.AddSeparateLine(1);
//--- Creamos el control
   if(!m_split_button1.CreateSplitButton(m_chart_id,button_text,m_subwin,x,y))
      return(false);
//--- Añadimos el puntero al control a la base
   CWndContainer::AddToElementsArray(0,m_split_button1);
   return(true);
  }

 

Después de compilar los archivos e iniciar el programa en el gráfico, verá aproximadamente el siguiente resultado:

Fig. 6. Prueba del control “Botón de divissión”.

Fig. 6. Prueba del control “Botón de divissión”.

 

Hemos terminado el desarrollo de la clase para la creación del “Botón de división”. Usted puede descargar la versión del EA representado en la captura de arriba usando los archivos adjuntos. En el siguiente artículo vamos a considerar el desarrollo de las clases para la creación de los grupos de botones, es decir los botones interconectados entre sí. 

 


Conclusión

Este capítulo estaba dedicado a la creación de los botones simples y multifuncionales. En el siguiente artículo, completaremos nuestra librería con las clases para la creación de los grupos de botones.

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 para realizar las pruebas en los terminales  MetaTrader. 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 tercera parte: