Controles gráficos personalizados. Parte 1: Creando un control simple

Dmitry Fedoseev | 23 diciembre, 2013


Introducción

El lenguaje MQL5 proporciona a los desarrolladores un amplio rango de objetos gráficos programáticamente controlados: un botón, una etiqueta de texto, un campo de edición, una etiqueta bitmap (fig.1) y distintas herramientas gráficas para el análisis (fig. 2).


Fig. 1. Objetos gráficos: un botón, una etiqueta de texto, un campo de edición, una etiqueta bitmap


Fig. 2. Algunos objetos gráficos para el análisis: una elipse, el ventilador de Fibonacci y la expansión de Fibonacci

Hay en total más de 40 objetos gráficos en el terminal de cliente de Meta Trader 5. Todos estos objetos pueden usarse separadamente, pero lo más habitual es que sean usados en una cadena de objetos interconectados. Por ejemplo, cuando se usa un campo de edición (OBJ_EDIT), a menudo se usa conjuntamente con una etiqueta bitmap (OBJ_LABEL) para indicar la función del campo de edición.

Cuando se usa un campo de edición, a menudo es necesario verificar la corrección de los datos introducidos por un usuario, así como ofrecer la posibilidad de usar un punto y una coma como separador decimal.

Cuando se usa una salida de datos programática se debe dar formato a los datos. Por ejemplo, se deben borrar los ceros que no sean necesarios. Por tanto, sería más sencillo tener un objeto que incluyera el campo de edición, la etiqueta bitmap y algunas otras características funcionales.

Actualmente hay un cierto número de controles gráficos en el mundo de la programación que son usados en casi todas las aplicaciones: un formulario (la base de la interfaz de una aplicación, donde se ubican todos los elementos de control), un frame (que permite agrupar y separar conjuntos de elementos que tienen un propósito funcional), un botón, un campo de edición, una etiqueta, una casilla de verificación, un botón de radio, barras de desplazamiento verticales y horizontales, una lista, una lista desplegable, una fila de menú, una pestaña de menú (fig. 3). 

 
Fig. 3. El formulario con los controles estándar más comunes

La forma en que se representan los elementos citados anteriormente en MQL5 es similar a la de otros lenguajes de programación (un botón y un campo de edición). Sería conveniente tener otros controles en nuestro arsenal.

Varios entornos de programación proporcionan a los programadores herramientas especiales para la creación de controles personalizados. MQL5 no proporciona esta característica. Sin embargo, al ser MQL5 un lenguaje orientado a objeto, no es necesario tenerla. Todo puede realizarse en la forma de un objeto programado independiente.

Los principios y la metodología para la creación de controles personalizados serán tratados más adelante en este artículo. Básicamente, cualquiera que tenga cierto dominio de la programación puede crear el conjunto de controles necesario y usarlos repetidamente en sus aplicaciones.


1. Lo que debería ser un control gráfico

1.1. Requisitos y principios generales

Para que los controles gráficos sean útiles deberían facilitar el desarrollo de aplicaciones. Para ello, deben satisfacer los siguientes requisitos:

  1. Debe ser posible crear rápidamente un control durante el desarrollo. Este problema se soluciona usando el enfoque orientado a objeto en la programación. Un control gráfico se representa como un objeto programado.
     
  2. Un control debe ser flexible. En otras palabras, debe ser posible cambiar sus propiedades: tamaño, posición, color, etc.
     
  3. Un control debe ser fácil de usar -debe tener solo las propiedades y métodos necesarias, cuyo propósito está claramente delimitado a partir de los objetivos de cada elemento y los nombres de los métodos. Vamos ahora a dividir las propiedades de los controles en categorías:
     
    1. Propiedades no controladas. Estas propiedades incluyen el esquema de colores. Todos los controles usados en una aplicación deben tener un estilo general. Por esta razón, establecer los colores para cada control de forma individual podría llegar a ser agotador. Además, buscar un color para algunos controles es una ardua tarea en la que no quiero perder el tiempo. Por ejemplo, una barra de desplazamiento. Puede que algunos desarrolladores web se hayan encontrado con la necesidad de realizar esta interesante tarea.
       
    2. Propiedades establecidas en la etapa de creación de un control o propiedades modificadas en pocas ocasiones. Por ejemplo, el tamaño de un control. El equilibrio y la posición adecuada de todos los controles utilizados en una aplicación requiere una tarea dura y específica que solo se resuelve en la etapa de creación de la interfaz. Con relación a esto, el tamaño de los controles no cambia durante la operación del programa. Sin embargo, algunas veces podemos necesitar cambiar estas propiedades. Por eso debemos permitir la posibilidad de cambiar estas propiedades durante la operación del programa.
       
    3. Principales propiedades operativas Son propiedades que cambian con frecuencia en el programa. Propiedades que constituyen el objetivo de un control. Estas propiedades pueden dividirse en dos categorías:
       
      1. Propiedades con actualización automática en pantalla de un control. Por ejemplo, un campo "edit". Una vez que se establece el valor por el programa, los camios deben mostrarse en la pantalla. En programación, esto se debe realizar usando una sola línea de código.
         
      2. Propiedades que requieren una actualización forzosa de la representación en pantalla. Por ejemplo, una lista. Las listas implican trabajar con matrices de datos y por ello las listas no deben actualizarse después de trabajar con un único elemento de la lista. Aquí, es mejor realizar una actualización forzosa al final, trabajando con todos los elementos de la lista. Este enfoque incrementa el rendimiento de la aplicación considerablemente.
         
  4. Una posibilidad de ocultar y mostrar un control de forma rápida y simple. Hacer un control visible no requiere establecer las propiedades de representación en pantalla de forma repetida. El acceso a las propiedades del objeto debe ser independiente de la visibilidad de los objetos gráficos de este control. En otras palabras, un objeto programado debe contener todas las propiedades del control en él mismo y no debe usar las propiedades de los objetos gráficos.
      
  5. Para tener resultados de eventos que correspondan al control, debe haber un procesado de eventos de objetos gráficos individuales que estén incluidos en el control.

1.2. Forma de usar un control y métodos necesarios 

Teniendo en cuenta los requisitos mencionados anteriormente, tenemos el siguiente esquema de creación de una interfaz gráfica así como el conjunto de propiedades y métodos necesarios de un objeto programado:

  1. Inicialización de un control y establecimiento paralelo de propiedades que cambian en raras ocasiones. Este método se llamará Init() y tiene varios parámetros. El primero de ellos y obligatorio es el nombre de un control. Este parámetro se usará como prefijo para los nombres de todos los objetos gráficos incluidos en el control. Además, pueden incluirse los parámetros que establecen el tamaño del control y otros parámetros (dependiendo del propósito del control).
     
  2. Los controles pueden tener una ubicación fija en un gráfico y también pueden requerir la posibilidad de ser desplazados. Por tanto, vamos a usar métodos separados para determinar coordenadas: SetPosLeft() - establece la coordenada X , SetPosTop() - establece la coordenada Y. Cada uno de estos métodos debe tener un parámetro. Con frecuencia es necesario cambiar ambas coordenadas y, por tanto, es bueno tener el método SetPos() con dos parámetros, con cambios en las coordenadas X e Y simultáneamente.
    Para calcular la ubicación de un control podemos necesitar obtener información sobre el tamaño y la ubicación de otro control. Para este propósito vamos a usar los siguientes métodos: Width() - ancho, Height() - alto, Left() - la coordenada X , Top() - la coordenada Y. En esta fase de la operación se calculan las coordenadas del control y se llama a los métodos que las establecen.
     
  3. Justo después de la creación o en otra etapa del funcionamiento de la aplicación, necesitamos hacer visible el control. Para hacer esto, se usa el método Show(). Para ocultar el control se usa el método Hide().
     
  4. Como se ha mencionado antes, durante el funcionamiento del programa puede que necesitemos cambiar el tamaño del control. Por esta razón, el objeto programado debe tener métodos separados para establecer el tamaño - SetWidth() y/o SetHeght(). Debido a que estas propiedades se modifican muy raramente, para que los cambios tengan efecto necesitamos llamar al método Refresh(), que actualiza la pantalla.

  5. Los eventos de objetos gráficos individuales serán procesados en el método Event(), que devolverá un valor correspondiente a un evento específico del control. El método Event() debe llamarse desde la función OnChartEvent(). Tendrá el mismo conjunto de parámetros que la función OnChartEvent().

De esta forma, tenemos el conjunto de métodos necesarios para un objeto programado:

La presencia de otros métodos (o ausencia de alguno de los métodos listados) depende del propio control y su propósito.

Más adelante, en este artículo, vamos a intentar implementar los principios descritos anteriormente. Vamos a crear un control para que un usuario introduzca texto o números.

Pero antes necesitamos hacernos con las herramientas necesarias para poder trabajar de forma rápida y adecuada con objetos gráficos.


2. Cómo trabajar con objetos gráficos de forma rápida y adecuada

Para el trabajo con objetos gráficos MQL5 dispone de las siguientes funciones gráficas: ObjectCreate(), ObjectDelete(), ObjectSetDouble(), ObjectSetInteger(), ObjectSetString(), ObjectGetDouble(), ObjectGetInteger(), ObjectGetString(). Podemos usar estas funciones directamente, pero el proceso de programación será muy laborioso y largo. Las funciones tienen nombres largos y deben pasarse a estas funciones un montón de identificadores que tienen a su vez nombres largos.

Para hacer más práctico el trabajo con objetos gráficos podemos usar una clase ya diseñada que viene incluida en el paquete del terminal de cliente de Meta Trader 5, (la clase CChartObject del archivo MQL5/Include/ChartObjects/ChartObject.mqh), o podemos escribir nuestra propia clase y darle los métodos necesarios.

Hablando en broma, este enfoque sobre la programación consiste en presionar una tecla con un punto después. Tras especificar el nombre de un objeto se le deja poner un punto y se abre la lista de sus propiedades y métodos. Elija el elemento que necesite de la lista (fig. 4).


Fig. 4. La lista de propiedades y métodos del objeto

Hay dos formas de gestionar un objeto gráfico usando clases auxiliares:

  1. Se crea una instancia de una clase individual para cada objeto gráfico. Un método muy adecuado pero poco conveniente por la cantidad de memoria consumida. Para esta forma es mejor escribir clases especiales para cada tipo de objeto gráfico. Pero este enfoque no es óptimo ya que es muy laborioso. A diferencia de la programación de asesores expertos,no existe un requisito estricto sobre un rendimiento máximo al crear la interfaz de usuario.
  2. Use una instancia de clase. Si se necesita para gestionar un objeto gráfico, el objeto se añade a la clase. Vamos a usar la segunda variante.

Vamos a crear una clase universal que es más adecuada para el segundo tipo de gestión de objetos gráficos.


3. Clase universal para gestionar los objetos gráficos

Durante la programación, el trabajo con cada objeto gráfico consta de tres fases: creación, lectura/establecimiento de propiedades y borrado al final del funcionamiento de la aplicación.

Por tanto, antes que nada, la clase para gestionar los objetos gráficos debe contener los métodos que los crean. Un parámetro obligatorio para la creación de objetos gráficos es su nombre. Por ello, los métodos de creación de objetos tendrán un parámetro obligatorio para especificar el nombre del objeto creado.

Habitualmente, los objetos gráficos se crean en el gráfico donde se ejecuta el programa (asesor experto, indicador o script). Una caso menos habitual es una subventana, y otro todavía mas raro es otra ventana de gráfico del terminal. De esta forma, el segundo parámetro opcional será aquel que especifique el número de una subventana, y el tercero es el identificador de un gráfico.

Por defecto, ambos parámetros opcionales serán iguales a 0 (el gráfico de precio está en su "propio" gráfico). Consulte la lista de tipos de objetos gráficos en la documentación. Añada el método Create para cada tipo.

Pero antes, necesita crear un archivo. Para hacer esto, abra MetaEditor, cree un nuevo archivo include y llámelo IncGUI.mqh. En el archivo abierto, cree la clase CGraphicObjectShell con las secciones protegidas y públicas. En la sección protegida, declare las variables para el nombre de un objeto y el identificador de un gráfico.

En los métodos de creación de objetos a aquellas variables se les asignarán los valores pasados por los métodos como parámetros para tener la posibilidad de gestionarlos tras la creación del objeto sin tener que especificar un nombre y un identificador de gráfico. De esta forma, podemos también usar la clase para la primera opción de la gestión de objetos gráficos.

Para poder usar la clase para la segunda variante (gestionar cualquier objeto gráfico) hay que darle un método para adjuntar un objeto gráfico (el método Attach()). Este método tendrá un parámetro obligatorio (el nombre de un objeto gráfico) y un parámetro opcional (un identificador de gráfico). Puede que necesite conocer el nombre y el identificador de un objeto gráfico adjunto. Para hacer esto añada los siguientes métodos al objeto: Name() y ChartID().

Como resultado, obtendremos el siguiente fragmento de la clase: 

class CGraphicObjectShell
  {
protected:
   string            m_name;
   long              m_id;
public:
   void Attach(string aName,long aChartID=0)
     {
      m_name=aName;
      m_id=aChartID;
     }
   string Name()
     {
      return(m_name);
     }    
   long ChartID()
     {
      return(m_id);
     }
  };

Añada los métodos anteriores de creación de objetos gráficos. Los nombres de dichos métodos empezarán con "Create".

Aunque el lector no necesita hacerlo, ya que el archivo IncGUI.mqh adjunto a este artículo contiene ya la clase CGraphicObjectShell.

Como ejemplo, a continuación se muestra un método de creación del objeto gráfico de línea vertical (OBJ_VLINE):

void CreateVLine(string aName,int aSubWindow=0,long aChartID=0)
  {
   ObjectCreate(m_id,m_name,OBJ_VLINE,aSubWindow,0,0);
   Attach(aName,aChartID);
  }

Ahora abra la lista de propiedades de objetos gráficos en la guía de usuario y escriba los métodos para establecer un valor para cada propiedad usando las funciones ObjectSetDouble(), ObjectSetInteger() y ObjectSetString(). Los nombres de dichos métodos empezarán con "Set". A continuación, escriba los métodos para leer las propiedades usando las funciones ObjectGetDouble(), ObjectGetInteger() y ObjectGetString().

Como ejemplo, estos son algunos métodos para establecer y obtener el color:

void SetColor(color aColor)
  {
   ObjectSetInteger(m_id,m_name,OBJPROP_COLOR,aColor);
  }
color Color()
  {
   return(ObjectGetInteger(m_id,m_name,OBJPROP_COLOR));
  }

Bien, ahora tenemos las herramientas mínimas necesarias para trabajar con objetos gráficos, pero no tenemos todas.

En ocasiones, al trabajar con un objeto gráfico puede necesitar realizar tan solo una acción con un objeto. En este caso, no es conveniente ejecutar el método Attach() para el objeto y luego volver al objeto principal y ejecutar de nuevo Attach().

Vamos a añadir dos variantes más de los métodos para establecer/obtener propiedades con relación a la clase.

El primero es por el nombre en su "propio gráfico":

void SetColor(string aName,color aColor)
  {
   ObjectSetInteger(0,aName,OBJPROP_COLOR,aColor);
  }
color Color(string aName)
  {
   return(ObjectGetInteger(0,aName,OBJPROP_COLOR));
  }

El segundo es por el nombre e identificador de un gráfico:

void SetColor(long aChartID,string aName,color aColor)
  {
   ObjectSetInteger(aChartID,aName,OBJPROP_COLOR,aColor);
  }
color Color(long aChartID,string aName)
  {
   return(ObjectGetInteger(aChartID,aName,OBJPROP_COLOR));
  }

Además de las funciones ObjectGet y ObjectSet, hay otras funciones para trabajar con objetos gráficos: ObjectDelete(), ObjectMove(), ObjectFind(), ObjectGetTimeByValue(), ObjectGetValueByTime(), ObjectsTotal(). Estas también pueden añadirse a la clase con tres variantes de llamada a cada una de ellas.

Finalmente, declare la clase CGraphicObjectShell con un simple y corto nombre "g" en este archivo. 

CGraphicObjectShell g;

Ahora, para empezar a trabajar con objetos gráficos es suficiente conectar el archivo IncGUI.mqh, y así tendremos la clase "g" para poder trabajar con ella. Usándola, es fácil gestionar todos los objetos gráficos disponibles.


4. Fragmentos de código para los controles

A pesar de que tenemos la clase para trabajar rápidamente con objetos gráficos, podemos crear controles de forma sencilla. Todos los controles pueden ser creados sobre la base de cuatro objetos gráficos.

  1. Etiqueta rectángulo (OBJ_RECTANGLE_LABEL),
  2. Etiqueta texto (OBJ_LABEL),
  3. Campo "Edit" (OBJ_EDIT),
  4. Botón (OBJ_BUTTON).

Tras la creación de objetos gráficos pueden establecerse muchas propiedades para ellos: coordenadas, tamaño, color, tamaño de fuente, etc. Para aligerar el proceso, vamos a crear otra clase y la llamaremos CWorkPiece, y le proporcionaremos métodos para la creación de objetos gráficos con propiedades pasadas como parámetros. 

Para que los controles funcionen necesitamos gestionar los eventos de gráfico. Los eventos de otros gráficos no se encuentran disponibles, por lo que vamos a trabajar solo con el propio gráfico y no habrá identificador de gráfico en los parámetros de los métodos de la clase CWorkPiece. 0 (propio gráfico) se usará en todas partes.

El parámetro que especifica un número de subventana se usará para ofrecer la posibilidad de crear controles tanto en el gráfico de precio como en sus subventanas. Los objetos gráficos estarán limitados solo a la esquina superior izquierda. Si es necesario reubicar un control con relación a alguna otra esquina, es mucho más fácil recalcular las coordenadas de todo el control considerando el tamaño del gráfico. Para controlar los cambios en el tamaño del gráfico puede usar el evento CHARTEVENT_CHART_CHANGE.

Como base para muchos controles, vamos a usar el objeto "etiqueta rectángulo". Añadimos el método de creación de este objeto a la clase CWorkPiece. El método se llama Canvas():

void Canvas(string aName="Canvas",
             int aSubWindow=0,
             int aLeft=100,
             int aTop=100,
             int aWidth=300,
             int aHeight=150,
             color aColorBg=clrIvory,
             int aColorBorder=clrDimGray)
  {
   g.CreateRectangleLabel(aName,aSubWindow); // Creation of rectangle label
   g.SetXDistance(aLeft);                    // Estableciendo la coordenada X
   g.SetYDistanse(aTop);                     // Estableciendo la coordenada Y
   g.SetXSize(aWidth);                       // Estableciendo el ancho
   g.SetYSize(aHeight);                      // Estableciendo la altura
   g.SetBgColor(aColorBg);                   // Setting of background color
   g.SetColor(aColorBorder);                 // Estableciendo el color del borde
   g.SetCorner(CORNER_LEFT_UPPER);           // Establece un punto de anclaje
   g.SetBorderType(BORDER_FLAT);             // Establece el tipo de borde
   g.SetTimeFrames(OBJ_ALL_PERIODS);         // Establece la visibilidad en todos los períodos
   g.SetSelected(false);                     // Deshabilita la selección
   g.SetSelectable(false);                   // Deshabilita la posibilidad de selección
   g.SetWidth(1);                            // establece el ancho del borde
   g.SetStyle(STYLE_SOLID);                  // establece el estilo del borde
  }

Preste atención: el método consta de 14 líneas de código. Resultará muy frustrante tener que escribir todas ellas cada vez que necesite crear este objeto. Ahora solo necesita escribir una única línea y todos los parámetros del método son opcionales y se listan ordenados por su frecuencia de uso: posición, tamaño, color, etc.

De forma similar a Canvas(), escribe los métodos de creación de una etiqueta de texto, un botón y un campo "edit": Label(), Button() y Edit(). La clase ya diseñada CWorkPiece se adjunta a este artículo en el archivo IncGUI.mqh. Además de los métodos mencionados antes, la clase contiene otros métodos: Frame() y DeleteFrame() - los métodos de creación y eliminación de un frame (fig. 5). Un frame es una etiqueta rectangular con un encabezado en la esquina superior izquierda.

Los frames están pensados para ser usados al agrupar controles en un formulario.


Fig. 5. El frame con encabezado.

La lista de todos los métodos de la clase CWorkPiece se adjunta al artículo.

De forma similar a la clase CGraphicObjectShell, declare la clase CWorkPiece con el nombre corto "w" para que pueda usarlo después de conectar el archivo IncGUI.mqh.

CWorkPiece w;

Todas las herramientas auxiliares están listas y podemos continuar con el tema del artículo, la creación de un control personalizado.


5. Creación de un control "Edit"

En primer lugar, no hay que confundir la terminología. Llamemos al objeto gráfico OBJ_EDIT como a un campo de texto, al objeto OBJ_LABEL como a una etiqueta y a los controles creados como a un campo edit. El control creado consta de dos objetos gráficos: un campo edit (OBJ_EDIT) y una etiqueta de texto (OBJ_LABEL).

El control soporta dos modos de funcionamiento: entrada de texto y entrada de datos numéricos. En el modo de entrada de datos numéricos habrá una limitación en el rango de valores de entrada y serán aceptables como separadores decimales tanto una coma como un punto. En la salida programada de un valor en un campo edit, es formateado de acuerdo con un número especificado de posiciones decimales.

De esta forma, al inicializar un control debemos especificar su modo de operación: datos de texto o numéricos. El modo se especifica usando el parámetro aDigits. Un valor mayor que cero establece el modo numérico con el número especificado de posiciones decimales y un valor negativo establece el modo de texto.

Por defecto, el rango de valores aceptables es de -DBL_MAX a DBL_MAX (todo el rango de valores de una variable doble). Si es necesario, puede establecer otro rango llamando a los métodos SetMin() y SetMax(). De los parámetros de tamaño, solo se establecerá la anchura (width) para el control. Para que el campo edit esté equilibrado debe establecerse para él la correspondiente altura y anchura.

Un cambio en el tamaño de fuente requiere el correspondiente cambio de altura del objeto gráfico. Y esto requiere cambiar la ubicación de todos los demás controles. Ninguno haría esto. Suponemos el uso de un tamaño de fuente constante para todos los controles y los correspondientes campos edit. Sin embargo, la clase del control tendrá un método que devuelve su altura para poder calcular las coordenadas de otros elementos.

Habrá cuatro parámetros de color: color de fondo, color de texto, color de encabezado y color de aviso (podrá cambiar el color de fondo del campo de texto para llamar la atención del usuario, por ejemplo, en caso de introducir un valor incorrecto).

Como hemos mencionado antes, es posible disponer controles en una subventana. Además de los parámetros principales necesarios para que un control funcione, vamos a usar el otro parámetro Tag. Es un simple valor de texto que se almacena en una instancia de la clase. Un tag es una cómoda herramienta auxiliar.

La clase se llamará CInputBox. Tenemos pues el siguiente conjunto de variables de la clase (ubicado en la sección privada):

string m_NameEdit;     // Nombre del objeto  Edit
string m_NameLabel;    // Nombre del objeto Label 
int m_Left;            // coordenada X 
int m_Top;             // coordenada Y 
int m_Width;           // Anchura
int m_Height;          // Altura
bool m_Visible;        // marca de visibilidad del control
int m_Digits;          // Número de posiciones decimal para el número doble; -1 establece el modo texto
string m_Caption;      // Encabezado
string m_Value;        // Valor
double m_ValueMin;     // Valor mínimo
double m_ValueMax;     // Valor máximo
color m_BgColor;       // Color de fondo
color m_TxtColor;      // Color de texto
color m_LblColor;      // Color de encabezado
color m_WarningColor;  // Color de fuente de aviso
bool m_Warning;        // Marca de aviso
int m_SubWindow;       // Subventana
string m_Tag;          // Tag

Al usar un control, el primer método llamado es Init().

En este método preparamos los valores de todos los parámetros determinados antes:

// El método de inicialización
void Init(string aName="CInputBox",
           int aWidth=50,
           int aDigits=-1,
           string aCaption="CInputBox")
 { 
   m_NameEdit=aName+"_E";  // Prepara el nombre del próximo campo de texto
   m_NameLabel=aName+"_L"; // Prepara el nombre del encabezado
   m_Left=0;               // coordenada X 
   m_Top=0;                // coordenada Y 
   m_Width=aWidth;         // Anchura
   m_Height=15;            // Altura
   m_Visible=false;        // Visibilidad
   m_Digits=aDigits;       // El modo de operación y el número de posiciones decimales
   m_Caption=aCaption;     // texto del encabezamiento
   m_Value="";             // valor en el modo texto
   if(aDigits>=0)m_Value=DoubleToString(0,m_Digits); // valor en el modo numérico 
   m_ValueMin=-DBL_MAX;                // Valor mínimo
   m_ValueMax=DBL_MAX;                 // Valor máximo
   m_BgColor=ClrScheme.Color(0);       // Color de fondo del campo de texto
   m_TxtColor=ClrScheme.Color(1);      // Color de texto y marco del campo de texto
   m_LblColor=ClrScheme.Color(2);      // Color del encabezamiento
   m_WarningColor=ClrScheme.Color(3);  // Color de aviso
   m_Warning=false;                      // Modo: aviso, normal
   m_SubWindow=0; // Número de subventana
   m_Tag="";      // Tag
 }

Si un control funciona en el modo texto, a la variable m_Value se le asigna el valor "", y si funciona en el modo numérico se le asigna un cero con el número especificado de posiciones decimales. Los parámetros de color se establecen a sus valores por defecto. Trabajaremos con los esquemas de color en la última etapa.

Las variables que determinan las coordenadas del control se establecen al valor cero, ya que el control aun no es visible. Después de llamar al método Init() (si se ha pensado que el control tenga una posición fija en el gráfico) podemos establecer las coordenadas usando el método SetPos():

// Estableciendo las coordenadas X e Y 
void SetPos(int aLeft,int aTop)
{ 
   m_Left=aLeft;
   m_Top=aTop;
}

Después de esto podemos hacer visible el control (El método Show()):

// Habilitar la visibilidad en la posición especificada previamente
void Show()
{ 
   m_Visible=true; // Registro de la visibilidad
   Create();       // Creación de objetos gráficos
   ChartRedraw();   // Actualización del gráfico
}

La función Create() es llamada desde el método Show() y crea objetos gráficos (ubicados en la sección privada), a continuación se actualiza el gráfico (ChartRedraw()). El código de la función Create() se muestra a continuación:

// La función de creación de objetos gráficos
void Create(){ 
   color m_ctmp=m_BgColor;  // Color de fondo normal
      if(m_Warning){ // Se establece el método de aviso
         m_ctmp=m_WarningColor; // El campo de texto se mostrará de color en el color de aviso
      }
    // Creación del campo de texto
   w.Edit(m_NameEdit,m_SubWindow,m_Left,m_Top,m_Width,m_Height,m_Value,m_ctmp,m_TxtColor,7,"Arial"); 
      if(m_Caption!=""){ // Hay un encabezamiento
          // Creación del encabezamiento
         w.Label(m_NameLabel,m_SubWindow,m_Left+m_Width+1,m_Top+2,m_Caption,m_LblColor,7,"Arial"); 
      } 
}   

Al crear objetos gráficos en la función Create() en función de los valores de m_Warning, el campo de texto es asignado al correspondiente color de fondo. Si la variable m_Caption tiene un valor, se crea el encabezamiento (puede crear un control sin encabezamiento).

Si piensa realizar un control móvil use la segunda variante del método Show() con especificación de las coordenadas. En este método se establecen las coordenadas y se realiza la llamada a la primera variante del método Show():

// Estableciendo las coordenadas X e Y 
void SetPos(int aLeft,int aTop)
{ 
   m_Left=aLeft;
   m_Top=aTop;
}

Después de mostrar el control en pantalla, necesitaremos ocultarlo algún tiempo.

Para este propósito se usa el método Hide():

// Ocultando (borrado de objetos gráficos)
void Hide()
{ 
   m_Visible=false; // Registro del estado invisible
   Delete();        // Borrado de objetos gráficos
   ChartRedraw();    // Actualización del gráfico
}  

El método Hide() llama a la función Delete() que borra los objetos gráficos y a continuación llama a la función ChartRedraw() que actualiza el gráfico. La función Delete() se encuentra en la sección privada:

// Función de borrado de objetos gráficos
void Delete()
{ 
   ObjectDelete(0,m_NameEdit);  // Borrado del campo de texto
   ObjectDelete(0,m_NameLabel); // Borrado del encabezamiento
}   

Como ya conocemos un método que solo establece los valores de las propiedades sin cambiar la representación en pantalla de un control (el método SetPos()), es lógico crear un método para una actualización forzada de un control, el método Refresh():

// Actualización de la representación en pantalla (borrado y creación)
void Refresh()
{ 
   if(m_Visible)
   {   // Visibilidad habilitada
      Delete();     // Borrado de objeto gráfico
      Create();     // Creación de objetos gráficos
      ChartRedraw(); // Redibujar el gráfico
   }            
}   

El control es muy simple, por eso usamos el método simple de actualización (borrado y creación). Si fuera un control más complejo, como una lista con muchos campos edit, tendríamos que elegir un enfoque más apropiado.

Por tanto, hemos terminado con la ubicación del control. Vamos ahora a establecer un valor, el método SetValue(). Como el control puede funcionar de dos formas, habrá dos variantes del método SetValue(): con el string y el de tipo doble. En modo texto, el valor se usa tal cual:

// Estableciendo un valor de texto
void SetValue(string aValue)
{ 
   m_Value=aValue; // Asignando un valor a la variable para guardarlo
      if(m_Visible)
      { // Se habilita la visibilidad del control
          // Asignando el campo de texto al objeto para gestionar los objetos gráficos
         g.Attach(m_NameEdit); 
         g.SetText(m_Value); // Estableciendo el valor para el campo de texto
         ChartRedraw();        // Redibujando el gráfico
      }
} 

El argumento obtenido se asigna a la variable m_Value y si el control el visible se muestra en pantalla en el campo de texto.

En el modo numérico, se normaliza el argumento obtenido de acuerdo con el valor de m_Digits y a continuación se corrige según el valor máximo y mínimo (m_MaxValue, m_MinValue), convertido a un string, y luego se llama al primer método SetValue().

// Estableciendo un valor numérico
void SetValue(double aValue)
{ 
   if(m_Digits>=0)
   {  // En el modo numérico
       // Normalización del número según la precisión especificada
      aValue=NormalizeDouble(aValue,m_Digits);
      // "Alineación" del valor según el valor mínimo aceptable
      aValue=MathMax(aValue,m_ValueMin); 
       // Alineación" del valor según el valor máximo aceptable
      aValue=MathMin(aValue,m_ValueMax); 
       // Establecer el valor obtenido como string
      SetValue(DoubleToString(aValue,m_Digits)); 
   }
   else
   { // En el modo texto
      SetValue((string)aValue); // Asignando el valor a la variable para almacenarlo como está
   }            
}

Vamos a escribir dos métodos para obtener valores, uno para obtener un valor string y otro para obtener un valor doble:

// Obtener un valor de texto
string ValueStrind()
{ 
   return(m_Value);
}

// Obtener un valor numérico
double ValueDouble()
{ 
   return(StringToDouble(m_Value));
}

El valor establecido para el control se corrige de acuerdo con los valores máximos y mínimos aceptables. Vamos a añadir métodos para poder obtenerlos y asignarlos:

// Estableciendo el valor máximo aceptable
void SetMaxValue(double aValue)
{ 
   m_ValueMax=aValue; // Registro del nuevo valor máximo aceptable
      if(m_Digits>=0)
     { // El control funciona en modo numérico
         if(StringToDouble(m_Value)>m_ValueMax)
         { /* El valor actual del control es mayor que el nuevo valor máximo aceptable*/
            SetValue(m_ValueMax); // Estableciendo el nuevo valor que es igual al valor máximo aceptable
         }
      }
}

// Estableciendo el valor mínimo aceptable
void SetMinValue(double aValue)
{ 
   m_ValueMin=aValue; // Registro del nuevo valor mínimo aceptable  
      if(m_Digits>=0)
      { // El control funciona en modo numérico
         if(StringToDouble(m_Value)<m_ValueMin)
         { /*  El valor actual del control es menor que el nuevo valor máximo aceptable*/
            SetValue(m_ValueMin); // Estableciendo el nuevo valor que es igual al valor mínimo aceptable
         }
      }
}

// Obteniendo el valor máximo aceptable
double MaxValue()
{ 
   return(m_ValueMax); 
}

// Obteniendo el valor mínimo aceptable
double MinValue()
{ 
   return(m_ValueMin);
}

Si el control funciona en modo numérico, la verificación y corrección (en caso de ser necesaria) del valor actual se realiza al establecer nuevos valores máximos y mínimos aceptables.

Vamos a trabajar ahora con la introducción de un valor por un usuario, con el método Event(). La verificación de la entrada de datos por parte de un usuario se realizará usando el evento CHARTEVENT_OBJECT_ENDEDIT. Al trabajar en modo texto, si el valor especificado por un usuario no es igual a m_Value, el nuevo valor se asigna a a m_Value y el valor 1 se asigna a la variable m_event devuelta por el método Event().

Al trabajar en modo numérico, memoriza el valor previo de m_Value en la variable m_OldValue, reemplaza la coma con un punto, convierte el string a un número y lo pasa a la función SetValue(). A continuación, si m_Value y m_OldValue no son iguales, "genera" el evento (establece el valor 1 en la variable m_event).

// Gestión de eventos
int Event(const int id,
           const long & lparam,
           const double & dparam,
           const string & sparam)
{ 
   bool m_event=0; // Variable para un evento de este control
      if(id==CHARTEVENT_OBJECT_ENDEDIT)
      { // Ha habido un evento de final de edición del campo de texto
         if(sparam==m_NameEdit)
         { // El campo de texto con el nombre m_NameEdit se ha modificado
            if(m_Digits<0)
            { // En modo texto
               g.Attach(m_NameEdit); // Asignando el campo de texto para controlarlo
                  if(g.Text()!=m_Value)
                  { // Nuevo valor en el campo de texto
                     m_Value=g.Text(); // Asignando el valor a la variable para guardarlo
                     m_event=1;         // Ha habido un evento
                  }
            }
            else
            { // En el modo numérico
               string m_OldValue=m_Value; // La variable con el valor previo del control
               g.Attach(m_NameEdit);      // Adjuntando el campo de texto para controlarlo
               string m_stmp=g.Text();     // Obteniendo el texto especificado por un usuario en el campo de texto
               StringReplace(m_stmp,",",".");       // Reemplazando una coma con un punto
               double m_dtmp=StringToDouble(m_stmp); // Conversión a un número
               SetValue(m_dtmp);                     // Estableciendo el nuevo valor numérico
                     // Comparando el nuevo valor con el previo
                  if(StringToDouble(m_Value)!=StringToDouble(m_OldValue))
                  { 
                     m_event=1; // Ha habido un evento 
                  }
            }
         }
      }              
   return(m_event); // Devuelve el evento. 0 - no hay evento, 1 - hay evento
}

Soporte para el funcionamiento del control en subventanas. Para disponer de él, añada el método SetSubWindow() que debe ser llamado desde la función OnChartEvent() cuando tenga lugar el evento CHARTEVENT_CHART_CHANGE. Si piensa usar el control solo en un gráfico de precio, no hay necesidad de llamar a este método. 

La variable m_SubWindow ya ha sido declarada, es igual a 0 por defecto y se ha pasado a los métodos Edit() y Label() de la clase "w" en la creación de los objetos gráficos de un control. Se pasará el número de una subventana al método SetSubWindowName(). Si se ha cambiado el número, modifique el valor de la variable m_SubWindow y ejecute el método Refresh().

// Estableciendo una subventana por número
void SetSubWindow(int aNumber)
{ 
   int m_itmp=(int)MathMax(aNumber,0); /*si el número es negativo, se usará 0 - el gráfico de precio*/
      if(m_itmp!=m_SubWindow)
      { /* El número especificado no corresponde al número en el que se ubica el control*/
         m_SubWindow=m_itmp; // Registro del nuevo número de subventana
         Refresh(); // Nueva creación de los objetos gráficos
      }
} 

Probablemente, será más conveniente pasar el nombre de una subventana a la función en lugar de su número. Vamos a añadir otra variante del método SetSubWindow():

// Estableciendo una subventana por el nombre
void SetSubWindow(string aName)
{ 
   SetSubWindow(ChartWindowFind(0,aName)); // Determinación del número de la subventana por su nombre y estableciendo la subventana por su número
}

Suministramos la clase del control con los demás métodos que faltan según el concepto que hemos establecido al principio del artículo.

Tan pronto como tenemos disponible el método SetPos() que permite establecer ambas coordenadas del control simultáneamente, vamos a añadir los métodos para establecer las coordenadas por separado:

// Estableciendo la coordenada X
void SetPosLeft(int aLeft)
{ 
   m_Left=aLeft;
}      

// Estableciendo la coordenada Y
void SetPosTop(int aTop)
{ 
   m_Top=aTop;
}  

Método para establecer la anchura:

// Estableciendo la anchura
void SetWidth(int aWidth)
{ 
   m_Width=aWidth;
}

Método para obtener las coordenadas y el tamaño:

// Obteniendo la coordenada X
int Left()
{ 
   return(m_Left);
}

// Obteniendo la coordenada Y
int Top()
{ 
   return(m_Top);
}

// Obteniendo la anchura
int Width()
{ 
   return(m_Width);
}

// Obteniendo la altura
int Height()
{
   return(m_Height); 
}

Los métodos para trabajar con el tag:

// Estableciendo el tag
void SetTag(string aValue)
{ 
   m_Tag=aValue;
}

// Obteniendo el tag
string Tag()
{ 
   return(m_Tag);
}  

Los métodos para los avisos:

// Estableciendo el modo de avisos
void SetWarning(bool aValue)
{ 
      if(m_Visible)
      { // La visibilidad está habilitada
         if(aValue)
         { // Necesitamos habilitar el modo avisos
            if(!m_Warning)
            { // No se ha habilitado el modo avisos
               g.Attach(m_NameEdit);         // Adjuntando el campo de texto para controlar
               g.SetBgColor(m_WarningColor); // Estableciendo el color del texto del aviso en el campo de texto
            }
         }
         else
         { // Necesitamos inhabilitar el modo de aviso
            if(m_Warning)
            { // El modo de aviso se ha habilitado
               g.Attach(m_NameEdit);    // Adjuntando el campo de texto para controlar 
               g.SetBgColor(m_BgColor); // Estableciendo el color de fuente normal               
            }
         }
      }
   m_Warning=aValue; // Registro del modo actual
}

// Obteniendo el modo de aviso
bool Warning()
{ 
   return(m_Warning);
}

Si el control es visible al establecer el modo de aviso, el valor del parámetro pasado al método SetWarning es verificado. Si su valor no corresponde al estado actual del control, el color de fondo del campo de texto es modificado.

En cualquier caso, el modo establecido se registra para no asignar el color correspondiente al campo de texto en caso de que el control sea invisible.

Nos queda aun una propiedad, m_Digits. Vamos a añadir métodos para obtener y establecer su valor: 

// Estableciendo el número de posiciones decimales
void SetDigits(int aValue)
{ 
   m_Digits=aValue; // Registro del nuevo valor
      if(m_Digits>=0)
      { // El modo numérico
         SetValue(ValueDouble()); // Volviendo a establecer el valor actual
      }
}  

// Obteniendo el valor de m_Digits
int Digits()
{ 
   return(m_Digits);
}  

Bien, hemos terminado con la parte más interesante. Ahora le toca el turno a la más bonita.


6. Esquemas de colores

Los esquemas de colores se almacenarán en las variables de la clase CСolorSchemes.

La clase será declarada previamente en el archivo IncGUI.mqh con el nombre ClrScheme. Para establecer un esquema de color llamaremos al método SetScheme() con el número de un esquema de color especificado como parámetro. Si el método SetScheme() no es llamado, se usará el esquema de color con el número 0.

Para obtener un color, vamos a usar el método Color() con un número especificado del esquema de color. Vamos a escribir la clase CСolor Schemes con las secciones privadas y públicas. En la sección privada, declaramos la variable m_ShemeIndex para almacenar el índice de un esquema de color. En la sección pública se escribe el método SetScheme():

// Establece el número del esquema de color
void SetScheme(int aShemeIndex)
{ 
   m_ShemeIndex=aShemeIndex;
}

El método Color(). Se declara en el método una matriz bidimensional: la primera dimensión es el número del esquema de color y la segunda el número de color en el esquema. Dependiendo del número de esquema de color especificado, devuelve el color por el número especificado en los parámetros del método.

color Color(int aColorIndex)
{
   color m_Color[3][4];  // La primera dimensión -el número del esquema de color, l segunda- el número del color en el esquema de color
   // default
   m_Color[0][0]=clrSnow;
   m_Color[0][1]=clrDimGray;
   m_Color[0][2]=clrDimGray;
   m_Color[0][3]=clrPink;
   // amarillo-negro
   m_Color[1][0]=clrLightYellow;
   m_Color[1][1]=clrBrown;
   m_Color[1][2]=clrBrown;
   m_Color[1][3]=clrPink;
   // azul
   m_Color[2][0]=clrAliceBlue;
   m_Color[2][1]=clrNavy;
   m_Color[2][2]=clrNavy;
   m_Color[2][3]=clrPink;
   return(m_Color[m_ShemeIndex][aColorIndex]); // Devolviendo un valor según el número de esquema y el número de color en el esquema
}

Por ahora, los esquemas de color incluyen cuatro colores cada uno, de los que dos tienen los mismos valores. Además, al crear otros controles podemos necesitar más colores.

Para encontrar con facilidad un color apropiado en el esquema o decidir sobre la inclusión de un nuevo color, la clase incluye un método que permite ver los colores, el método Show() (fig. 6). Y también hay un método inverso Hide() para borrar muestras de color del gráfico.


Fig. 6. Visualizando esquemas de color a través del método Show()

Este artículo tiene adjunto el archivo ColorSchemesView.mq5. Es un asesor experto para ver esquemas de color (ColorSchemesView.mq5)

Vamos a modificar ligeramente el método Init() en la clase CInputBox. Reemplazamos sus colores con los de la clase ClrScheme:

m_BgColor=ClrScheme.Color(0);       // Color de fondo para el campo de texto
m_TxtColor=ClrScheme.Color(1);      // Color de la fuente y el marco del campo de texto
m_LblColor=ClrScheme.Color(2);     // Color del encabezamiento
m_WarningColor=ClrScheme.Color(3); // Color de aviso

Aquí finaliza la creación de un control y ahora ya disponemos de una base para el desarrollo de cualquier otro.


7. Utilizando el control

Vamos a crear un asesor experto y lo vamos a nombrar GUITest. Conectamos el archivo IncGUI.mqh:  

#include <IncGUI.mqh>
Declaramos la clase CInputBox con el nombre ib:
CInputBox ib;

En la función OnInit() del asesor experto, llamamos al método Init() del objeto ib:

ib.Init("InpytBox",50,4,"input");

Hacemos el control visible y establecemos una posición para el mismo:

ib.Show(10,20);

En la función OnDeinit() del asesor experto, borramos el control:

ib.Hide(); 

Compilamos y adjuntamos el asesor experto al gráfico. Verá nuestro control (fig. 7).


Fig. 7. El control InputBox

Añadimos la posibilidad de cambiar el esquema de color al asesor experto.

En este momento, tenemos tres esquemas de color. Vamos a hacer una enumeración y a utilizar una variable externa para elegir un esquema de color: 

enum eColorScheme
  {
   DefaultScheme=0,
   YellowBrownScheme=1,
   BlueScheme=2
  };

input eColorScheme ColorScheme=DefaultScheme;

Al principio de la función OnInit() del asesor experto añadimos el establecimiento de un esquema de color:

ClrScheme.SetScheme(ColorScheme);

Ahora, en la ventana de propiedades del asesor experto podemos elegir uno de los tres esquemas de color (fig. 8).




Fig. 8. Diferentes esquemas de color

Para gestionar el evento que especifica un nuevo valor, añadimos el siguiente código a la función OnChartEvent() del asesor experto:

if(ib.Event(id,lparam,dparam,sparam)==1)
  {
   Alert("Valor introducido "+ib.ValueStrind());
  }

Ahora, cuando se especifica un nuevo valor en el campo edit, se abre una ventana de mensaje informando sobre el valor especificado.

Dotamos al asesor experto con la posibilidad de crear controles en una subventana.

En primer lugar, creamos un indicador de prueba TestSubWindow (adjunto en el archivo TestSubWindow.mq5). Al crear el indicador en el MQL5 Wizard, especificamos que debe funcionar en una subventana separada. Añadimos el siguiente código a la función OnChartEvent() del asesor experto:

if(CHARTEVENT_CHART_CHANGE)
  {
   ip.SetSubWindow("TestSubWindow");
  }

Ahora, si el indicador no está en el gráfico, el control se crea en el gráfico de precio. Si adjuntamos el indicador al gráfico, el control pasará a la subventana (fig. 9). Si se borra el indicador, el control volverá al gráfico de precio.

 
Fig. 9. El control en la subventana

Conclusión

Como resultado del trabajo que hemos realizado, tenemos el archivo IncGUI.mqh que contiene las siguientes clases: CGraphicObjectShell (creación y gestión de objetos gráficos), CWorkPiece (creación rápida de varios objetos gráficos estableciendo sus propiedades por medio de parámetros), CColorSchemes (estableciendo un esquema de color y obteniendo el color del esquema de color actual) y una clase del control CInputBox.

Las clases CGraphicObjectShell, CWorkPiece y CColorSchemes ya han sido declaradas en el archivo con los nombres "g", "w" y "ClrScheme", es decir, están listas para ser usadas después de conectar con el archivo IncGUI.mqh.

Vamos a repetir como se usa la clase CInputBox:

  1. Conectar con el archivo IncGUI.mqh.
  2. Declarar una clase del tipo CInputBox.
  3. Llamar al método Init().
  4. Establecer las coordenadas usando el método SetPos(). Habilitar la visibilidad usando Show() si es necesario. La segunda variante: habilitar la visibilidad usando Show() con la especificación de las coordenadas.
  5. Si es necesario o al final del trabajo del asesor experto, ocultar el control usando el método Hide().
  6. Añadir la llamada del método Event() a la función OnChartEvent().
  7. Si necesita crear un control en una subventana, usamos la función OnChartEvent() con una llamada del método SetSubWindow() cuando ocurra un evento CHARTEVENT_CHART_CHANGE.
  8. Para usar esquemas de color, llamamos al método SetScheme() de la clase ClrScheme antes de llamar al método Init(). 


Adjuntos