Interfaces gráficas XI: Refactorización del código de la librería (build 14.1)

Anatoli Kazharski | 31 julio, 2017

Contenido


Introducción

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

El objetivo de la última actualización de la librería ha sido optimizar el código, reducir su tamaño y hacer que la implementación esté aún más orientada a objetos. Todo eso hará el código más comprensible para el análisis. La descripción detallada de los cambios introducidos permitirá al lector mejorar la librería personalmente según sus necesidades, gastando el mínimo de tiempo para este proceso. 

Debido a un volumen grande del material, he dividido la descripción de la última actualización de la librería en dos partes. A su atención, se ofrece la primera parte.


Propiedades comunes de los controles

En primer lugar, los cambios han recaído en las propiedades comunes de los controles de la librería. Los campos y los métodos de la clase de algunas propiedades (color del fondo, borde, texto, etc.) se almacenaban antes en las clases derivadas de cada control separado. Desde el punto de vista de la programación orientada a objetos, no es una sobrecarga. Ahora, cuando todos los controles de la interfaz gráfica ya están implementados, se puede determinar fácilmente los campos y los métodos para el ajuste de las propiedades comunes, y traspasarlos en la clase base. 

Vamos a determinar la lista completa de las propiedades propias de todos los controles de la librería y que pueden ubicarse en la clase base.

Ahora tenemos que decidir en qué clase exactamente van a colocarse los campos y los métodos para determinar y obtener estas propiedades. 

En la jerarquía de las clases de cada control hay dos clases: CElementBase y CElement. En la clase base CElementBase, colocaremos los campos y los métodos para las siguientes propiedades: coordenadas, tamaños, identificadores, índices, así como los modos propios de cada control de la lista. En la clase derivada CElement, colocaremos los campos y los métodos para gestionar las propiedades relacionadas con la apariencia de los controles.

Además de eso, en la clase CElementBase, han sido añadidos los métodos para formar el nombre del objeto gráfico de los controles. Antes el nombre se formaba en los métodos de la creación de los controles. Pero ahora, cuando cada control se dibuja en un objeto separado, es posible crear los métodos universales que pueden encontrarse en la clase base. 

Para determinar y obtener la parte del nombre que define el tipo del control, se usan los métodos de CElementBase::NamePart().

//+------------------------------------------------------------------+
//| Clase base del control                                           |
//+------------------------------------------------------------------+
class CElementBase
  {
protected:
   //--- Parte del nombre (tipo del control)
   string            m_name_part;
   //---
public:
   //--- (1) Guarda y (2) devuelve la parte del nombre del control
   void              NamePart(const string name_part)                { m_name_part=name_part;                }
   string            NamePart(void)                            const { return(m_name_part);                  }
  };

Para formar el nombre completo del objeto gráfico, se usa el método de CElementBase::ElementName(). A este método hay que pasarle la parte del nombre que define el tipo del control. Si resulta que la parte del nombre ya ha sido determinada antes, el valor pasado no va a usarse. Con relación a los últimos cambios en la librería (véase abajo), este enfoque se utiliza en los casos cuando un control es derivado del otro y es necesario redifinir la parte del nombre.

class CElementBase
  {
protected:
   //--- Nombre del control
   string            m_element_name;
   //---
public:
   //--- Formación del nombre del objeto
   string            ElementName(const string name_part="");
  };
//+------------------------------------------------------------------+
//| Devuelve el nombre formado del control                           |
//+------------------------------------------------------------------+
string CElementBase::ElementName(const string name_part="")
  {
   m_name_part=(m_name_part!="")? m_name_part : name_part;
//--- Formación del nombre del objeto
   string name="";
   if(m_index==WRONG_VALUE)
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Id();
   else
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Index()+"__"+(string)CElementBase::Id();
//---
   return(name);
  }

Al procesar el clic del ratón en algún control, es necesario comprobar el nombre del objeto gráfico que ha sido pulsado. Esta comprobación se repetía con frecuencia para muchos controles, por eso en la clase base fue colocado el método especial CElementBase::CheckElementName():

class CElementBase
  {
public:
   //--- Comprobación de la presencia de una parte significativa del nombre del control en la línea
   bool              CheckElementName(const string object_name);
  };
//+------------------------------------------------------------------+
//| Devuelve el nombre formado del control                           |
//+------------------------------------------------------------------+
bool CElementBase::CheckElementName(const string object_name)
  {
//--- Si el clic no ha sido hecho en este control
   if(::StringFind(object_name,m_program_name+"_"+m_name_part+"_")<0)
      return(false);
//---
   return(true);
  }

En cuanto a las demás propiedades, tiene sentido analizar más detalladamente sólo la descripción de los métodos para el trabajo con las imágenes.


Clase para trabajar con los datos de la imagen

Para trabajar con las imágenes, ha sido implementada la clase CImage en la que se puede guardar los datos de la imagen:

Para obtener los valores de estas propiedades, vamos a necesitar los métodos correspondientes:

//+------------------------------------------------------------------+
//| Clase para almacenar los datos de la imagen                      |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // Array de los píxeles de la imagen
   uint              m_image_width;  // Ancho de la imagen
   uint              m_image_height; // Alto de la imagen
   string            m_bmp_path;     // Ruta hacia el archivo de la imagen
   //---
public:
                     CImage(void);
                    ~CImage(void);
   //--- (1) Tamaño del array de datos, (2) establecer/devolver datos (color del píxel)
   uint              DataTotal(void)                             { return(::ArraySize(m_image_data)); }
   uint              Data(const uint data_index)                 { return(m_image_data[data_index]);  }
   void              Data(const uint data_index,const uint data) { m_image_data[data_index]=data;     }
   //--- Establecer/devolver el ancho de la imagen
   void              Width(const uint width)                     { m_image_width=width;               }
   uint              Width(void)                                 { return(m_image_width);             }
   //--- Establecer/devolver el alto de la imagen
   void              Height(const uint height)                   { m_image_height=height;             }
   uint              Height(void)                                { return(m_image_height);            }
   //--- Establecer/devolver la ruta hacia la imagen
   void              BmpPath(const string bmp_file_path)         { m_bmp_path=bmp_file_path;          }
   string            BmpPath(void)                               { return(m_bmp_path);                }
  };

Para leer la imagen y guardar sus datos, se usa el método CImage::ReadImageData() al que es necesario traspasar la ruta hacia el archivo de la imagen:

class CImage
  {
public:
   //--- Lee y guarda los datos de la imagen enviada
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| Guarda la imagen traspasada en el array                          |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- Salir si es una línea vacía
   if(bmp_file_path=="")
      return(false);
//--- Guardamos la ruta hacia la imagen
   m_bmp_path=bmp_file_path;
//--- Resetear el último error
   ::ResetLastError();
//--- Leer y guardar los datos de la imagen
   if(!::ResourceReadImage("::"+m_bmp_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > Error al leer las imágenes ("+m_bmp_path+"): ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

A veces hace falta copiar los datos de la imagen traspasada. Para eso se utiliza el método CImage::CopyImageData().  En este método, se traspasa por referencia el objeto tipo CImage cuyos datos del array es necesario copiar. Aquí primero, obtenemos el tamaño del array fuente y establecemos el mismo tamaño para el array receptor. Luego, usando el método CImage::Data() obtenemos en el ciclo los datos del array traspasado, y los guardamos en el array receptor.

class CImage
  {
public:
   //--- Copia los datos de la imagen traspasada
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| Copia los datos de la imagen traspasada                          |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- Obtenemos el tamaño del array fuente
   uint source_data_total =array_source.DataTotal();
//--- Cambiar el tamaño del array receptor
   ::ArrayResize(m_image_data,source_data_total);
//--- Copiamos los datos
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=array_source.Data(i);
  }

Para eliminar los datos de la imagen, se utiliza el método CImage::DeleteImageData():

class CImage
  {
public:
   //--- Elimina los datos de la imagen
   void              DeleteImageData(void);
  };
//+------------------------------------------------------------------+
//| Elimina los datos de la imagen                                   |
//+------------------------------------------------------------------+
void CImage::DeleteImageData(void)
  {
   ::ArrayFree(m_image_data);
   m_image_width  =0;
   m_image_height =0;
   m_bmp_path     ="";
  }

La clase CImage se encuentra en el archivo Objects.mqh. Ahora todos los controles van a dibujarse, por eso las clases para la creación de los objetos gráficos primitivos ya no son necesarios. Han sido eliminados del archivo Objects.mqh. La excepción ha sido hecha sólo para el objeto-gráfico que permite crear los gráficos similares al gráfico principal del símbolo. En su ventana, se ubican todos los tipos de las aplicaciones MQL y, por consecuencia, se crea la interfaz gráfica.

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Enums.mqh"
#include "Defines.mqh"
#include "Fonts.mqh"
#include "Canvas\Charts\LineChart.mqh"
#include <ChartObjects\ChartObjectSubChart.mqh>
//--- Lista de las clases en el archivo para una rápida navegación (Alt+G)
class CImage;
class CRectCanvas;
class CLineChartObject;
class CSubChart;
...


Métodos para trabajar con las imágenes

Han sido implementados varios métodos para trabajar con las imágenes de los controles. Todos ellos se encuentran en la clase CElement. Ahora existe la posibilidad de establecer la cantidad necesaria de los grupos (arrays) de imágenes para uno u otro control. Eso permite hacer que la apariencia de los controles sea más informativa. El desarrollador de la aplicación MQL decide personalmente cuántos iconos van a mostrarse en el control.

Para eso, en la clase CElement ha sido creada la estructura EImagesGroup y ha sido declarado el array dinámico de sus instancias. Ahí van a almacenarse las propiedades de los grupos de las imágenes (los márgenes y la imagen a mostrar), así como las propias imágenes para las que sirve el array dinámico tipo CImage

//+------------------------------------------------------------------+
//| Clase derivada del control                                       |
//+------------------------------------------------------------------+
class CElement : public CElementBase
  {
protected:
   //--- Grupos de imágenes
   struct EImagesGroup
     {
      //--- Arrays de imágenes
      CImage            m_image[];
      //--- Márgenes del icono
      int               m_x_gap;
      int               m_y_gap;
      //--- Imagen seleccionada para mostrar en el grupo
      int               m_selected_image;
     };
   EImagesGroup      m_images_group[];
  };

Para añadir una imagen al control, primero es necesario agregar el grupo. Se puede hacer eso usando el método CElement::AddImagesGroup(). De los argumentos a traspasar servirán los márgenes para las imágenes en este grupo desde el punto superior izquierdo del control. Por defecto, será seleccionada la la primera imagen del grupo.

class CElement : public CElementBase
  {
public:
   //--- Adición del grupo de imágenes
   void              AddImagesGroup(const int x_gap,const int y_gap);
  };
//+------------------------------------------------------------------+
//| Adición del grupo de imágenes                                    |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap)
  {
//--- Obtenemos el tamaño del array de los grupos de imágenes
   uint images_group_total=::ArraySize(m_images_group);
//--- Añadimos un grupo
   ::ArrayResize(m_images_group,images_group_total+1);
//--- Establecer los márgenes para las imágenes
   m_images_group[images_group_total].m_x_gap=x_gap;
   m_images_group[images_group_total].m_y_gap=y_gap;
//--- Imagen predefinida
   m_images_group[images_group_total].m_selected_image=0;
  }

Para añadir las imágenes al grupo, se usa el método CElement::AddImage(), en los argumentos se indica el índice del grupo y la ruta hacia el archivo de la imagen. La imagen no se añade si no hay por lo menos un grupo. Además, existe la corrección para evitar la superación del rango. En caso de salir fuera, la imagen será añadida al último grupo.

class CElement : public CElementBase
  {
public:
   //--- Añadiendo la imagen al grupo especificado
   void              AddImage(const uint group_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| Añadiendo la imagen al grupo especificado                        |
//+------------------------------------------------------------------+
void CElement::AddImage(const uint group_index,const string file_path)
  {
//--- Obtenemos el tamaño del array de los grupos de imágenes
   uint images_group_total=::ArraySize(m_images_group);
//--- Salir si no hay ningún grupo
   if(images_group_total<1)
     {
      Print(__FUNCTION__,
      " > Se puede añadir un grupo de imágenes usando los métodos CElement::AddImagesGroup()");
      return;
     }
//--- Prevención de superación del rango
   uint check_group_index=(group_index<images_group_total)? group_index : images_group_total-1;
//--- Obtenemos el tamaño del array de imágenes
   uint images_total=::ArraySize(m_images_group[check_group_index].m_image);
//--- Aumentamos el tamaño del array a un elemento
   ::ArrayResize(m_images_group[check_group_index].m_image,images_total+1);
//--- Añadimos una imagen
   m_images_group[check_group_index].m_image[images_total].ReadImageData(file_path);
  }

Se puede añadir un grupo directamente con el array de imágenes usando la segunda versión del método CElement::AddImagesGroup(). Aquí, como argumentos, aparte de los márgenes, hay que traspasar el array en el que se indican las rutas hacia los archivos. Después de aumentar el tamaño del array de los grupos a un elemento, el programa añadirá en el ciclo el array entero de las imágenes traspasadas a través del método CElement::AddImage() (см. выше).

class CElement : public CElementBase
  {
public:
   //--- Añadiendo un grupo de imágenes con el array de imágenes
   void              AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[]);
  };
//+------------------------------------------------------------------+
//| Añadiendo un grupo de imágenes con el array de imágenes          |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[])
  {
//--- Obtenemos el tamaño del array de los grupos de imágenes
   uint images_group_total=::ArraySize(m_images_group);
//--- Añadimos un grupo
   ::ArrayResize(m_images_group,images_group_total+1);
//--- Establecer los márgenes para las imágenes
   m_images_group[images_group_total].m_x_gap =x_gap;
   m_images_group[images_group_total].m_y_gap =y_gap;
//--- Imagen predefinida
   m_images_group[images_group_total].m_selected_image=0;
//--- Obtenemos el tamaño del array de las imágenes a añadir
   uint images_total=::ArraySize(file_pathways);
//--- Añadimos las imágenes al grupo nuevo si ha sido traspasado un array no vacío
   for(uint i=0; i<images_total; i++)
      AddImage(images_group_total,file_pathways[i]);
  }

Usted puede colocar o reemplazar la imagen en cualquier grupo durante la ejecución del programa. Utilice para eso el método CElement::SetImage(), traspasando el (1) índice del grupo, (2) índice de la imagen y la (3) ruta hacia el archivo como argumentos. 

class CElement : public CElementBase
  {
public:
   //--- Colocar/reemplazar la imagen
   void              SetImage(const uint group_index,const uint image_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| Colocar/reemplazar la imagen                                     |
//+------------------------------------------------------------------+
void CElement::SetImage(const uint group_index,const uint image_index,const string file_path)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- Eliminar la imagen
   m_images_group[group_index].m_image[image_index].DeleteImageData();
//--- Añadimos una imagen
   m_images_group[group_index].m_image[image_index].ReadImageData(file_path);
  }

Si las imágenes necesarias han sido colocadas durante la creación del control, será mejor simplemente cambiarlas usando el método CElement::ChangeImage():

class CElement : public CElementBase
  {
public:
   //--- Cambiar las imágenes
   void              ChangeImage(const uint group_index,const uint image_index);
  };
//+------------------------------------------------------------------+
//| Cambiar las imágenes                                             |
//+------------------------------------------------------------------+
void CElement::ChangeImage(const uint group_index,const uint image_index)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- Guardar el índice de la imagen a mostrar
   m_images_group[group_index].m_selected_image=(int)image_index;
  }

Usted puede averiguar qué imagen está seleccionada en un momento dado en cualquier grupo usando el método CElement::SelectedImage(). Si no hay ningún grupo o en el grupo especificado no hay imágenes, el método devolverá el valor negativo.

class CElement : public CElementBase
  {
public:
  //--- Devuelve la imagen seleccionada para mostrar en el grupo especificado
   int               SelectedImage(const uint group_index=0);
  };
//+-------------------------------------------------------------------------+
//| Devuelve la imagen seleccionada para mostrar en el grupo especificado   |
//+-------------------------------------------------------------------------+
int CElement::SelectedImage(const uint group_index=0)
  {
//--- Salir si no hay ningún grupo
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- Salir si el grupo especificado no contiene imágenes
   uint images_total=::ArraySize(m_images_group[group_index].m_image);
   if(images_total<1)
      return(WRONG_VALUE);
//--- Devolver la imagen seleccionada para mostrar
   return(m_images_group[group_index].m_selected_image);
  }

Antes, en todas las clases de los controles en los que usuario podía necesitar mostrar el icono, había los métodos para establecer las imágenes. Por ejemplo, a los botones se les podía asignar los iconos en el estado pulsado y suelto, así como para el estado cuando el control estaba bloqueado. Vamos a dejar esta posibilidad, ya que es una opción clara y evidente. Los márgenes para los iconos pueden establecerse como antes, a través de los métodos CElement::IconXGap() y CElement::IconYGap().

class CElement : public CElementBase
  {
protected:
   //--- Márgenes del icono
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //---
public:
   //--- Márgenes del icono
   void              IconXGap(const int x_gap)                       { m_icon_x_gap=x_gap;              }
   int               IconXGap(void)                            const { return(m_icon_x_gap);            }
   void              IconYGap(const int y_gap)                       { m_icon_y_gap=y_gap;              }
   int               IconYGap(void)                            const { return(m_icon_y_gap);            }
   //--- Establecer los iconos para el estado activo y bloqueado
   void              IconFile(const string file_path);
   string            IconFile(void);
   void              IconFileLocked(const string file_path);
   string            IconFileLocked(void);
   //--- Establecer los iconos para el control en el estado pulsado (disponible/bloqueado)
   void              IconFilePressed(const string file_path);
   string            IconFilePressed(void);
   void              IconFilePressedLocked(const string file_path);
   string            IconFilePressedLocked(void);
  };

Como ejemplo, mostraremos el código del método CElement::IconFile(). Aquí, si en el control todavía no hay ningún grupo de imágenes, primero se añade el grupo. Si antes de llamar al método, no han sido establecidos los márgenes, entonces para ellos se establecen los valores cero. Después de agregar el grupo, se añade la imagen traspasada en el argumento, así como se reserva el sitio para la imagen en el estado bloqueado del control.

//+------------------------------------------------------------------+
//| Establecer la imagen para el estado activo                       |
//+------------------------------------------------------------------+
void CElement::IconFile(const string file_path)
  {
//--- Si todavía no hay ningún grupo de imágenes
   if(ImagesGroupTotal()<1)
     {
      m_icon_x_gap =(m_icon_x_gap!=WRONG_VALUE)? m_icon_x_gap : 0;
      m_icon_y_gap =(m_icon_y_gap!=WRONG_VALUE)? m_icon_y_gap : 0;
      //--- Añadimos el grupo y la imagen
      AddImagesGroup(m_icon_x_gap,m_icon_y_gap);
      AddImage(0,file_path);
      AddImage(1,"");
      //--- Imagen predefinida
      m_images_group[0].m_selected_image=0;
      return;
     }
//--- Colocar la imagen en el primer grupo como el primer elemento
   SetImage(0,0,file_path);
  }

Para averiguar el número de los grupos de imágenes o el número de las imágenes en algún grupo, hay que usar los métodos correspondientes (véase el código de abajo):

class CElement : public CElementBase
  {
public:
   //--- Devuelve el número de los grupos de imágenes
   uint              ImagesGroupTotal(void) const { return(::ArraySize(m_images_group)); }
   //--- Devuelve el número de imágenes en el grupo especificado
   int               ImagesTotal(const uint group_index);
  };
//+------------------------------------------------------------------+
//| Devuelve el número de imágenes en el grupo especificado          |
//+------------------------------------------------------------------+
int CElement::ImagesTotal(const uint group_index)
  {
//--- Comprobación del índice del grupo
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- Número de imágenes
   return(::ArraySize(m_images_group[group_index].m_image));
  }


Fusión de los controles en el marco de la optimización del código de la librería

Hasta ahora, muchos controles prácticamente se repetían, siendo únicos sólo en algunos métodos. Eso ha hinchado demasiado el código. Por esa razón, en la librería han sido introducidas modificaciones que simplifican el código y lo hacen más claro para la comprensión.

1. Antes, han sido desarrolladas dos clases para la creación de los botones.

Pero ahora, en cualquier control es posible crear un icono a través de los medios básicos. Por eso, para crear los botones con diferentes propiedades, ya no hace falta disponer de dos clases. Hemos dejado sólo una clase modificada, CButton. Para centrar el texto en el botón, basta con activar el modo correspondiente usando el método CElement::IsCenterText() que se puede aplicar a cualquier control.

class CElement : public CElementBase
  {
protected:
   //--- Modo de alineación del texto
   bool              m_is_center_text;
   //--- 
public:

  //--- Alineación por el centro
   void              IsCenterText(const bool state)                  { m_is_center_text=state;          }
   bool              IsCenterText(void)                        const { return(m_is_center_text);        }
  };


2. Lo mismo se refiere a la creación de los grupos de botones. En las versiones anteriores de la librería, para la creación de los grupos de botones con diferentes propiedades, fueron creadas tres clases:

En todas estas clases, los botones se creaban a base de los objetos gráficos primitivos estándar. Ahora, nos queda sólo la clase CButtonsGroup, y los botones se crean como los controles hechos del tipo CButton

El modo de los botones de opción (cuando uno de los botones del grupo siempre está seleccionado) puede activarse a través del método CButtonsGroup::RadioButtonsMode(). La apariencia como de los botones de opción se habilita a través del método CButtonsGroup::RadioButtonsStyle().

class CButtonsGroup : public CElement
  {
protected:
   //    Modo de botones de opción
   bool              m_radio_buttons_mode;
   //--- Estilo de visualización de los botones de opción
   bool              m_radio_buttons_style;
   //--- 
public:
   //--- (1) Establecer el modo y (2) el estilo de visualización de los botones de opción
   void              RadioButtonsMode(const bool flag)              { m_radio_buttons_mode=flag;       }
   void              RadioButtonsStyle(const bool flag)             { m_radio_buttons_style=flag;      }
  };


3. Vamos a ver las siguientes tres clases para la creación de los controles con campos de edición:

Todas las propiedades de las clases mencionadas podemos meterlas de forma compacta en una sola, que sea la clase CTextEdit. Si hace falta crear un campo de edición numérico con los conmutadores del incremento y decremento, active el modo correspondiente usando el método CTextEdit::SpinEditMode(). Si además es necesario que en el control haya un checkbox, active este modo usando el método CTextEdit::CheckBoxMode(). 

class CTextEdit : public CElement
  {
protected:
   //--- Modo del control con checkbox
   bool              m_checkbox_mode;
   //--- Modo del campo de edición numérico con botones
   bool              m_spin_edit_mode;
   //--- 
public:
   //--- (1) Modos del checkbox y (2) campo de edición numérico
   void              CheckBoxMode(const bool state)          { m_checkbox_mode=state;              }
   void              SpinEditMode(const bool state)          { m_spin_edit_mode=state;             }
  };


4. Lo mismo se refiere a los controles para crear los cuadros combinados (combobox). Antes había dos clases:

Dos clases casi iguales nos sobran, por eso dejamos sólo una de ellas, CComboBox. Para activar el modo con checkbox, se usa el método CComboBox::CheckBoxMode().

class CComboBox : public CElement
  {
protected:
   //--- Modo del control con checkbox
   bool              m_checkbox_mode;
   //--- 
public:
   //--- Establecer el modo del control con checkbox
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };


5. Antes había dos clases para crear los controles tipo Slider:

Ha sido dejada sólo la clase CSlider. El modo del slider doble se activa a través del método CSlider::DualSliderMode().

class CSlider : public CElement
  {
protected:
   //--- Modo del slider doble
   bool              m_dual_slider_mode;
   //--- 
public:
   //--- Modo del slider doble
   void              DualSliderMode(const bool state)           { m_dual_slider_mode=state;           }
   bool              DualSliderMode(void)                 const { return(m_dual_slider_mode);         }
  };


6. Antes había dos clases para crear las listas con barra de desplazamiento, una de las cuales permitía crear una lista con checkbox:

Ahora nos queda sólo CListView, y para crear una lista con los checkbox, basta con activar el modo correspondiente a través del método CListView::CheckBoxMode().

class CListView : public CElement
  {
protected:
   //--- Modo de la lista con los checkbox
   bool              m_checkbox_mode;
   //--- 
public:
   //--- Establecer el modo del control con checkbox
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };


7. En la versión anterior de la librería había de hasta tres clases de las tablas:

Durante el proceso de las mejoras de la librería, la clase CCanvasTable ha resultado ser la más desarrollada. Por eso las demás clases han sido eliminadas, mientras que la clase CCanvasTable ha sido renombrada en CTable.


8. Antes había dos clases para crear las pestañas:

Tampoco es necesario dejar las dos clases. Antes el array de los botones-pestañas se creaba a partir de los objetos primitivos. Ahora, para eso se usan los botones tipo CButton, en los cuales se puede determinar los iconos a través de los métodos base descritos anteriormente. Como resultado, dejemos sólo la clase CTabs.


Jerarquía de los controles

También ha sido cambiada la jerarquía de los controles de la interfaz gráfica. Hasta ahora, todos los controles se vinculaban al formulario (CWindow). Los controles se ubicaban en el formulario respecto a su punto superior izquierdo. A la hora de crear la interfaz gráfica, hay que especificar las coordenadas para cada uno de los controles. Y si en el proceso de la actualización, se cambiaba la posición del control, entonces para todos los controles que debían encontrarse dentro de una determinada área en este control, era necesario volver a establecer manualmente las coordenadas respecto a esta área. Por ejemplo, en el área del control «Pestañas» se encuentran los grupos de otros controles. Y si tuviéramos que mover las «Pestañas» a otra parte del formulario, entonces todos los controles de cada pestaña se quedarían en sus posiciones anteriores. Es muy inconveniente, pero ahora este problema está solucionado.

Anteriormente, antes de crear algún control en la clase personalizada de la aplicación MQL, era necesario obligatoriamente pasarle, usando el método CElement::WindowPointer(), el objeto del formulario para guardar el puntero al formulario. Ahora para eso se utiliza el método CElement::MainPointer(). Como argumento, él traspasa el objeto del control al que es necesario vincular el control creado. 

class CElement : public CElementBase
  {
protected:
   //--- Puntero al control principal
   CElement         *m_main;
   //--- 
public:
   //--- Guarda y devuelve el puntero al control principal
   CElement         *MainPointer(void)                               { return(::GetPointer(m_main));    }
   void              MainPointer(CElement &object)                   { m_main=::GetPointer(object);     }
  };

Igual como antes, será imposible crear un control si éste no está vinculado al control principal. La presencia del puntero al control principal se comprueba en el método principal de la creación de los controles (en todas las clases). El método para esta comprobación ha sido renombrado y ahora se llama CElement::CheckMainPointer(). Aquí mismo se guarda el puntero al formulario y el puntero al objeto del cursor del ratón. Además, se determina y se guarda el identificador del control, se guardan el identificador del gráfico y el número de la subventana a los que se adjunta la aplicación MQL y su interfaz gráfica. Antes, este código se repetía en cada clase.

class CElement : public CElementBase
  {
protected:
   //--- Comprobación de la presencia del puntero al control principal
   bool              CheckMainPointer(void);
  };
//+------------------------------------------------------------------+
//| Comprobación de la presencia del puntero al control principal    |
//+------------------------------------------------------------------+
bool CElement::CheckMainPointer(void)
  {
//--- Si no hay puntero
   if(::CheckPointer(m_main)==POINTER_INVALID)
     {
      //--- Mostrar el mensaje en el registro del terminal
      ::Print(__FUNCTION__,
              " > Antes de crear el control... \n...hay que traspasar el puntero al control principal: "+
              ClassName()+"::MainPointer(CElementBase &object)");
      //--- Interrumpir la construcción de la interfaz gráfica de la aplicación
      return(false);
     }
//--- Guardando el puntero al formulario
   m_wnd=m_main.WindowPointer();
//--- Guardando el puntero al cursor del ratón
   m_mouse=m_main.MousePointer();
//--- Guardando las propiedades
   m_id       =m_wnd.LastId()+1;
   m_chart_id =m_wnd.ChartId();
   m_subwin   =m_wnd.SubwindowNumber();
//--- Enviar el indicio de la presencia del puntero
   return(true);
  }

Este método del anclaje de los controles al control principal se extiende en todas las clases. Los controles complejos se componen de varios controles, y todos ellos se vinculan unos a otros guardando una determinada sucesión. Excepto las siguientes tres clases:

Se componen de varios objetos gráficos tipo OBJ_BITMAP_LABEL, cuyo contenido también se dibuja. El testeo de diferentes enfoques ha demostrado que el uso de varios objetos es una manera óptima para esta fase del desarrollo de la librería.

La clase CButton ha sido en su especie un «ladrillo» universal que se usa prácticamente en todos los demás controles de la librería. Además de eso, ahora el control CButton (botón) es básico para algunas otras clases de los controles, tales como: 

Al fin y al cabo, ahora el esquema general de las interconexiones entre las clases en el formato «base - derivada» (padre - heredero) es el siguiente:

Fig. 1. Esquema de interconexiones entre las clases como «base - derivada» (padre - heredero).

Fig. 1. Esquema de interconexiones entre las clases en el formato «base - derivada» (padre - heredero).


En este momento, la librería cuenta con 33 (treinta y tres) diferentes controles. A continuación, se muestra una serie de esquemas que incluyen todos los controles de la librería en orden alfabético. Los controles raíces están marcados en verde y están numerados. Luego se muestran los controles incluidos a toda la profundidad de la inclusión. Cada columna es la siguiente capa de los controles incluidos para algún control. Los controles que tienen varias instancias están marcados con el símbolo ‘[]’. Esta visión permitirá al lector comprender el esquema de programación orientada a objetos de la librería de forma más rápida.

Las imágenes de abajo incluyen los esquemas de los siguientes controles:

1. CButton — botón.
2. CButtonsGroup — grupo de n¡botones.
3. CCalendar — calendario.
4. CCheckBox — casilla de verificación (checkbox).
5. CColorButton — botón para abrir la paleta de colores.
6. CColorPicker — paleta de colores.
7. CComboBox — botón para abrir la lista desplegable (combobox).
8. CContextMenu — menú contextual.

Fig. 2. Representación esquemática de los controles (parte 1). 

Fig. 2. Representación esquemática de los controles (parte 1).


9. CDropCalendar — botón para abrir el calendario desplegable.
10. CFileNavigator — explorador de archivos.
11. CLineGraph — gráfico lineal.
12. CListView — lista.

Fig. 3. Representación esquemática de los controles (parte 2).

Fig. 3. Representación esquemática de los controles (parte 2).


13. CMenuBar — menú principal.
14. CMenuItem — elemento del menú.
15. CPicture — imagen.
16. CPicturesSlider — slider de imágenes.
17. CProgressBar — indicador de progreso.
18. CScroll — barra de desplazamiento.
19. CSeparateLine — línea separadora.
20. CSlider — slider numérico.
21. CSplitButton — botón de división.
22. CStandartChart — gráfico estándar.
23. CStatusBar — barra de estado.
24. CTable — tabla.
 


Fig. 4. Representación esquemática de los controles (parte 3).

Fig. 4. Representación esquemática de los controles (parte 3).



25. CTabs — pestañas.
26. CTextBox — campo de edición de texto con posibilidad de activar el modo de multilíniea.
27. CTextEdit — campo de edición.
28. CTextLabel — etiqueta de texto.
29. CTimeEdit — control para introducir la hora.
30. CTooltip — descripción emergente.
31. CTreeItem — elemento de la lista jerárquica.
32. CTreeView — lista jerárquica.
33. CWindow — formulario (ventana) para los controles.
 

Fig. 5. Representación esquemática de los controles (parte 4).

Fig. 5. Representación esquemática de los controles (parte 4).



Array de los controles incluidos

Hablando de la versión anterior de la librería, los punteros a los objetos de los objetos primitivos gráficos se guardaban en la clase base de los controles a la hora de crearlos. Ahora, van a guardarse los punteros a los controles que forman parte de uno u otro control de la interfaz gráfica. 

Los punteros a los controles se añaden al array dinámico tipo CElement a través del método CElement::AddToArray() protegido (protected). Este método sirve para las necesidades internas y va a utilizarse sólo en las clases de los controles. 

class CElement : public CElementBase
  {
protected:
   //--- Punteros a los controles incluidos
   CElement         *m_elements[];
   //---
protected:
   //--- Método para añadir los punteros a los a los controles incluidos
   void              AddToArray(CElement &object);
  };
//+------------------------------------------------------------------+
//| Método para añadir los punteros a los a los controles incluidos  |
//+------------------------------------------------------------------+
void CElement::AddToArray(CElement &object)
  {
   int size=ElementsTotal();
   ::ArrayResize(m_elements,size+1);
   m_elements[size]=::GetPointer(object);
  }

Está previsto obtener el puntero del control desde el array según el índice especificado. Para eso se utiliza el método público (public) CElement::Element(). Además, hay métodos para obtener el número de los controles incluidos y para vaciar el array.

class CElement : public CElementBase
  {
public:
   //--- (1) Obtener el número de los controles incluidos, (2) vaciar el array de los controles incluidos
   int               ElementsTotal(void)                       const { return(::ArraySize(m_elements)); }
   void              FreeElementsArray(void)                         { ::ArrayFree(m_elements);         }
   //--- Devuelve el puntero del control incluido según el índice especificado
   CElement         *Element(const uint index);
  };
//+------------------------------------------------------------------+
//| Devuelve el puntero del control incluido                         |
//+------------------------------------------------------------------+
CElement *CElement::Element(const uint index)
  {
   uint array_size=::ArraySize(m_elements);
//--- Comprobar tamaño del array de objetos
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > En este control ("+m_class_name+") no hay controles incluidos!");
      return(NULL);
     }
//--- Ajuste en caso de salir fuera del diapasón
   uint i=(index>=array_size)? array_size-1 : index;
//--- Devolver el puntero del objeto
   return(::GetPointer(m_elements[i]));
  }


Código base de los métodos virtuales

Ha sido determinado el código base de los métodos virtuales que sirven para la gestión de los controles. A estos métodos les pertenece:

Ahora cada control se dibuja en un objeto gráfico separado. Por eso se puede hacer que estos métodos sean universales, evitando de esta manera múltiples repeticiones en todas las clases de los controles. Hay casos cuando algunos de estos métodos serán redeterminados en las clases de algunos controles. Precisamente para eso, han sido declarados como virtuales. Por ejemplo, con estos controles se puede relacionar CListView, CTable y CTextBox. Antes ya hemos dicho que en la versión actual sólo estos controles de la librería se componen de varios objetos gráficos dibujados. 

Como ejemplo, mostraremos el método CElement::Moving(). Ahora ya no tiene argumentos. Antes, recibía las coordenadas y el modo en que podía trabajar. Pero eso ya no es necesario ya que todo es mucho más fácil. Eso está relacionado con importantes cambios en el núcleo de la librería (clase CWndEvents), pero lo discutiremos en uno de los siguientes apartados del artículo. 

El control se desplaza respecto a la posición actual del control principal al que está vinculado. Después de que el objeto gráfico del control ha sido desplazado, luego desplazamos en el ciclo otros controles incluidos en él, si exiten. Prácticamente en todos los controles, se invoca la misma copia del método CElement::Moving(). Y así consecutivamente, en todos los niveles de anidación. Es decir, si hace falta mover algún control, basta con llamar una vez al método, y los métodos semejantes de otros controles van a invocarse automáticamente (consecutivamente, en orden de su creación). 

El listado de abajo muestra con detalles el código del método virtual CElement::Moving():

class CElement : public CElementBase
  {
public:
   //--- Desplazamiento del control
   virtual void      Moving(void);
  };
//+------------------------------------------------------------------+
//| Desplazamiento del control                                       |
//+------------------------------------------------------------------+
void CElement::Moving(void)
  {
//--- Salir si el control está ocultado
   if(!CElementBase::IsVisible())
      return;
//--- Si el anclaje está a la derecha
   if(m_anchor_right_window_side)
     {
      //--- Guardar las coordenadas en los campos del control
      CElementBase::X(m_main.X2()-XGap());
      //--- Guardar las coordenadas en los campos de los objetos
      m_canvas.X(m_main.X2()-m_canvas.XGap());
     }
   else
     {
      CElementBase::X(m_main.X()+XGap());
      m_canvas.X(m_main.X()+m_canvas.XGap());
     }
//--- Si el anclaje está abajo
   if(m_anchor_bottom_window_side)
     {
      CElementBase::Y(m_main.Y2()-YGap());
      m_canvas.Y(m_main.Y2()-m_canvas.YGap());
     }
   else
     {
      CElementBase::Y(m_main.Y()+YGap());
      m_canvas.Y(m_main.Y()+m_canvas.YGap());
     }
//--- Actualizando las coordenadas de objetos gráficos
   m_canvas.X_Distance(m_canvas.X());
   m_canvas.Y_Distance(m_canvas.Y());
//--- Desplazamiento de controles incluidos
   int elements_total=ElementsTotal();
   for(int i=0; i<elements_total; i++)
      m_elements[i].Moving();
  }


Determinación automática de las prioridades para el clic izquierdo del ratón

En la versión anterior de la librería, las prioridades para el clic izquierdo del ratón se escribían manualmente en las clases de cada control de la librería. Ahora, cuando cada control es un objeto gráfico, es posible implementar el modo automático para determinar las prioridades. Cada objeto gráfico que se encuentra por encima de otro objeto, adquiere la prioridad más alta:

Fig. 6. Representación visual de la determinación de las prioridades para el clic izquierdo del ratón. 

Fig. 6. Representación visual de la determinación de las prioridades para el clic izquierdo del ratón.


Pero hay controles que no tienen su propio objeto gráfico. Como ya hemos mencionado antes, hay controles que se componen de varios objetos dibujados. En ambos casos, la prioridad se corrige en la clase de cada control en concreto. Primero, vamos a determinar los controles donde esta corrección será necesaria.

Controles sin objetos gráficos:

Controles con varios objetos gráficos:

Para establecer y obtener la prioridad, a la clase CElement han sido añadidos los métodos correspondientes:

class CElement : public CElementBase
  {
protected:
   //--- Prioridad para el clic izquierdo del ratón
   long              m_zorder;
   //---
public:
   //--- Prioridad para el clic izquierdo del ratón
   long              Z_Order(void)                             const { return(m_zorder);                }
   void              Z_Order(const long z_order);
  };
//+------------------------------------------------------------------+
//| Prioridad para el clic izquierdo del ratón                       |
//+------------------------------------------------------------------+
void CElement::Z_Order(const long z_order)
  {
   m_zorder=z_order;
   SetZorders();
  }

Los controles sin objetos gráficos son en su especie unos módulos de gestión a los que se conectan otros controles. En este control no hay nada a que se puede establecer la prioridad. Pero la prioridad de los controles incluidos se calcula respecto a la prioridad del control principal. Por eso, para que todo funcione correctamente, hay que establecer todo de forma manual.

La prioridad para los controles sin objetos se establece el mismo que para su control principal:

...
//--- La prioridad es la misma que tiene el control principal, ya que el control no tiene su área de pulsación
   CElement::Z_Order(m_main.Z_Order());
...

Para todos los demás controles, la prioridad se establece después de la creación del objeto para el dibujado. Para los formularios, este valor es igual a cero. Luego, para todos los controles creados posteriormente, el valor se incrementa en 1 respecto al control principal:

...
//--- Todos los controles, excepto el formulario, tienen la prioridad mayor que el control principal
   Z_Order((dynamic_cast<CWindow*>(&this)!=NULL)? 0 : m_main.Z_Order()+1);
...

Como ejemplo, vamos a analizar otro esquema. La interfaz gráfica va a componerse de los siguientes controles (van en orden de su creación):

No es necesario hacer ningunas acciones para establecer las prioridades para los objetos de la interfaz gráfica. Todo se hará automáticamente según el siguiente esquema:

Fig. 7. Ejemplo de determinación de las prioridades para el clic izquierdo del ratón. 

Fig. 7. Ejemplo de determinación de las prioridades para el clic izquierdo del ratón.


El formulario recibe la prioridad más baja con el valor 0 (cero). Los botones en el formulario tienen la prioridad con el valor 1

Cada botón que forma parte del control tipo CTabs (pestañas) recibe la prioridad 1, y el área de trabajo de las pestañas también recibe la prioridad 1. Pero en control CButtonsGroup va a guardarse el valor 0, ya que no dispone de su propio objeto gráfico, es simplemente un módulo de gestión para los botones tipo CButton. En la clase personalizada de la aplicación MQL, utilice el método CElement::MainPointer() para indicar el control principal (véase el fragmento del código de abajo). Aquí, del control principal va a actuar el formulario (CWindow), al que se adjunta el control CTabs. Es necesario guardar el puntero antes de llamar al método de la creación de los controles.

...
//--- Guardamos el puntero al control principal
   m_tabs1.MainPointer(m_window);
...

La lista obtiene la prioridad 2, ya que su control principal aquí es CTabs. Y hay que indicar eso obligatoriamente antes de crear el control:

...
//--- Guardamos el puntero al control principal
   m_listview1.MainPointer(m_tabs1);
...

No es necesario indicar el control principal para la barra de desplazamiento, porque eso ya está implementado dentro de la clase de la lista (CListView). Lo mismo se refiere a todos los demás controles de la librería que forman parte de otros controles. Si para la barra de desplazamiento, el control principal es la lista (CListView) cuya prioridad es 2, el valor se aumenta en uno (3). Y para los botones de la barra de desplazamiento, siendo ésa el control principal para ellos, el valor será 4.

Igual que en la lista (CListView), todo funciona también en el control CTextBox.


Aplicación para la prueba de controles

Para las pruebas ha sido implementada una aplicación MQL cuya interfaz gráfica incluye todos los controles de la librería, para que Usted pueda ver cómo funciona todo eso. A continuación, se puede ver cómo es: 

Fig. 12. Interfaz gráfica de la aplicación MQL.

Fig. 12. Interfaz gráfica de la aplicación MQL.


Puede descargar esta aplicación de prueba al final del artículo para estudiarla más detalladamente.



Conclusión

Esta versión de la librería tiene unas diferencias considerables de la que ha sido presentada en el artículo Interfaces gráficas: Selección del texto en el campo de edición multilínea (build 13). Ha sido realizado un gran trabajo que ha influido prácticamente en todos los archivos de la librería. Ahora, todos los controles de la librería se dibujan en los objetos separados. La legibilidad del código se ha mejorado, su volumen se ha reducido aproximadamente a un 30% y las posibilidades se han aumentado. Ha sido corregida una serie de errores y problemas de los que han avisado los usuarios. 

Si ya ha empezado a crear sus aplicaciones MQL usando la versión anterior de la librería, se recomienda primero descargar la nueva versión en una copia del terminal MetaTrader 5 instalada separadamente con el fin de familiarizarse con ella y probar todo minuciosamente.

En esta fase del desarrollo de la librería para la creación de las interfaces gráficas, su esquema general tiene el siguiente aspecto. No es una versión definitiva, la librería va a seguir desarrollando y mejorándose en el futuro.

 Fig. 13. Estructura de la librería en la fase actual del desarrollo.

Fig. 13 Estructura de la librería en la fase actual del desarrollo.


Si le surgen algunas preguntas sobre el uso del material de estos archivos, puede dirigirse a la descripción detallada del proceso de desarrollo de la librería en uno de los artículos de esta serie, o bien hacer su pregunta en los comentarios para el artículo.