Interfaces gráficas I: Formulario para controles (Capítulo 2)

Anatoli Kazharski | 15 febrero, 2016

Índice

 

Introducción

Este artículo es la continuación de la primera parte de la serie sobre las interfaces gráficas. El artículo Interfaces gráficas I: Preparación de la estructura de la librería (Capítulo 1) cuenta con más detalles para qué sirve esta librería. Al final de cada capítulo se puede encontrar la lista completa de los enlaces a los artículos de la primera parte, así como descargar la versión completa de la librería en la fase actual del desarrollo del proyecto. Es necesario colocar los ficheros en los mismos directorios, tal como están ubicados en el archivo.

En el capítulo anterior hemos hablado la estructura de la librería para la creación de las interfaces gráficas. Hemos creado (1) las clases para los objetos primitivos, (2) la clase base para todos los controles, (3) las clases principales para almacenar los punteros a los controles y manejar estos controles en el manejador común de eventos.

En este artículo vamos a crear el primero y el más importante elemento de las interfaces gráficas: formulario para los controles. A este formulario se le puede adjuntar múltiples controles de diferentes tipos en cualquier orden y secuencia. El formulario será movible, y todos los controles adjuntos a él van a desplazarse de la misma manera.

 

Clase del formulario para los controles

Recordaré que antes, en la descripción de la clase CElement, ya hemos hablado de algunos métodos virtuales que serán únicos para cada control. Vamos a colocar sus duplicados en la clase CWindow, y haremos lo mismo en cada siguiente descripción de cualquier otro control. El código de estos métodos será considerado por nosotros después de que hayamos creado los métodos para la construcción del formulario (ventana) para los controles.

//+------------------------------------------------------------------+
//|                                                       Window.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
//+------------------------------------------------------------------+
//| Clase de creación del formulario para los controles              |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
public:
                     CWindow(void);
                    ~CWindow(void);
   //--- 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);
   //--- Mostrar, ocultar, resetear, eliminar
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- Establecer, poner a cero las prioridades para el clic izquierdo del ratón
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWindow::CWindow(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWindow::~CWindow(void)
  {
  }
//+------------------------------------------------------------------+

En la clase CWindow, así como en algunas otras clases, van a aplicarse muchos modos de diferentes tipos a unas u otras acciones. Las listas de los identificadores de las constantes nombradas para todos los modos y tipos de datos van a almacenarse en el archivo separado Enums.mqh. Lo incluiremos en el archivo Objects.mqh para que todas las enumeraciones de los modos y tipos estén disponibles en todos los archivos de la librería:

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Enums.mqh"
#include "Defines.mqh"
#include <ChartObjects\ChartObjectsBmpControls.mqh>
#include <ChartObjects\ChartObjectsTxtControls.mqh>

¿De qué partes se compone la ventana que vamos a crear?

  1. Fondo. Todos los controles van a colocarse en esta área.
  2. Encabezado. Esta parte sirve para mover la ventana, además de contener elementos de la interfaz que se listan más abajo.
  3. Icono. Atributo adicional para la identificación visual.
  4. Título. Nombre de la ventana.
  5. Botón “Ayudas emergentes”. Si está apretado, está habilitado el modo de visualización de ayudas emergentes para los controles en los que están presentes.
  6. Botón para minimizar/maximizar la ventana.
  7. Botón para cerrar la ventana.

Fig. 1. Partes del formulario para los controles

Fig. 1. Partes del formulario para los controles

En la librería para las interfaces gráficas que estamos desarrollando, será implementado el modo de ventanas múltiples. La aplicación puede componerse también de una sola ventana. Pero a veces, si el programa dispone de muchas opciones y no existe la posibilidad de meterlas en un solo formulario, se requiere un formulario más. Puede contener algunos ajustes comunes de la aplicación, manual de usuario o ser una ventana para abrir/guardar el archivo, e incluso una paleta de selección de colores. En total, puede surgir una gran cantidad de diferentes ideas para crear las ventanas adicionales. Suelen llamarlas “cuadros de diálogo”.

En la clase CWindow hay que tener la posibilidad de determinar el tipo de la ventana, es decir, qué ventana es la principal de la aplicación, y cuál es de diálogo. Puede que en el proceso del desarrollo surjan otras otras ideas con nuevos tipos de las ventanas, pero ahora vamos a crear en el archivo Enums.mqh una enumeración que va a contener sólo dos tipos para (1) la ventana principal y (2) la ventana de diálogo:

//+------------------------------------------------------------------+
//| Enumeración de tipos de ventanas                                 |
//+------------------------------------------------------------------+
enum ENUM_WINDOW_TYPE
  {
   W_MAIN   =0,
   W_DIALOG =1
  };

El fondo y encabezado serán creados desde el objeto primitivo “Etiqueta rectangular”. Para eso tenemos la clase CRectLabel. Para el título vamos a usar la clase CLabel que sirve para crear el objeto “Etiqueta de texto”. Para el icono de la ventana y los botones mencionados antes hace falta el objeto “Etiqueta gráfica”, y para eso ya tenemos preparada la clase CBmpLabel.

Si el archivo Element.mqh ya está incluido, todas estas clases definidas en el archivo Object.mqh están disponibles para el uso. Vamos a crear sus instancias para cada control de la ventana en el cuerpo de la clase CWindow, así como todos los métodos necesarios para el diseño de la ventana:

class CWindow : public CElement
  {
private:
   //--- Objetos para crear el formulario
   CRectLabel        m_bg;
   CRectLabel        m_caption_bg;
   CBmpLabel         m_icon;
   CLabel            m_label;
   CBmpLabel         m_button_tooltip;
   CBmpLabel         m_button_unroll;
   CBmpLabel         m_button_rollup;
   CBmpLabel         m_button_close;
   //---
public:
   //--- Métodos para crear la ventana
   bool              CreateWindow(const long chart_id,const int window,const string caption_text,const int x,const int y);
   //---
private:
   bool              CreateBackground(void);
   bool              CreateCaption(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   bool              CreateButtonClose(void);
   bool              CreateButtonRollUp(void);
   bool              CreateButtonUnroll(void);
   bool              CreateButtonTooltip(void);
  };

Antes de empezar a llenar estos métodos, es necesario preparar alguna funcionalidad en la clase CWndContainer donde los arrays van a almacenar los punteros a todos los controles de la interfaz y a sus partes integrantes. Además, tenemos que hacer el siguiente paso en el desarrollo de la estructura de la librería y preparar el programa para las pruebas.

Incluimos el archivo Window.mqh con la clase CWindow en el archivo WndContainer.mqh con la clase CWndContainer. Aquí mismo vamos a incluir todas las demás clases con los controles.

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Window.mqh"
// ...
// Aquí mismo vamos a incluir todas las demás clases con los controles
// ...

De esta manera, estarán disponibles para el uso en la clase CProgram, es decir, en la clase donde va a crearse la interfaz de la aplicación MQL, mientras que en las clases CWndContainer y CWndEvents habrá la posibilidad de crear y usar los miembros de la clase con estos tipos de datos. Por ejemplo, si después de la inclusión del archivo Window.mqh se crea un array dinámico de punteros del tipo CWindow (lo vamos a necesitar para nuestro trabajo), no habrá ningún problema, o sea, los mensajes de errores no aparecerán.

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Clase para almacenar todos los objetos de la interfaz            |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Array de ventanas
   CWindow          *m_windows[];
  };

Pero si el archivo Window.mqh no estuviera incluido, entonces durante la creación del miembro de la clase con el tipo CWindow y el intento de compilar el archivo, saltaría un mensaje del error mostrado en la captura de pantalla a continuación:

Fig. 2. Mensaje de compilación que el tipo especificado no se encuentra

Fig. 2. Mensaje de compilación que el tipo especificado no se encuentra

Además del array dinámico de las ventanas vamos a necesitar una estructura de los arrays dinámicos de los punteros a todos los controles (CElement) y arrays de todos los objetos integrantes (CChartObject). Como instancia de esta estructura, vamos a crear un array dinámico con el tamaño agual al array de ventanas (m_window[]). Como resultado, obtenemos el array de punteros a los controles para cada formulario (véase el código de abajo).

Por cierto, el array m_objects[] está declarado con el tipo CChartObject. Durante la compilación, no habrá ningún error porque este tipo de objetos ya está presente en la estructura de la librería y está incluido en el archivo Objects.mqh.

class CWndContainer
  {
protected:
   //--- Estructura de los arrays de controles
   struct WindowElements
     {
      //--- Array común de todos los objetos
      CChartObject     *m_objects[];
      //--- Array común de todos los controles
      CElement         *m_elements[];
     };
   //--- Array de los arrays de los controles para cada ventana
   WindowElements    m_wnd[];
  };

No es una lista completa de los arrays dinámicos de la estructura WindowElements. A la medida de crear otros controles, esta lista va a actualizarse.

Para obtener los tamaños de los arrays, hacen falta los métodos correspondientes. El número de objetos de todos los controles y el número de controles de cada ventana se puede obtener al pasar el índice de la ventana (window_index) como parámetro.

class CWndContainer
  {
public:
   //--- Número de ventanas en la interfaz
   int               WindowsTotal(void) { return(::ArraySize(m_windows)); }
   //--- Número de objetos de todos los controles
   int               ObjectsElementsTotal(const int window_index);
   //--- Número de controles
   int               ElementsTotal(const int window_index);
  };

Tenemos que realizar la prueba de la superación del tamaño del array. Las frases repetidas frecuentemente pueden ser escritas en forma de una macro, añadiendo al principio una macro sustitución predefinida que contenga el nombre de la función (__FUNCTION__). Vamos a escribir la macro (PREVENTING_OUT_OF_RANGE) e insertarla en el archivo Defines.mqh:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Prevención de superar el rango
#define PREVENTING_OUT_OF_RANGE __FUNCTION__," > Prevención de superar el tamaño del array."

Ahora va a ser cómodo usarla en todas las funciones donde se comprueba el exceso del tamaño del array:

//+----------------------------------------------------------------------------+
//| Devuelve el número de objetos según el índice especificado de la ventana   |
//+----------------------------------------------------------------------------+
int CWndContainer::ObjectsElementsTotal(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_objects));
  }
//+----------------------------------------------------------------------------+
//| Devuelve el número de controles según el índice especificado de la ventana |
//+----------------------------------------------------------------------------+
int CWndContainer::ElementsTotal(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_elements));
  }

Imaginémonos que somos el usuario de esta librería y pasemos a la clase CProgram del EA que ya hemos creado para las pruebas. Supongamos que hay que hacer un panel de trading. Por eso vamos a crear el método CreateTradePanel() en la clase CProgram. Por favor, no olvide que con el propósito de ahorrar el espacio del artículo, estamos completando lo que ha sido hecho antes. Y según vamos avanzando, en el articulo discutimos lo que añadimos a una u otra clase.

class CProgram : public CWndEvents
  {
public:
   //--- Crea el panel de trading
   bool              CreateTradePanel(void);
  };
//+------------------------------------------------------------------+
//| Crea el panel de trading                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creación del formulario para los controles
// ...
//--- Creación de controles
// ...
//---
   ::ChartRedraw();
   return(true);
  }

Por ahora, el cuerpo de este método está vacío, pero muy pronto se llenará con la funcionalidad necesaria. De los comentarios en el cuerpo del método queda claro que en primer lugar hay que crear la ventana para los controles. Necesitamos un método donde vamos a especificar las propiedades de la ventana creada. Como antes ya hemos incluido el archivo Window.mqh en el archivo WndContainer.mqh, ahora la clase CWindow está disponible en la clase CProgram. Por consiguiente, vamos a crear la instancia de esta clase, así como el método de creación de la ventana para los controles CreateWindow():

class CProgram : public CWndEvents
  {
private:
   //--- Formulario para controles
   CWindow           m_window;
   //---
private:
   //--- Creación del formulario
   bool              CreateWindow(const string caption_text);
  };

Hemos llegado al momento cuando hay que responder a las siguientes preguntas:

  1. ¿De qué manera los punteros a los controles y sus objetos van a llegar a los arrays que hemos creado antes en la clase CWndContainer?
  2. ¿Cómo va a definirse el identificador de cada elemento de la interfaz?

Puesto que la creación de una interfaz requiere una determinada (estricta) secuencia, es necesario implementar el proceso del desarrollo de la interfaz de tal manera que, en caso de violación casual del orden de la secuencia, el desarrollador no se encuentre en la situación cuando no entienda qué debe hacer en adelante. Por eso cada acción del usuario de la librería será controlada por el “motor” de la librería. Hagamos que sea imposible crear el formulario para los controles hasta que el puntero a él no se encuentre en la base de los punteros en la clase CWndContainer. Será imposible crear un control hasta que en su clase no se quede guardado el puntero al formulario al que se adjunta.

Vamos a crear el método AddWindow() en la clase CWndContainer para añadir el puntero de la ventana en la base de los controles de la interfaz. Aquí también va a realizarse (1) el almacenamiento del puntero en el array de controles, (2) almacenamiento de los punteros de objetos del control en el array común de los punteros de objetos, para lo que vamos a necesitar el método AddToObjectsArray(), así como (3) va a establecerse el identificador. Además, (4) hay que guardar el último identificador en las propiedades de la ventana, porque será necesario para definir los indicadores para cada control dentro de sus clases. Como ya se ha dicho en el párrafo anterior, eso será posible porque cada control tendrá el puntero a la ventana a la que está adjuntado.

Empecemos con la creación de los métodos LastId() en la clase CWindow para guardar y obtener el identificador del último control creado de la interfaz:

class CWindow : public CElement
  {
private:
   //--- Identificador del último control
   int               m_last_id;
   //---
public:
   //--- Métodos para guardar y obtener ID del último control creado
   void              LastId(const int id)                                    { m_last_id=id;                       }
   int               LastId(void)                                      const { return(m_last_id);                  }
  };

Luego vamos a crear el resto de los métodos en la clase CWndContainer que van a utilizarse en el método AddWindow(). Cada control tendrá el nombre único de la clase, por eso el método AddToObjectsArray() será de plantilla (template) ya que en él van a llegar los objetos de diferentes controles. En este método vamos a iterar cíclicamente el array de los objetos del control, añadiendo por turno el puntero de cada uno en el array de la base. Para eso vamos a necesitar un método más. Vamos a llamarlo AddToArray(). A este método se pasarán los objetos de un solo tipo (CChartObject), por eso no hace falta hacerlo de plantilla.

class CWndContainer
  {
protected:
   //--- Añadir los punteros de objetos del control en array común
   template<typename T>
   void              AddToObjectsArray(const int window_index,T &object);
   //--- Añade el puntero del objeto en el array
   void              AddToArray(const int window_index,CChartObject &object);
  };
//+------------------------------------------------------------------+
//| Añade los punteros de objetos del control en array común         |
//+------------------------------------------------------------------+
template<typename T>
void CWndContainer::AddToObjectsArray(const int window_index,T &object)
  {
   int total=object.ObjectsElementTotal();
   for(int i=0; i<total; i++)
      AddToArray(window_index,object.Object(i));
  }
//+------------------------------------------------------------------+
//| Añade el puntero del objeto en el array                             |
//+------------------------------------------------------------------+
void CWndContainer::AddToArray(const int window_index,CChartObject &object)
  {
   int size=::ArraySize(m_wnd[window_index].m_objects);
   ::ArrayResize(m_wnd[window_index].m_objects,size+1);
   m_wnd[window_index].m_objects[size]=::GetPointer(object);
  }

Ahora podemos crear el método AddWindow(). Aquí nos hará falta también el contador de controles (m_counter_element_id). Hay que aumentar el valor de esta variable tras la adición de un control en la base.

class CWndContainer
  {
private:
   //--- Contador de elementos
   int               m_counter_element_id;
   //---
protected:
   //--- Añade el puntero de la ventana en la base de los controles de la interfaz
   void              AddWindow(CWindow &object);
  };
//+---------------------------------------------------------------------------+
//| Añade el puntero de la ventana en la base de los controles de la interfaz |
//+---------------------------------------------------------------------------+
void CWndContainer::AddWindow(CWindow &object)
  {
   int windows_total=::ArraySize(m_windows);
//--- Si no hay ventanas, ponemos el contador de controles a cero
   if(windows_total<1)
      m_counter_element_id=0;
//--- Añadimos el puntero en el array de ventanas
   ::ArrayResize(m_wnd,windows_total+1);
   ::ArrayResize(m_windows,windows_total+1);
   m_windows[windows_total]=::GetPointer(object);
//--- Añadimos el puntero en el array común de controles
   int elements_total=::ArraySize(m_wnd[windows_total].m_elements);
   ::ArrayResize(m_wnd[windows_total].m_elements,elements_total+1);
   m_wnd[windows_total].m_elements[elements_total]=::GetPointer(object);
 //--- Añadir los objetos del control en array común de objetos
   AddToObjectsArray(windows_total,object);
//--- Establecemos el identificador y recordamos el ID del último control
   m_windows[windows_total].Id(m_counter_element_id);
   m_windows[windows_total].LastId(m_counter_element_id);
//--- Aumentamos el contador de identificadores de controles
   m_counter_element_id++;
  }

Ahora, cada vez cuando se cree una nueva ventana en la clase personalizada del desarrollador de la aplicación MQL (en el artículo es CProgram), hay que añadirla en la base usando el método AddWindow().

Luego, los métodos para la creación de la ventana declarados anteriormente tienen que ser implementados en la clase CWindow. Para eso vamos a necesitar las variables adicionales y los métodos relacionados con el tipo, apariencia de la ventana y con los modos en los que puede encontrarse la ventana según la instrucción del usuario, o en función del tipo del programa MQL. Vamos a especificarlos abajo acompañando con una breve descripción:

  1. Métodos para establecer y obtener el estatus de la ventana (minimizado/maximizado).
  2. Métodos para establecer y obtener el tipo de la ventana (principal/de diálogo).
  3. Métodos para establecer el modo de minimizar la ventana tomando en cuenta el tipo del programa MQL a crear (Asesor Experto, indicador). Para eso vamos a necesitar los métodos que nos permitan manejar el tamaño de la ventana si se trata del indicador que se encuentra fuera de la ventana principal del gráfico.
  4. Métodos para establecer los colores de cada objeto de la ventana por el usuario.
  5. Método para establecer el icono de la ventana.
  6. Método para definir el icono de la ventana por defecto si el usuario no lo ha especificado.
  7. Identificación de los límites del área de la captura en el encabezado de la ventana.
  8. Constantes para las sangrías de los botones desde el borde derecho de la ventana.
  9. Visualización del botón para habilitar el modo de ayudas emergentes.
  10. Variables de las prioridades para el clic izquierdo del ratón para cada objeto de la ventana.

Añadimos todo lo mencionado arriba en la clase CWindow. Al principio del archivo hay que añadir las constantes para las sangrías de los botones desde el borde derecho del formulario:

//+------------------------------------------------------------------+
//|                                                       Window.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
//--- Sangrías para los botones desde el borde derecho de la ventana
#define CLOSE_BUTTON_OFFSET   (20)
#define ROLL_BUTTON_OFFSET    (36)
#define TOOLTIP_BUTTON_OFFSET (53)

Creamos las variables y métodos mencionados en el cuerpo de la clase:

//+------------------------------------------------------------------+
//| Clase de creación del formulario para los controles              |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
private:
   //--- Identificador del último control
   int               m_last_id;
   //--- Estatus de la ventana minimizada
   bool              m_is_minimized;
   //--- Tipo de la ventana
   ENUM_WINDOW_TYPE  m_window_type;
   //--- Modo del alto fijo de la subventana (para los indicadores)
   bool              m_height_subwindow_mode;
   //--- Modo de minimizar el formulario en la subventana del indicador
   bool              m_rollup_subwindow_mode;
   //--- Alto de la subventana del indicador
   int               m_subwindow_height;
   //--- Propiedades del fondo
   int               m_bg_zorder;
   color             m_bg_color;
   int               m_bg_full_height;
   //--- Propiedades del encabezado
   int               m_caption_zorder;
   string            m_caption_text;
   int               m_caption_height;
   color             m_caption_bg_color;
   color             m_caption_bg_color_hover;
   color             m_caption_bg_color_off;
   color             m_caption_color_bg_array[];
   //--- Propiedades de botones
   int               m_button_zorder;
   //--- Color de los marcos del formulario (fondo, encabezado)
   color             m_border_color;
   //--- Icono del formulario
   string            m_icon_file;
   //--- Presencia del botón para el modo de ayudas emergentes
   bool              m_tooltips_button;
   //--- Para la identificación de los límites del área de la captura en el encabezado de la ventana
   int               m_right_limit;
   //---
public:
   //--- Tipo de la ventana
   ENUM_WINDOW_TYPE  WindowType(void)                                  const { return(m_window_type);              }
   void              WindowType(const ENUM_WINDOW_TYPE flag)                 { m_window_type=flag;                 }
   //--- Icono por defecto
   string            DefaultIcon(void);
   //--- (1) icono personalizado de la ventana, (2) usar el botón de ayudas, (3) limitar el área de captura del encabezado
   void              IconFile(const string file_path)                        { m_icon_file=file_path;              }
   void              UseTooltipsButton(void)                                 { m_tooltips_button=true;             }
   void              RightLimit(const int value)                             { m_right_limit=value;                }
   //--- Estatus de la ventana minimizada
   bool              IsMinimized(void)                                 const { return(m_is_minimized);             }
   void              IsMinimized(const bool flag)                            { m_is_minimized=flag;                }
   //--- Propiedades del encabezado
   void              CaptionText(const string text);
   string            CaptionText(void)                                 const { return(m_caption_text);             }
   void              CaptionHeight(const int height)                         { m_caption_height=height;            }
   int               CaptionHeight(void)                               const { return(m_caption_height);           }
   void              CaptionBgColor(const color clr)                         { m_caption_bg_color=clr;             }
   color             CaptionBgColor(void)                              const { return(m_caption_bg_color);         }
   void              CaptionBgColorHover(const color clr)                    { m_caption_bg_color_hover=clr;       }
   color             CaptionBgColorHover(void)                         const { return(m_caption_bg_color_hover);   }
   void              CaptionBgColorOff(const color clr)                      { m_caption_bg_color_off=clr;         }
   //--- Propiedades de la ventana
   void              WindowBgColor(const color clr)                          { m_bg_color=clr;                     }
   color             WindowBgColor(void)                                     { return(m_bg_color);                 }
   void              WindowBorderColor(const color clr)                      { m_border_color=clr;                 }
   color             WindowBorderColor(void)                                 { return(m_border_color);             }
   //--- Modos de la subventana del indicador
   void              RollUpSubwindowMode(const bool flag,const bool height_mode);
   //--- Cambio del alto de la subventana del indicador
   void              ChangeSubwindowHeight(const int height);
  };

Inicialización de las variables en el constructor de la clase usando los valores por defecto. En el cuerpo del constructor, también se almacena el nombre de la clase del control de la interfaz y se establece la secuencia estricta de las prioridades para el clic izquierdo del ratón.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWindow::CWindow(void) : m_last_id(0),
                         m_subwindow_height(0),
                         m_rollup_subwindow_mode(false),
                         m_height_subwindow_mode(false),
                         m_is_minimized(false),
                         m_tooltips_button(false),
                         m_window_type(W_MAIN),
                         m_icon_file(""),
                         m_right_limit(20),
                         m_caption_height(20),
                         m_caption_bg_color(C'88,157,255'),
                         m_caption_bg_color_off(clrSilver),
                         m_caption_bg_color_hover(C'118,177,255'),
                         m_bg_color(C'15,15,15'),
                         m_border_color(clrLightGray)

  {
//--- Guardamos el nombre de la clase del control en la clase base
   CElement::ClassName(CLASS_NAME);
//--- Establecemos la secuencia estricta de prioridades
   m_bg_zorder      =0;
   m_caption_zorder =1;
   m_button_zorder  =2;
  }

La implementación de los métodos resaltados en amarillo en el código más arriba se muestra a continuación. Si en la clase hacen falta las variables y los métodos de la clase base, es mejor usarlos a través de doble dos puntos, indicando al principio el nombre de la clase (ClassName::) donde están declarados. Así, el código se hace más comprensible para estudiar, y recordar más rápido qué y de dónde viene si llevas tiempo sin echar un vistazo a él. El valor de la variable m_subwindow_height va a determinarse automáticamente en el momento de la creación de la ventana.

//+------------------------------------------------------------------+
//| Modo de minimizar la subventana del indicador                    |
//+------------------------------------------------------------------+
void CWindow::RollUpSubwindowMode(const bool rollup_mode=false,const bool height_mode=false)
  {
   if(CElement::m_program_type!=PROGRAM_INDICATOR)
      return;
//---
   m_rollup_subwindow_mode =rollup_mode;
   m_height_subwindow_mode =height_mode;
//---
   if(m_height_subwindow_mode)
      ChangeSubwindowHeight(m_subwindow_height);
  }

Ya volveremos al método CWindow::RollUpSubwindowMode() en uno de los siguientes capítulos, cuando vamos a considerar los ejemplos prácticos en el gráfico del terminal. Así, tendremos más claro qué es lo que representa cada uno de los modos propuestos.

El método CWindow::ChangeSubwindowHeight() permite cambiar el alto de la subventana del indicador si el programa está configurado a este modo:

//+------------------------------------------------------------------+
//| Cambia el alto de la subventana del indicador                    |
//+------------------------------------------------------------------+
void CWindow::ChangeSubwindowHeight(const int height)
  {
   if(CElement::m_subwin<1 || CElement::m_program_type!=PROGRAM_INDICATOR)
      return;
//---
   if(height>0)
      ::IndicatorSetInteger(INDICATOR_HEIGHT,height);
  }

 

Métodos para crear el formulario

Para que todas las partes integrantes de la ventana estén visibles tras su creación, hay que establecerlas en una estricta secuencia (capa por capa):

  1. Fondo.
  2. Encabezado.
  3. Elementos del encabezado.

En el inicio del método CWindow::CreateWindow(), en el que van a crearse todas estas partes de la ventana, organizamos la prueba de que si el puntero de esta ventana ha sido guardado en la base de los controles. Si eso no ha sido hecho, el identificador de la ventana tendrá el valor WRONG_VALUE. En este caso, la ventana no será creada y la función devolverá false, mostrando antes de eso el mensaje en el registro con la descripción de las acciones necesarias. Si todo está bien, se realiza la inicialización de las variables de la clase actual y la clase base con los valores de los parámetros trasferidos. Estas variables incluyen:

El identificador del gráfico y el número de la ventana del gráfico se definen en el constructor de la clase CWndEvents. Cuando se crean los controles en la clase personalizada (CWndEvents::CProgram), precisamente estos valores se pasan en los métodos de su creación.

Luego, siguen los métodos para crear todos los objetos del formulario. Si algún objeto no se crea, la función devuelve false. Después de la instalación de todos los objetos, dependiendo del tipo del programa y modo establecido se realiza la prueba comprobando si hace falta fijar el tamaño de la ventana (si es el indicador en una ventana separada del gráfico). Antes he mencionado la variable m_subwindow_height que se inicializa cuando se crea la ventana. Precisamente aquí eso ocurre. En el código de abajo se resalta con amarillo.

Y por último, si se establece que es una ventana de diálogo, es necesario ocultarla. Aquí hay que dar ciertas explicaciones. Si la interfaz del programa va a tener más de una ventana, todas estas ventanas van a adjuntarse al gráfico durante el inicio del programa. Entonces, cuando el usuario llama una u otra ventana de diálogo, no será necesario crear y eliminar constantemente las ventanas de diálogo. Ya se encuentran en el gráfico, simplemente permanecerán en el modo oculto. Cuando se llama una ventana, ésta se queda visible; y cuando se cierra, va a ocultarse pero no eliminarse.

Todo de lo que hemos hablado antes, queda reflejado en el código de abajo:

//+------------------------------------------------------------------+
//| Crea el formulario para los controles                            |
//+------------------------------------------------------------------+
bool CWindow::CreateWindow(const long chart_id,const int subwin,const string caption_text,const int x,const int y)
  {
   if(CElement::Id()==WRONG_VALUE)
     {
      ::Print(__FUNCTION__," > Antes de crear la ventana, hay que pasar su puntero "
            "al array de ventanas usando el método CWndContainer::AddWindow(CWindow &object).");
      return(false);
     }
//--- Inicialización de variables
   m_chart_id       =chart_id;
   m_subwin         =subwin;
   m_caption_text   =caption_text;
   m_x              =x;
   m_y              =y;
   m_bg_full_height =m_y_size;
//--- Creación de todos los objetos de la ventana
   if(!CreateBackground())
      return(false);
   if(!CreateCaption())
      return(false);
   if(!CreateLabel())
      return(false);
   if(!CreateIcon())
      return(false);
   if(!CreateButtonClose())
      return(false);
   if(!CreateButtonRollUp())
      return(false);
   if(!CreateButtonUnroll())
      return(false);
   if(!CreateButtonTooltip())
      return(false);
//--- Si este programa es un indicador
   if(CElement::ProgramType()==PROGRAM_INDICATOR)
     {
       //--- Si está establecido el modo del alto fijo de la subventana
      if(m_height_subwindow_mode)
        {
         m_subwindow_height=m_bg_full_height+3;
         ChangeSubwindowHeight(m_subwindow_height);
        }
     }
//--- Ocultar la ventana si es de diálogo
   if(m_window_type==W_DIALOG)
      Hide();
//---
   return(true);
  }

Para ocultar los objetos del control, se utiliza la función virtual Hide(). En el código anterior, se resalta con el color verde. Antes, en la clase CWindow, ha sido mostrada sólo su declaración. En su código, todos los objetos de la ventana se ocultan en todos los períodos de tiempo, y la variable m_is_visible de la clase base recibe el estatus false.

//+------------------------------------------------------------------+
//| Oculta la ventana                                                |
//+------------------------------------------------------------------+
void CWindow::Hide(void)
  {
//--- Ocultar todos los objetos
   for(int i=0; i<ObjectsElementTotal(); i++)
      Object(i).Timeframes(OBJ_NO_PERIODS);
//--- Estado de visibilidad
   CElement::IsVisible(false);
  }

Si ya ha introducido todos los cambios de arriba, no apresúrese a compilar el archivo con la clase CWindow porque recibirá los mensajes de error (véase la captura de pantalla de abajo). Es que todos los métodos para crear las partes de la ventana no tienen contenido en este momento. Por eso, a continuación nos dedicaremos a su implementación.

Fig. 3. Mensajes sobre la falta de implementación de los métodos

Fig. 3. Mensajes sobre la falta de implementación de los métodos

Empezaremos con el fondo usando el método CWindow::CreateBackground(). Al principio de cada método relacionado con la creación del objeto, vamos a formar el nombre de este objeto. Va a componerse de varias partes:

Las partes irán separadas con el guión bajo «_». El nombre del programa se guarda en la clase base CElement en la lista de inicialización del constructor. El identificador del control se obtiene en el momento de añadir la ventana a la base de controles.

Luego, vamos a comprobar el tamaño del fondo de la ventana y ajustarlo en función de que si la ventana está reducida o ampliada. Aquí podemos adelantarnos un poco. La cosa es que al cambiar los períodos de tiempo o símbolos del gráfico, todos los objetos van a eliminarse en la función de deinicialización del programa, y en la función de inicialización todo va a recuperarse de acuerdo con los valores actuales de los campos (variables) de las clases.

Después de la corrección del tamaño del fondo, se realiza el ajuste del objeto, seguido del ajuste de las propiedades. Al final del método se guarda el puntero al objeto en el array de la clase base. Más abajo se muestra el código completo de la función CWindow::CreateBackground():

//+------------------------------------------------------------------+
//| Crea el fondo de la ventana                                      |
//+------------------------------------------------------------------+
bool CWindow::CreateBackground(void)
  {
//--- Formación del nombre del objeto
   string name=CElement::ProgramName()+"_window_bg_"+(string)CElement::Id();
//--- El tamaño de la ventana depende de su estado (minimizado/maximizado)
   int y_size=0;
   if(m_is_minimized)
     {
      y_size=m_caption_height;
      CElement::YSize(m_caption_height);
     }
   else
     {
      y_size=m_bg_full_height;
      CElement::YSize(m_bg_full_height);
     }
//| Establecemos el fondo de la ventana |
   if(!m_bg.Create(m_chart_id,name,m_subwin,m_x,m_y,m_x_size,y_size))
      return(false);
//--- Establecemos las propiedades
   m_bg.BackColor(m_bg_color);
   m_bg.Color(m_border_color);
   m_bg.BorderType(BORDER_FLAT);
   m_bg.Corner(m_corner);
   m_bg.Selectable(false);
   m_bg.Z_Order(m_bg_zorder);
   m_bg.Tooltip("\n");
//--- Guardamos el puntero del objeto
   CElement::AddToArray(m_bg);
   return(true);
  }

En el método de la creación del encabezado de la ventana, aparte de las acciones principales tal como eso ha sido implementado en el método de creación del fondo de la ventana, también hay que guardar las coordenadas y tamaños en la clase del objeto primitivo CRectLabel, que se encuentra en el archivo Objects.mqh. Los valores de estos campos van a actualizarse cuando la ventana se desplaza (coordenadas), así como cuando se cambian sus dimensiones. Si los valores van a ser estáticos, será imposible seguir el cursor sobre el encabezado. La misma regla se aplicará para todos los demás objetos en todos los demás controles.

Una diferencia más consiste en que el encabezado puede reaccionar al cursor del ratón cuando éste se encuentra dentro del área de la ventana o cuando el cursor sale del área de la ventana. Para eso antes en la clase CElement ha sido creado el método InitColorArray().

Código del método CElement::CreateCaption():

//+------------------------------------------------------------------+
//| Crea el encabezado de la ventana                                 |
//+------------------------------------------------------------------+
bool CWindow::CreateCaption(void)
  {
   string name=CElement::ProgramName()+"_window_caption_"+(string)CElement::Id();
//--- Establecemos el encabezado de la ventana
   if(!m_caption_bg.Create(m_chart_id,name,m_subwin,m_x,m_y,m_x_size,m_caption_height))
      return(false);
//--- Establecemos las propiedades
   m_caption_bg.BackColor(m_caption_bg_color);
   m_caption_bg.Color(m_border_color);
   m_caption_bg.BorderType(BORDER_FLAT);
   m_caption_bg.Corner(m_corner);
   m_caption_bg.Selectable(false);
   m_caption_bg.Z_Order(m_caption_zorder);
   m_caption_bg.Tooltip("\n");
//--- Guardamos las coordenadas
   m_caption_bg.X(m_x);
   m_caption_bg.Y(m_y);
//--- Guardamos los tamaños (en el objeto)
   m_caption_bg.XSize(m_caption_bg.X_Size());
   m_caption_bg.YSize(m_caption_bg.Y_Size());
//--- Inicialización del array del gradiente
   CElement::InitColorArray(m_caption_bg_color,m_caption_bg_color_hover,m_caption_color_bg_array);
//--- Guardamos el puntero del objeto
   CElement::AddToArray(m_caption_bg);
   return(true);
  }

El icono del programa en la ventana principal de la interfaz va a determinarse por defecto con las imágenes previamente preparadas. Cuál de ellas va a mostrarse dependerá del tipo del programa desarrollado (ENUM_PROGRAM_TYPE). Al final del artículo se puede descargar todos los iconos. Para usar en los programas, los iconos que no pertenecen a los controles interactivos tienen que ubicarse en el directorio <carpeta_de_datos>\MQLX\Images\EasyAndFastGUI\Icons\bmp16. El nombre bmp16 significa que la carpeta contiene las imágenes del tamaño 16x16 píxeles. Por ejemplo, si en su colección de imágenes para la librería hay iconos del tamaño 24x24, la carpeta llevará el nombre bmp24 , etc.

Las imágenes estándar que se utilizan por defecto para crear los controles tienen que ubicarse en el directorio <carpeta_de_datos>\MQLX\Images\EasyAndFastGUI\Controls.

Para identificar automáticamente el icono de la ventana principal del programa, en la clase CWindow ya ha sido declarado el método DefaultIcon(). Abajo se muestra el código de este método:

//+------------------------------------------------------------------+
//| Determinar el icono por defecto                                  |
//+------------------------------------------------------------------+
string CWindow::DefaultIcon(void)
  {
   string path="Images\\EasyAndFastGUI\\Icons\\bmp16\\advisor.bmp";
//---
   switch(CElement::ProgramType())
     {
      case PROGRAM_SCRIPT:
        {
         path="Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp";
         break;
        }
      case PROGRAM_EXPERT:
        {
         path="Images\\EasyAndFastGUI\\Icons\\bmp16\\advisor.bmp";
         break;
        }
      case PROGRAM_INDICATOR:
        {
         path="Images\\EasyAndFastGUI\\Icons\\bmp16\\indicator.bmp";
         break;
        }
     }
//---
   return(path);
  }

Cabe mencionar que en los scripts se podrá usar sólo los controles de la interfaz que están relacionados con la visualización de datos. Eso se debe al hecho de que en los scripts no hay posibilidad de usar la función para el seguimiento de eventos.

Si hay que redefinir una imagen para la ventana, para eso se usa el método CWindow::IconFile(). Los ejemplos para éste y para otros métodos serán proporcionados cuando vamos a probar las interfaces a base de esta librería en el gráfico en el terminal.

Los recursos con las imágenes para diferentes controles serán incluidos (#resource) junto al método (fuera del cuerpo del método) en el que van a utilizarse (marcado con amarillo). El icono siempre va a tener un solo estado (una imagen), por eso en los métodos del objeto primitivo CChartObjectBmpLabel::BmpFileOn() y CChartObjectBmpLabel::BmpFileOff() se indicará la ruta sólo hacia una versión.

Todas las coordenadas de los objetos (salvo el encabezado de la ventana) de todos los controles de cada determinada ventana se calculan respecto a las coordenadas (esquina superior izquierda) de esta ventana en las variables locales del método. Por eso todas las sangrías de los objetos respecto a las coordenadas de la ventana se guardan en los campos correspondientes de la clase del objeto primitivo en el archivo Objects.mqh. Son necesarias para no calcular todas las coordenadas de nuevo cuando la ventana se desplaza. Es decir, para actualizar las coordenadas de cada objeto de todos los controles será necesaria una operación en vez de dos, como en el momento del establecimiento del objeto. En el siguiente código, el mantenimiento de las sangrías desde el punto extremo de la ventana se marca con el verde.

//+------------------------------------------------------------------+
//| Crea el icono del programa                                       |
//+------------------------------------------------------------------+
//--- Imágenes (por defecto) que simbolizan el tipo del programa
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\advisor.bmp"
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\indicator.bmp"
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp"
//---
bool CWindow::CreateIcon(void)
  {
   string name=CElement::ProgramName()+"_window_icon_"+(string)CElement::Id();
//--- Coordenadas del objeto
   int x=m_x+5;
   int y=m_y+2;
//--- Establecemos el icono de la ventana
   if(!m_icon.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
 //--- Icono por defecto, si no está especificado por el usuario
   if(m_icon_file=="")
      m_icon_file=DefaultIcon();
//--- Establecemos las propiedades
   m_icon.BmpFileOn("::"+m_icon_file);
   m_icon.BmpFileOff("::"+m_icon_file);
   m_icon.Corner(m_corner);
   m_icon.Selectable(false);
   m_icon.Z_Order(m_button_zorder);
   m_icon.Tooltip("\n");
//--- Guardamos las coordenadas
   m_icon.X(x);
   m_icon.Y(y);   
//--- Sangrías desde el punto extremo
   m_icon.XGap(x-m_x);
   m_icon.YGap(y-m_y);
//--- Guardamos los tamaños (en el objeto)
   m_icon.XSize(m_icon.X_Size());
   m_icon.YSize(m_icon.Y_Size());
 //--- Añadimos los objetos en el array del grupo
   CElement::AddToArray(m_icon);
   return(true);
  }

El código del método para crear la etiqueta de texto CWindow::CreateLabel() no tiene ningunas peculiaridades diferentes de las que han sido descritas en el método para crear el icono del programa, por eso no vamos a considerarlo aquí para ahorrar el espacio en el artículo. Se puede encontrar su código en el archivo Window.mqh, al final del artículo.

Ahora vamos a considerar los botones que se ubicarán en el encabezado de la ventana. Todos estos botones tendrán las reglas que los hacen diferentes de otros métodos descritos anteriormente:

Lo que se refiere a la funcionalidad para minimizar y maximizar la ventana, para eso serán necesarios dos botones. Van a ubicarse uno encima de otro, y la visibilidad de cada uno dependerá del estado en el que se encuentra la ventana. Si la ventana está ampliada, se visualiza el botón para minimizarla, y viceversa.

Por lo demás, estos botones no se diferencian en nada de otros métodos de creación de las partes del formulario de controles. Para examinar más detalladamente el código de los métodos CreateButtonClose(), CreateButtonRollUp(), CreateButtonUnroll() y CreateButtonTooltip(), diríjase al archivo adjunto Window.mqh al final del artículo.

Las imágenes para los botones que van a usarse en los ejemplos a continuación también se puede descargar al final del artículo. Usted puede usar sus propias imágenes. Pero hay que tener en cuenta que es necesario guardar los mismos nombres de los archivos bmp que aparecen en el código adjunto al artículo, o bien editar en el código la ruta hacia los archivos bmp si los nombres de sus archivos se diferencian de los propuestos en el artículo.

 

Adjuntar el formulario al gráfico

Hemos llegado al momento cuando por fin podemos testear una parte de lo que hemos hecho y ver el resultado. Vamos a ver cómo será el formulario de controles en el gráfico. La funcionalidad para la construcción de este formulario hemos creado antes.

Sigamos trabajando en el archivo para las pruebas que ha sido creado anteriormente. Ahí hemos parado en que hemos creado el método común para la construcción de la interfaz CProgram::CreateTradePanel() y el método para la creación del formulario de controles CProgram::CreateWindow(). Más abajo viene el código para crear el formulario. Fíjense en la manera de que se puede redefinir los colores del fondo y encabezado del formulario.

//+------------------------------------------------------------------+
//| Crea el formulario para los controles                            |
//+------------------------------------------------------------------+
bool CProgram::CreateWindow(const string caption_text)
  {
//--- Añadimos el puntero de la ventana en el array de ventanas
   CWndContainer::AddWindow(m_window);
//--- Propiedades
   m_window.XSize(200);
   m_window.YSize(200);
   m_window.WindowBgColor(clrWhiteSmoke);
   m_window.WindowBorderColor(clrLightSteelBlue);
   m_window.CaptionBgColor(clrLightSteelBlue);
   m_window.CaptionBgColorHover(C'200,210,225');
 //--- Creación del formulario
   if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,1,1))
      return(false);
//---
   return(true);
  }

En el método común CProgram::CreateTradePanel() hay que llamar al método CProgram::CreateWindow(), pasando como único parámetro el texto para el encabezado de la ventana:

//+------------------------------------------------------------------+
//| Crea el panel de trading                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creación del formulario para los controles
   if(!CreateWindow("EXPERT PANEL"))
      return(false);
//--- Creación de controles
// ...
//---
   ::ChartRedraw();
   return(true);
  }

Ahora sólo nos queda añadir la llamada al método CProgram::CreateTradePanel() en el archivo principal del programa en la función OnInit(). Si surge un error durante la creación de la interfaz del programa, mostraremos el mensaje correspondiente en el registro y terminaremos el trabajo:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   program.OnInitEvent();
//--- Establecemos el panel de trading
   if(!program.CreateTradePanel())
     {
      ::Print(__FUNCTION__," > ¡Fallo al crear la interfaz gráfica!");
      return(INIT_FAILED);
     }
//--- Inicialización con éxito
   return(INIT_SUCCEEDED);
  }

Por favor, compile el código y cargue el programa en el gráfico. Si ha hecho todo bien, en el gráfico aparece el formulario que se muestra a continuación:

Fig. 4. La primera prueba de colocación del formulario en el gráfico

Fig. 4. La primera prueba de colocación del formulario en el gráfico

Si en el proceso del desarrollo de la aplicación MQL se le olvida añadir el puntero de la ventana en la base de punteros, Usted igual podrá conseguir la compilación del código. Sin embargo, al intentar cargar el programa al gráfico, verá que éste será eliminado inmediatamente, y en el registro en la pestaña “Asesores Expertos” (panel “Caja de Herramientas”) aparecerán los siguientes mensajes:

2015.09.23 19:26:10.398 TestLibrary (EURUSD,D1) OnInit > ¡Fallo al crear la interfaz gráfica!
2015.09.23 19:26:10.398 TestLibrary (EURUSD,D1) CWindow::CreateWindow > Antes de crear la ventana, hay que guardar su puntero en la base: CWndContainer::AddWindow(CWindow &object).

Los ejemplos más detallados del uso de los métodos de la clase CWindow en el indicador y script serán mostrados en los capítulos (artículos) siguientes.

 

Conclusión

En este momento, la estructura de nuestra librería es como se muestra en el esquema de abajo:

Fig. 5. Añadiendo el control principal de la interfaz y el formulario para los controles en el proyecto

Fig. 5. Añadiendo el control principal de la interfaz y el formulario para los controles en el proyecto

Todas las clases relacionadas con los controles de la interfaz y derivadas de su clase base CElement serán ubicadas en el rectángulo grande con el marco azul grueso. Todos los controles de la interfaz serán incluidos en el archivo con la clase CWndContainer.

Más abajo puede descargar el material de la primera parte de la serie para poder probar cómo funciona todo eso. Si le surgen 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 primera parte: