Interfaces gráficas I: Preparación de la estructura de la biblioteca (Capítulo 1)

Anatoli Kazharski | 9 febrero, 2016

Índice

 

Introducción

Con este artículo yo empiezo una serie más que concierne al desarrollo de las interfaces gráficas. Actualmente, no hay ninguna librería del código que permita crear fácil y rápidamente las interfaces gráficas en las aplicaciones MQL. Me refiero a las interfaces gráficas a las que estamos acostumbrados en los sistemas operativos comunes.

El objetivo del proyecto consiste en ofrecer al usuario final esta posibilidad y enseñar hacerlo usando mi librería. He tratado de hacerla máximamente comprensible para el aprendizaje, con posibilidades de su futuro desarrollo.

Además, cabe mencionar la librería del código de Dmitry Fedoseev la que ha compartido en una serie de sus artículos:

Aunque ambas librerías (ofrecida por Dmitry y la estándar) poseen sus ventajas, también tienen sus desventajas. Dmitry ha dado una descripción muy detallada en forma de un manual de referencia, y una serie más amplia de los elementos de la interfaz en comparación con la librería estándar, pero no estoy de acuerdo con algunos momentos en la funcionalidad y en el mecanismo implemendado para el control de los objetos. Algunos miembros del foro han compartido ideas interesantes en sus comentarios sobre el artículo, y he tratado de tomar en cuenta algunas de ellas.

La librería estándar no tiene la descripción detallada y, según mi opinión, su estructura está mejor implementada pero no hasta un nivel suficiente. Tampoco dispone de muchos controles que pueden ser necesarios a la hora de diseñar las interfaces gráficas. En ambas implementaciones, cuando surge la necesidad de modernizar y mejorar la calidad, uno sin querer llega a la conclusión que hay que diseñar el esquema desde cero. En mi serie de artículos he intentado reunir todas las ventajas y eliminar todas las desventajas.

Antes he escrito dos simples artículos de introducción que tratan de los controles:

Están escritas en el estilo procesal y sirven más bien para familiarizarse con el lenguaje MQL. Ahora es el momento para mostrar una estructura más compleja, usando de ejemplo un proyecto bastante grande que va a ser implementado en forma orientada a objetos.

¿Qué obtendrá el lector después de leer estos artículos?

He llamado al método de narración que va a utilizarse en esta serie de artículos “intento de imitación de la secuencia ideal”. La cosa es que durante el proceso del desarrollo de proyectos grandes, la secuencia de acciones y el hilo de los pensamientos son mucho más caóticos y se componen de varios experimentos, pruebas y errores. Pues, aquí vamos a ocultar todas estas complicaciones. A los que se cruzan con los proyectos de esta envergadura por primera vez, se les recomienda repetir todas las acciones para el mejor entendimiento del material cuando estudian esta librería, o mejor dicho el proceso de su creación. Es que los artículos de esta serie permiten representar el hilo de los pensamientos en forma de una secuencia ideal, cuando tenemos las respuestas a la mayoría de las preguntas y todas las partes del proyecto se crean a medida de que vaya surgiendo la necesidad en ellas.

 

Lista de elementos de control

Pues bien, ¿cómo tiene que ser esta librería? ¿Cómo tiene que ser la estructura POO del código de esta librería? ¿Con qué empezamos? En realidad, habrá bastante más preguntas, pero para empezar, vamos a aclarar qué elementos de control y de la interfaz serán necesarios para crear una aplicación MQL cómoda para el uso. Cada uno tiene sus propios requerimientos, y la escala de ideas también es diferente. Para una persona, unos cuantos botones y casillas de verificación serán suficientes. Otros necesitan unas interfaces de múltiples ventanas con la posibilidad de seleccionar del conjunto de datos y su control.

Además, me gustaría señalar que la implementación descrita en esta serie de artículos puede ser utilizada como un producto final en las plataformas comerciales MetaTrader 4 y MetaTrader 5 inmediatamente después de su publicación. Pero si enfocamos el asunto a través del prisma de un ideal, entonces, desde luego, también hay espacio para crecer. Una vez publicados todos los artículos de esta serie, compartiré mis pensamientos sobre qué es para mí una implementación ideal de una librería para el desarrollo de interfaces gráficas en MQL.

La versión inicial de la librería va a contener los elementos de la interfaz de la lista a continuación. Algunos de ellos serán implementados de varios modos y estarán representados como clases separadas del código, cada una de las cuales va a servir para determinadas tareas. Eso significa que algunos de ellos van a convenir mejor para unas tareas, y otros para otras tareas.

La presente lista contiene los controles que incluyen otros elementos de la misma lista para su funcionamiento. Por ejemplo, en la lista de arriba se puede observar que los controles tipo Cuadro combinado incluyen el elemento “lista” (list view), y “lista” (list view), a su vez, incluye la barra de desplazamiento vertical (scroll). La barra de desplazamiento horizontal y vertical forman parte de todos los tipos de las tablas. El calendario desplegable (drop calendar) contiene el elemento ya hecho “calendario” (calendar) que puede utilizarse como un control independiente. Vamos a analizar la creación de cada elemento con más detalles después de que ya tengamos aclarada la estructura del proyecto.

 

Clases base de la librería estándar como objetos primitivos

A pesar de todo, vamos a utilizar algunas clases del código desde la biblioteca estándar. Pertenecen a las primitivas gráficas básicas de las que se construyen todos los controles. Cada una de estas clases permite crear/eliminar rápidamente un objeto gráfico, obtener o cambiar cualquiera de sus propiedades.

Los archivos con las clases para trabajar con las primitivas gráficas se ubican en:

Ya existe un artículo con la descripción de estas clases y ejemplos de su uso: Cree su propia observación del mercado usando las clases de la librería estándar. Por eso aquí no vamos a entrar en sus detalles. Sólo procede recordar que la clase base de este grupo de las clases es CObject. Su clase derivada es CChartObject. Ella contiene los métodos comunes que pueden aplicarse a todos los objetos gráficos. Todas las demás clases se derivan de la clase CChartObject, y contienen los métodos para manejar las propiedades únicas para cada objeto gráfico en particular.

La estructura general de interconexiones entre todas las clases de la librería estándar referentes a los objetos gráficos puede resumirse como sigue. Entendámonos desde el principio que las flechas rojas significarán la conexión de la clase base con la derivada.

Fig. 1. Estructura general de interconexiones entre las clases de objetos gráficos de la librería estándar.

Fig. 1. Estructura general de interconexiones entre las clases de objetos gráficos de la librería estándar

A medida de que vayamos escribiendo la librería, vamos a mostrar sus estructura en los artículos con similares esquemas Pero para ahorrar el espacio, usaremos la versión abreviada tal como se muestra en la imagen de abajo. Eso quiere decir que el último elemento en el esquema presentado puede ser cualquier objeto gráfico (…) del esquema de arriba.

Fig. 2. Versión abreviada de la estructura de objetos gráficos de la librería estándar.

Fig. 2. Versión abreviada de la estructura de objetos gráficos de la librería estándar

Para construir las interfaces gráficas, vamos a necesitar sólo algunas de estas clases:

Su propiedad común consiste en que no se adjuntan a la escala de tiempo, es decir no se mueven junto con el gráfico cuando éste se desplaza. Luego, para cada una de estas primitivas tenemos que crear las clases derivadas que guardan algunas propiedades a las cuales hay que referirse a menudo:

Además, en estas clases hay que crear los métodos correspondientes para obtener y guardar los valores de estas propiedades. En realidad, se puede obtener los valores de estas propiedades usando los métodos de las clases base del nivel más alto. Pero este enfoque será muy derramado en cuanto al consumo de recursos.

 

Clases derivadas de objetos primitivos con métodos adicionales

En el directorio <carpeta de datos>\MQL5\Include (para MetaTrader 4: <carpeta de datos>\MQL4\Include) crearemos la carpeta EasyAndFastGUI en la que vamos a ubicar todos los archivos de nuestra librería. Para abrir la Carpeta de Datos, hay que seleccionar Archivo > Abrir carpeta de datos en el menú principal de MetaTrader o MetaEditor. En la carpeta EasyAndFastGUI, todos los archivos relacionados con la librería para crear las interfaces gráficas van a almacenarse en la subcarpeta Controls. Luego, en la subcarpeta Controls hay que crear el archivo Objects.mqh. Va a contener las clases derivadas mencionadas anteriormente.

Al principio del archivo Objects.mqh, incluimos los archivos necesarios de la librería estándar:

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

Todas las clases para los objetos listados antes serán del mismo tipo, por eso aquí vamos a mostrar el código sólo de una de ellas. Todos estos métodos caben prácticamente en una sola línea. Con el fin de ahorrar el espacio dentro del archivo y presentar la información de forma máximamente comprimida, estos métodos van a ubicarse directamente en el cuerpo de la clase.

La inicialización de todas las variables se realiza en el constructor de la clase. Una parte de ellas está en la lista de inicialización del constructor (antes del cuerpo de la clase). Pero la verdad es que en nuestro caso eso no importa mucho ya que durante el desarrollo de la librería no hubo ninguna ocasión cuando habría que gestionar la secuencia de la inicialización de los campos (variables) de las clases derivadas y clases base. Por eso en el cuerpo de la clase se puede inicializar todas las variables o sólo las que requieren algunos comentarios.

//+------------------------------------------------------------------+
//| Clase con propiedades adicionales para el objeto Rectangle Label |
//+------------------------------------------------------------------+
class CRectLabel : public CChartObjectRectLabel
  {
protected:
   int               m_x;
   int               m_y;
   int               m_x2;
   int               m_y2;
   int               m_x_gap;
   int               m_y_gap;
   int               m_x_size;
   int               m_y_size;
   bool              m_mouse_focus;
public:
                     CRectLabel(void);
                    ~CRectLabel(void);
   //--- Coordenadas
   int               X(void)                      { return(m_x);           }
   void              X(const int x)               { m_x=x;                 }
   int               Y(void)                      { return(m_y);           }
   void              Y(const int y)               { m_y=y;                 }
   int               X2(void)                     { return(m_x+m_x_size);  }
   int               Y2(void)                     { return(m_y+m_y_size);  }
   //--- Sangrías desde el punto extremo (xy)
   int               XGap(void)                   { return(m_x_gap);       }
   void              XGap(const int x_gap)        { m_x_gap=x_gap;         }
   int               YGap(void)                   { return(m_y_gap);       }
   void              YGap(const int y_gap)        { m_y_gap=y_gap;         }
   //--- Tamaños
   int               XSize(void)                  { return(m_x_size);      }
   void              XSize(const int x_size)      { m_x_size=x_size;       }
   int               YSize(void)                  { return(m_y_size);      }
   void              YSize(const int y_size)      { m_y_size=y_size;       }
   //--- Foco
   bool              MouseFocus(void)             { return(m_mouse_focus); }
   void              MouseFocus(const bool focus) { m_mouse_focus=focus;   }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CRectLabel::CRectLabel(void) : m_x(0),
                               m_y(0),
                               m_x2(0),
                               m_y2(0),
                               m_x_gap(0),
                               m_y_gap(0),
                               m_x_size(0),
                               m_y_size(0),
                               m_mouse_focus(false)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CRectLabel::~CRectLabel(void)
  {
  }

Un archivo va a contener varias clases. Para navegar rápidamente entre ellas, al principio del archivo (justo después de los archivos incluidos de la librería estándar), vamos a escribir el contenido del archivo que representará una lista de las clases sin cuerpo:

//--- Lista de las clases en el archivo para una rápida navegación (Alt+G)
class CRectLabel;
class CEdit;
class CLabel;
class CBmpLabel;
class CButton;

Ahora, de la misma manera que Usted puede moverse entre las funciones y métodos, colocando el cursor en el nombre de la clase en esta lista y pulsando Alt+G puede navegar rápidamente a la clase necesaria en el archivo.

En esta fase, podemos representar nuestro esquema como se muestra en la imagen de abajo. Aquí el rectángulo con el marco azul grueso es el archivo Objects.mqh con las clases dentro (rectángulos con el marco azul fino) que han sido descritas más arriba. Los marcos azules significan que todas las clases en este archivo son derivadas de una de las clases representadas por el rectángulo CChartObject…, del que va la última flecha azul.

Fig. 3. Expansión de la estructura mediante la creación de las clases derivadas para los objetos primitivos.

Fig. 3. Expansión de la estructura mediante la creación de las clases derivadas para los objetos primitivos

La cuestión con las primitivas gráficas está solucionada. A continuación, necesitamos decidir cómo gestionar estos objetos cuando se encuentran en el grupo, es que prácticamente cada control va a componerse de varios objetos simples. Cada control es único, pero el conjunto común de propiedades para todos los elementos existe también. Por eso vamos a crear la clase base CElement que va a contener el conjunto común de propiedades para cada control.

 

Clase base para todos los controles

La clase CElement estará en el archivo Element.mqh, y el archivo Objects.mqh va a conectarse mediante el comando #include:

//+------------------------------------------------------------------+
//|                                                      Element.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Objects.mqh"
//+------------------------------------------------------------------+
//| Clase base del control                                           |
//+------------------------------------------------------------------+
class CElement
  {
public:
                     CElement(void);
                    ~CElement(void);
  };

Luego, con el fin de ahorrar el espacio en el artículo, voy a mostrar los métodos de esta clase en grupos separados en el cuerpo de la clase. Al final del artículo, puede descargar en su ordenador la versión completa de la clase con todos los métodos que se describen a continuación. Vamos a seguir el mismo enfoque para todas las demás clases también.

Cabe señalar que ninguna de las clases del archivo Objects.mqh va a servir de la clase base para la clase CElement, así como ninguna de ellas va a figurar como objetos incluidos en esta clase. Pero más tarde van a utilizarse como objetos en todas las clases derivadas de la clase CElement y guardarse en la clase base sólo como el array de punteros a los objetos. Una vez incluido el archivo Objects.mqh en el archivo Element.mqh, ya no tendremos que incluirlo en ningún otro archivo en el futuro. Como resultado, en vez de incluir dos archivos (Objects.mqh y Element.mqh), habrá que incluir sólo uno, o sea Element.mqh.

En la carpeta Controls crearemos un archivo para guardar en las directrices #define algunas propiedades generales para el programa entero:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Nombre de la clase
#define CLASS_NAME ::StringSubstr(__FUNCTION__,0,::StringFind(__FUNCTION__,"::"))
//--- Nombre del programa
#define PROGRAM_NAME ::MQLInfoString(MQL_PROGRAM_NAME)
//--- Tipo del programa
#define PROGRAM_TYPE (ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)
//--- Prevención de superar el rango
#define PREVENTING_OUT_OF_RANGE __FUNCTION__," > Prevención de superar el tamaño del array."

//--- Fuente
#define FONT      ("Calibri")
#define FONT_SIZE (8)

Nótese que en el código hay doble dos puntos antes de las funciones. La verdad es que se puede omitirlos y todo va a seguir funcionando bien. Sin embargo, en programación se considera de buen tono poner doble dos puntos antes de las funciones de sistema del lenguaje. Esto hace inívoco al hecho de que la función es de sistema.

Adjuntamos el archivo Defines.mqh al archivo Objects.mqh, así estará disponible para toda la cadena de los archivos incluidos uno en uno (Defines.mqh -> Objects.mqh -> Element.mqh) :

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

Ahora tenemos que definir qué propiedades generales deben compartir todos los controles. Cada control es un módulo del programa separado que va a funcionar independientemente de todos los demás módulos similares. Pero debido a que algunos controles serán agrupados, es decir reunidos en unos elementos de control más complejos (compuestos), surgirán las situaciones cuando los controles van a enviar los mensajes al módulo principal del programa, así como a otros controles. Por eso va a surgir la necesidad de definir si el mensaje ha sido recibido del control que pertenece precisamente a nuestro programa, ya que en el gráfico puede haber varias aplicaciones MQL al mismo tiempo.

Además, tendremos que definir:

class CElement
  {
protected:
   //--- (1) Nombre de la clase y (2) programa, (3) tipo del programa
   string            m_class_name;
   string            m_program_name;
   ENUM_PROGRAM_TYPE m_program_type;
   //--- Estado del control
   bool              m_is_visible;
   bool              m_is_dropdown;
   int               m_is_object_tabs;
   //--- Foco
   bool              m_mouse_focus;
   //---
public:
                     CElement(void);
                    ~CElement(void);
   //--- (1) Obtener y establecer el nombre de la clase, (2) obtener el nombre del programa, 
   //    (3) obtener el tipo del programa
   string            ClassName(void)                    const { return(m_class_name);           }
   void              ClassName(const string class_name)       { m_class_name=class_name;        }
   string            ProgramName(void)                  const { return(m_program_name);         }
   ENUM_PROGRAM_TYPE ProgramType(void)                  const { return(m_program_type);         }
   //--- Estado del control
   void              IsVisible(const bool flag)               { m_is_visible=flag;              }
   bool              IsVisible(void)                    const { return(m_is_visible);           }
   void              IsDropdown(const bool flag)              { m_is_dropdown=flag;             }
   bool              IsDropdown(void)                   const { return(m_is_dropdown);          }
   void              IsObjectTabs(const int index)            { m_is_object_tabs=index;         }
   int               IsObjectTabs(void)                 const { return(m_is_object_tabs);       }
   //--- Foco
   bool              MouseFocus(void)                   const { return(m_mouse_focus);          }
   void              MouseFocus(const bool focus)             { m_mouse_focus=focus;            }
  };

Puesto que en la interfaz del programa pueden haber varios controles (por ejemplo, varios botones o varias casillas de verificación), cada uno de ellos tiene que tener su número único, o identificador (id). Al mismo tiempo, si el control se compone de un array entero de otros controles, cada uno de ellos tiene que tener su número del índice.

class CElement
  {
protected:
   //--- Identificador y el índice del control
   int               m_id;
   int               m_index;
   //---
public:
   //--- Establecer y obtener el identificador del control
   void              Id(const int id)                         { m_id=id;                      }
   int               Id(void)                           const { return(m_id);                 }
   //--- Establecer y obtener el índice del control
   void              Index(const int index)                   { m_index=index;                }
   int               Index(void)                        const { return(m_index);              }
  };

Como se ha mencionado antes, todos los objetos de las primitivas gráficas del control van a almacenarse en el array tipo CChartObject como punteros a estos objetos. Por eso vamos a necesitar un método para poder insertar los punteros a objetos, tras su creación, en los arrays. También vamos a necesitar (1) obtener el puntero desde el array indicando su índice, (2) obtener el tamaño del array de objetos y (3) vaciar el búfer del array.

class CElement
  {
protected:
   //--- Array común de punteros a todos los objetos en el control
   CChartObject     *m_objects[];
   //---
public:
   //--- Obtener el puntero del objeto según el índice especificado
   CChartObject     *Object(const int index);
   //--- (1) Obtener el número de objetos del control, (2) vaciar el array de objetos
   int               ObjectsElementTotal(void)          const { return(::ArraySize(m_objects)); }
   void              FreeObjectsArray(void)                   { ::ArrayFree(m_objects);         }
   //---
protected:
   //--- Método para añadir punteros de objetos primitivos en array común
   void              AddToArray(CChartObject &object);
  };
//+------------------------------------------------------------------+
//| Devuelve el puntero del objeto del control según el índice       |
//+------------------------------------------------------------------+
CChartObject *CElement::Object(const int index)
  {
   int array_size=::ArraySize(m_objects);
//--- Comprobar tamaño del array de objetos
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > En este elemento ("+m_class_name+") no hay objetos");
      return(NULL);
     }
//--- Corrección en caso de superar el rango
   int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index;
//--- Devolver el puntero del objeto
   return(m_objects[i]);
  }
//+------------------------------------------------------------------+
//| Añade el puntero al objeto en el array                           |
//+------------------------------------------------------------------+
void CElement::AddToArray(CChartObject &object)
  {
   int size=ObjectsElementTotal();
   ::ArrayResize(m_objects,size+1);
   m_objects[size]=::GetPointer(object);
  }

Cada control tendrá su manejador de eventos del gráfico y su temporizador. En la clase CElement estos métodos serán virtuales. Estas funciones no pueden ser universales porque cada control es único. Discutiremos este asunto más detalladamente cuando vamos a desarrollar la clase que va a servir del contenedor para todos los objetos (controles). Los siguientes métodos también serán virtuales:

class CElement
  {
public:
   //--- 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) {}
  };

Usted ya ha visto que en las clases de las primitivas gráficas en el archivo Objects.mqh y en la clase CElement, hay propiedades y métodos que nos permiten obtener los límites del objeto. Por consiguiente, habrá la posibilidad de saber si el cursor se encuentra dentro del área del control, así como sobre uno u otro objeto primitivo individualmente. ¿Para qué lo necesitamos? Eso nos permite hacer una interfaz gráfica del programa máximamente intuitiva para el usuario.

Cuando el cursor se encuentre sobre un elemento de la interfaz, el color de su fondo o marco va a cambiar, indicando al usuario que se puede hacer clic en él. Para implementar esta funcionalidad en la clase CElement, vamos a necesitar los métodos para trabajar con los colores. En uno de ellos va a definirse el array de colores, para eso a este método habrá que pasar sólo dos colores de los que va a calcularse el gradiente. El cálculo se realizará sólo una vez para cada objeto en el momento de su colocación sobre el gráfico. En el segundo método para el trabajo con los colores, se trabajará sólo con el array de colores hecho, lo que ahorrará considerablemente los recursos.

Usted mismo puede hacer el método para calcular el gradiente, pero nosotros usaremos la clase del código ya hecha que se puede descargar de Code Base. Muchos métodos de Code Base serán usados en este proyecto en otras clases. Dmitry Fedoseev ha añadido su versión de la clase para trabajar con el color (IncColors). Pero yo propongo usar la versión que he redactado un poco. Se puede descargarla al final del artículo (Colors.mqh).

Esta clase (CColors) contiene muchos métodos para todas ocasiones. Lo único que he cambiado ha sido añadir la posibilidad de la navegación rápida cuando los nombres de los métodos se encuentran en el cuerpo de la clase y los propios métodos están fuera del cuerpo de la clase. Así será más fácil y más rápido encontrar el método necesario y moverse del contenido al método y atrás, usando la combinación Alt+G. Este archivo tiene que ubicarse en la carpeta EasyAndFastGUI, y vamos a incluirlo en nuestra librería a través del archivo Element.mqh en el directorio ..\EasyAndFastGUI\Controls. Puesto que dicho archivo estará en el directorio a un nivel más alto, habrá que incluirlo tal como se muestra a continuación:

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

Incluimos el objeto de la clase CColors en la clase CElement, así como (1) añadimos la variable y el método para especificar el número de colores en el gradiente y (2) los métodos para la inicialización del array del gradiente y el cambio del color del objeto especificado:

class CElement
  {
protected:
   //--- Instancia de la clase para trabajar con el color
   CColors           m_clr;
   //--- Número de colores en el gradiente
   int               m_gradient_colors_total;
   //---
public:
   //--- Establecer el tamaño del gradiente
   void              GradientColorsTotal(const int total)     { m_gradient_colors_total=total;  }
   //---
protected:
   //--- Inicialización del array del gradiente
   void              InitColorArray(const color outer_color,const color hover_color,color &color_array[]);
   //--- Cambio del color del objeto
   void              ChangeObjectColor(const string name,const bool mouse_focus,const ENUM_OBJECT_PROPERTY_INTEGER property,
                                       const color outer_color,const color hover_color,const color &color_array[]);
  };

Para inicializar el array del gradiente, usaremos el método Gradient() de la clase CColors. En este método hay que pasar lo siguiente como parámetros: (1) el array de colores que van a utilizarse para calcular el gradiente, (2) el array que va a contener la secuencia de colores del gradiente, (3) el tamaño solicitado del array, es decir el número de pasos que tiene que haber en el gradiente.

//+------------------------------------------------------------------+
//| Inicialización del array del gradiente                           |
//+------------------------------------------------------------------+
void CElement::InitColorArray(const color outer_color,const color hover_color,color &color_array[])
  {
//--- Array de colores del gradiente
   color colors[2];
   colors[0]=outer_color;
   colors[1]=hover_color;
//--- Formación del archivo de colores
   m_clr.Gradient(colors,color_array,m_gradient_colors_total);
  }

En el método para el cambio del color del objeto habrán los parámetros que permiten especificar:

Por favor, véase abajo los comentarios adicionales en el código del método ChangeObjectColor():

//+------------------------------------------------------------------+
//| Cambio del color del objeto al situar el cursor sobre él         |
//+------------------------------------------------------------------+
void CElement::ChangeObjectColor(const string name,const bool mouse_focus,const ENUM_OBJECT_PROPERTY_INTEGER property,
                                 const color outer_color,const color hover_color,const color &color_array[])
  {
   if(::ArraySize(color_array)<1)
      return;
//--- Obtenemos el color actual del objeto
   color current_color=(color)::ObjectGetInteger(m_chart_id,name,property);
//--- Si el cursor está sobre el objeto
   if(mouse_focus)
     {
      //--- Salimos si el color especificado ha sido conseguido
      if(current_color==hover_color)
         return;
      //--- Vamos del primero al último
      for(int i=0; i<m_gradient_colors_total; i++)
        {
         //--- Si los colores no coinciden, vamos al siguiente
         if(color_array[i]!=current_color)
            continue;
         //---
         color new_color=(i+1==m_gradient_colors_total)? color_array[i] : color_array[i+1];
         //--- Cambiamos el color
         ::ObjectSetInteger(m_chart_id,name,property,new_color);
         break;
        }
     }
//--- Si el cursor está fuera del área del objeto
   else
     {
      //--- Salimos si el color especificado ha sido conseguido
      if(current_color==outer_color)
         return;
      //--- Vamos del último al primero
      for(int i=m_gradient_colors_total-1; i>=0; i--)
        {
         //--- Si los colores no coinciden, vamos al siguiente
         if(color_array[i]!=current_color)
            continue;
         //---
         color new_color=(i-1<0)? color_array[i] : color_array[i-1];
         //--- Cambiamos el color
         ::ObjectSetInteger(m_chart_id,name,property,new_color);
         break;
        }
     }
  }

Otros dos propiedades comunes para todos los controles son el punto de anclaje de objetos y la esquina del gráfico.

class CElement
  {
protected:
   //--- Esquina del gráfico y punto de anclaje de objetos
   ENUM_BASE_CORNER  m_corner;
   ENUM_ANCHOR_POINT m_anchor;
  }

Hemos terminado de crear la clase CElement. Al final del artículo se puede descargar la versión completa de esta clase. En este momento la estructura de la librería es como se muestra en el esquema de abajo. Entendámonos que las flechas van a denotar que el archivo está incluido. Sin embargo, si contiene una clase, no será la clase base para las clases que se encuentran en el archivo en el que se incluye. Va a utilizarse como objeto incluido en la clase, de la misma manera como ha sido mostrado antes entre las clases CElement y CColors.

Рис. 4. Incluir la clase CColors para trabajar con el color.

Fig. 4. Clase base para los controles CElement

 

Clases base para una aplicación con interfaz gráfica

A continuación, antes de empezar a crear los elementos de la interfaz, es necesario definir de qué manera será implementada la interacción entre los objetos. Hay que configurar el esquema de tal manera que el acceso a cada objeto se realice desde una clase que sea contenedor, donde los objetos no sólo se almacenan sino también están ordenados por categorías. Así, siempre se puede saber no sólo el número y el tipo de los objetos en este contenedor, sino también tener la posibilidad de manejarlos.

Pero si toda la funcionalidad requerida para eso estará ubicada en una sola clase, no será muy conveniente, ya que la clase estará sobrecargada. Estas clases (objetos) se llaman divinas o antipatrones de la POO porque tienen muchas tareas encargadas. Según vaya creciendo la estructura del proyecto, será bastante complicado introducir en esta clase algún tipo de cambios o adiciones. Por eso vamos a almacenar los objetos separadamente de la clase en la que van a procesarse los eventos. Los objetos van a almacenarse en la clase base CWndContainer, y los eventos se procesarán en la clase derivada CWndEvents.

Ahora vamos a crear las clases CWndContainer y CWndEvents. A medida que van a crearse todos los controles listados al principio del artículo, vamos a llenar esta clases con funcionalidad necesaria. Mientras tanto determinaremos la estructura general del proyecto.

En la carpeta Controls, hay que crear los archivos WndContainer.mqh y WndEvents.mqh. Por ahora, la clase CWndContainer estará completamente vacía, ya que no hemos creado todavía ningún control.

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Clase para almacenar todos los objetos de la interfaz            |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
                     CWndContainer(void);
                    ~CWndContainer(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndContainer::CWndContainer(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndContainer::~CWndContainer(void)
  {
  }
//+------------------------------------------------------------------+

Hay que incluir el archivo WndContainer.mqh en el archivo WndEvents.mqh, porque la clase CWndEvents será derivada de la clase CWndContainer:

//+------------------------------------------------------------------+
//|                                                    WndEvents.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "WndContainer.mqh"
//+------------------------------------------------------------------+
//| Clase para procesar los eventos                                  |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
protected:
                     CWndEvents(void);
                    ~CWndEvents(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndEvents::CWndEvents(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(void)
  {
  }
//+------------------------------------------------------------------+

Las clases CWndContainer y CWndEvents serán las clases base para cualquier aplicación MQL que requiere la interfaz gráfica.

Para futuras pruebas de esta librería en el proceso de su desarrollo, crearemos a un Asesor Experto. Hay que crearlo en una carpeta separada, porque aparte del archivo principal del programa, habrá un archivo de inclusión Program.mqh con la clase de nuestro programa (CProgram). Esta clase será derivada de la clase CWndEvents.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <EasyAndFastGUI\Controls\WndEvents.mqh>
//+------------------------------------------------------------------+
//| Clase para crear panel de trading                                |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
public:
                     CProgram(void);
                    ~CProgram(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CProgram::~CProgram(void)
  {
  }
//+------------------------------------------------------------------+

Nos harán falta los métodos para manejar los eventos, los cuales luego van a ser llamar en el archivo principal del programa, es decir en principales funciones-manejadores de eventos de la aplicación MQL:

class CProgram : public CWndEvents
  {
public:
   //--- Inicialización/deinicialización
   void              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   //--- Temporizador
   void              OnTimerEvent(void);
   //---
protected:
   //--- Manejador virtual del evento del gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
  };

Es necesario crear el temporizador y el manejador de eventos también al nivel superior en la clase base CWndEvents:

class CWndEvents : public CWndContainer
  {
protected:
   //--- Temporizador
   void              OnTimerEvent(void);
   //--- Manejador virtual del evento del gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {}
  };

Nótese que en ambos listados del código de arriba, tanto en la clase base CWndEvents, como en la clase derivada CProgram, el manejador de eventos (método OnEvent) está declarado como (virtual). Pero en la clase CWndEvents este método es ficticio “{}”. Eso nos permite redirigir el flujo de eventos de la clase base a la derivada cuando sea necesario. Los métodos virtuales OnEvent() en estas clases sirven para el uso interno. Para la llamada en el archivo principal del programa se usará otro método de la clase CWndEvents. Vamos a llamarlo ChartEvent(). Crearemos también los métodos auxiliares para cada tipo de eventos principales que permitirán hacer el código más comprensible y legible.

Además de los métodos auxiliares que incluirán también la verificación de los eventos de usuario, es necesario crear el método para verificar los eventos en los controles. Lo llamaremos CheckElementsEvents(). Abajo se colorea en verde:

class CWndEvents : public CWndContainer
  {
public:
   //--- Manejadores de eventos del gráfico
   void              ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //---
private:
   void              ChartEventCustom(void);
   void              ChartEventClick(void);
   void              ChartEventMouseMove(void);
   void              ChartEventObjectClick(void);
   void              ChartEventEndEdit(void);
   void              ChartEventChartChange(void);
   //--- Verificación de los eventos en los controles
   void              CheckElementsEvents(void);
  };

Los métodos auxiliares van a utilizarse dentro del método ChartEvent() y sólo en la clase CWndEvents. Para evitar el paso de los mismos parámetros en ellos, crearemos las variables similares en forma de los miembros de la clase, así como el método para su inicialización que vamos a utilizar en el mismo comienzo del método ChartEvent(). Estarán ubicadas en la sección (private) ya que se usarán sólo en esta clase.

class CWndEvents : public CWndContainer
  {
private:
   //--- Parámetros de eventos
   int               m_id;
   long              m_lparam;
   double            m_dparam;
   string            m_sparam;
   //--- Inicialización de los parámetros de eventos
   void              InitChartEventsParams(const int id,const long lparam,const double dparam,const string sparam);
  };
//+------------------------------------------------------------------+
//| Inicialización de las variables de eventos                       |
//+------------------------------------------------------------------+
void CWndEvents::InitChartEventsParams(const int id,const long lparam,const double dparam,const string sparam)
  {
   m_id     =id;
   m_lparam =lparam;
   m_dparam =dparam;
   m_sparam =sparam;
  }

Ahora en el archivo principal, (1) incluimos el archivo con la clase CProgram, (2) creamos su instancia y (3) conectamos con las funciones principales del programa:

//+------------------------------------------------------------------+
//|                                                  TestLibrary.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2015, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
//--- Incluir la clase del panel de trading
#include "Program.mqh"
CProgram program;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   program.OnInitEvent();
//--- Inicialización con éxito
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   program.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   program.OnTimerEvent();
  }
//+------------------------------------------------------------------+
//| Trade function                                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   program.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

Si es necesario, en la clase CProgram se puede crear los métodos para otros manejadores de eventos, como OnTick(), OnTrade() etc.

 

Prueba de los manejadores de eventos de la librería y clase de la aplicación

Antes hemos mencionado que el método virtual OnEvent() de la clase CProgram puede ser llamado de la clase base CWndEvents en el método ChartEvent(). Tenemos que asegurarnos de que eso funciona, y ahora ya podemos probar este mecanismo. Para eso, en el método CWndEvents::ChartEvent() llame al método CProgram::OnEvent() tal como se muestra a continuación:

//+------------------------------------------------------------------+
//| Manejo de eventos del programa                                   |
//+------------------------------------------------------------------+
void CWndEvents::ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   OnEvent(id,lparam,dparam,sparam);
  }

Luego en el método CProgram::OnEvent() escriba el siguiente código:

//+------------------------------------------------------------------+
//| Manejador de eventos                                             |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CLICK)
     {
      ::Comment("x: ",lparam,"; y: ",(int)dparam);
     }
  }

Compile los archivos e inicie el EA en el gráfico: Si hace clic izquierdo en el gráfico, en la esquina izquierda superior van a mostrarse las coordenadas del cursor en el momento de soltar el botón. Después de la prueba, el código resaltado en dos últimos listados puede ser eliminado de los métodos CWndEvents::ChartEvent() y CProgram::OnEvent().

 

Conclusión

Vamos a hacer un resumen intermedio visualizando todo de lo que hemos hablado antes en forma de un esquema:

Fig. 5. Inclusión en el proyecto de las clases del almacenamiento de punteros y manejo de eventos.

Fig. 5. Inclusión en el proyecto de las clases del almacenamiento de punteros y manejo de eventos

En este momento el esquema se compone de dos partes no conectadas entre sí. Para conectarlos, primero hay que crear el elemento principal de la interfaz. El elemento principal es el formulario o la ventana (window) a la que van a conectarse todos los demás controles. Por eso, a continuación, vamos a escribir esta clase y la llamaremos CWindow. Conectaremos el archivo con la clase Element.mqh con el archivo Window.mqh, ya que la clase CElement será base para la clase CWindow.

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: