Interfaces gráficas II: Controles "Línea separadora" y "Menú contextual" (Capítulo 2)

Anatoli Kazharski | 10 marzo, 2016

Índice.


 

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 biblioteca (Capítulo 1). Al final de cada artículo de la serie se puede encontrar la lista 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. 

En el capítulo anterior hemos escrito la clase para crear un elemento del menú. Se utiliza como un control independiente, y como una parte del menú contextual y principal. En este artículo se describe la creación del control “Línea separadora” que también podrá utilizarse no sólo como un elemento independiente de la interfaz, sino como parte de muchos otros controles. Después de eso, tendremos todo lo necesario para desarrollar la clase del menú contextual que también será considerado al detalle en el presente artículo. Además, vamos a introducir adiciones necesarias en la clase que sirve de base para almacenar los punteros a todos los controles de la interfaz gráfica de la aplicación.



Desarrollando la clase para crear una línea separadora

En el menú contextual, aparte de varios tipos de elementos del menú, a menudo se puede observar un control más: es la línea separadora. Este control puede encontrarse no sólo en los menús contextuales. Por ejemplo, también podemos ver las líneas separadoras verticales en la barra de estado del terminal comercial MetaTrader y editor de códigos MetaEditor. Por eso, para este objeto vamos a crear una clase individual para tener la posibilidad de utilizarla en cualquier otro control, o incluso para usarla como un control separado del diseño de la interfaz gráfica.

Para imitar el relieve, una línea separadora debe componerse como mínimo de dos partes. Si hacemos una línea más clara que el fondo y la otra va a ser más oscura, obtendremos visualmente una canaleta en la superficie. Hay dos modos de crear una línea separadora: (1) usar dos objetos primitivos tipo CRectLabel que ya tenemos en el archivo Objects.mqh, o (2) crear un objeto tipo OBJ_BITMAP_LABEL y usarlo como lienzo para dibujar. Nosotros vamos a utilizar la segunda opción. Para dibujar, la librería estándar propone la clase CCanvas. Esta clase ya contiene todos los métodos necesarios para dibujar las figuras geométricas simples, lo que facilitará considerablemente la implementación de nuestra idea y nos ahorrará un montón de tiempo. 

La clase CCanvas tiene que ser integrada en la librería que desarrollamos de tal manera que haya posibilidad de usarla igual que los objetos primitivos que ahora se encuentran en el archivo Objects.mqh. Es fácil de conseguir si hacemos que la clase CCanvas sea derivada de la clase CChartObjectBmpLabel. Pero habrá que introducir una pequeña modificación en la clase CCanvas para que luego no aparezcan errores o advertencias durante la compilación del programa. Es que en la clase CCanvas, igual que en la clase CChartObject que es base para la clase CChartObjectBmpLabel, hay campo (variable) m_chart_id. Por eso, el compilador mostrará la advertencia diciendo que la variable con este nombre ya existe:

Fig. 1. Advertencia del compilador diciendo que la variable con este nombre ya existe.

Fig. 1. Advertencia del compilador

 

En realidad, esta advertencia no provocará errores críticos, y a pesar de eso la compilación será completada. Sin embargo, se recomienda hacer todo lo posible para evitar estas situaciones. Es que nunca se sabe cómo eso al final puede afectar el funcionamiento del programa. Vamos a tomarlo por regla y atenerse escrupulosamente a ella. Es más, tendremos que introducir modificaciones en el archivo Canvas.mqh en cualquier caso: hay que hacer que la clase CCanvas sea derivada de la clase CChartObjectBmpLabel. Así será muy fácil librarse de esta advertencia importuna. Simplemente, eliminaremos la variable m_chart_id en la clase CCanvas. Pero al introducir cambios en las clases de la librería estándar, tenemos que recordar que en caso de la siguiente actualización del terminal, también pueden actualizarse los archivos de la librería estándar, con lo cual dichos cambios serán anulados. Puesto que para conseguir nuestros objetivos, no podemos evitar la modificación de la clase CCanvas, crearemos su copia y la colocaremos en el directorio que contiene los archivos de nuestra librería.

Ahora cree la carpeta Canvas en la carpeta #Include. Cree una copia del archivo con la clase CCanvas y cambie su nombre por CustomCanvas.mqh, y la clase por CCustomCanvas. Incluya el archivo de la librería estándar ChartObjectsBmpControls.mqh en el archivo CustomCanvas.mqh, y haga que la clase CCustomCanvas sea derivada de la clase CChartObjectBmpLabel. Luego elimine la variable m_chart_id del cuerpo de la clase CCustomCanvas y del constructor.

//+------------------------------------------------------------------+
//|                                                 CustomCanvas.mqh |
//|                   Copyright 2009-2013, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Files\FileBin.mqh>
#include <Controls\Rect.mqh>
#include <ChartObjects\ChartObjectsBmpControls.mqh>

//...

//+------------------------------------------------------------------+
//| Class CCustomCanvas                                              |
//| Usage: class for working with a dynamic resource                 |
//+------------------------------------------------------------------+
class CCustomCanvas : public CChartObjectBmpLabel
  {
//...

Ahora hay que incluir el archivo CustomCanvas.mqh en el archivo Objects.mqh:

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

A continuación, creamos la clase CRectCanvas que tiene que ser derivada de la clase CCustomCanvas. La clase CRectCanvas será parecida a otras clasesque se encuentran en el archivo Objects.mqh (hemos hablado de su contenido en el artículo anterior). Ahora podremos usarla para dibujar otros controles de la interfaz que al mismo tiempo formarán parte de nuestra librería. 

Bues bien, tenemos todo preparado para la clase CSeparateLine que servirá para crear la línea separadora. Por favor, cree el archivo SeparateLine.mqh en la carpeta Controls. Incluya en él los archivos Element.mqh y Window.mqh. Luego hay que hacer lo siguiente:

1) crear la clase CSeparateLine;

2) declarar en él el puntero al formulario al que va a adjuntarse el control, así como crear un método para guardar el puntero dentro;

3) crear una instancia de la clase CRectCanvas;

4) declarar e implementar los métodos virtuales estándar para todos los controles mediante los cuales se puede manejar el control.

//+------------------------------------------------------------------+
//|                                                 SeparateLine.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Clase para crear una línea separadora                            |
//+------------------------------------------------------------------+
class CSeparateLine : public CElement
  {
private:
   //--- Puntero al formulario al que está adjuntado el control
   CWindow          *m_wnd;
   //--- Objeto para crear la línea separadora
   CRectCanvas       m_canvas;
   //---
public:
                     CSeparateLine(void);
                    ~CSeparateLine(void);
   //--- Guarda el puntero del formulario pasado
   void              WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
   //---
public:
   //--- Manejador del evento del gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Desplazamiento del control
   virtual void      Moving(const int x,const int y);
   //--- (1) Mostrar, (2) ocultar, (3) resetear, (4) eliminar
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSeparateLine::CSeparateLine(void)
  {
//--- Guardamos el nombre de la clase del control en la clase base  
   CElement::ClassName(CLASS_NAME);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CSeparateLine::~CSeparateLine(void)
  {
  }
//+------------------------------------------------------------------+

Para ajustar la apariencia de la línea separadora, crearemos tres métodos mediante los cuales se puede establecer los siguientes parámetros:

Hay que añadir la enumeración al archivo Enums.mqh que va a utilizarse para especificar el tipo:

//+------------------------------------------------------------------+
//| Enumeración del tipo de la línea separadora                      |
//+------------------------------------------------------------------+
enum ENUM_TYPE_SEP_LINE
  {
   H_SEP_LINE =0,
   V_SEP_LINE =1
  };

Ahora a la clase CSeparateLine podemos añadirle las variables y métodos correspondientes, y ejecutar la inicialización con valores por defecto en el constructor:

class CSeparateLine : public CElement
  {
private:
  //--- Propiedades
   ENUM_TYPE_SEP_LINE m_type_sep_line;   
   color             m_dark_color;
   color             m_light_color;
   //---
public:
   //--- (1) Tipo de la línea, (2) colores de la línea
   void              TypeSepLine(const ENUM_TYPE_SEP_LINE type) { m_type_sep_line=type; }
   void              DarkColor(const color clr)                 { m_dark_color=clr;     }
   void              LightColor(const color clr)                { m_light_color=clr;    }
   //---
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSeparateLine::CSeparateLine(void) : m_type_sep_line(H_SEP_LINE),
                                     m_dark_color(clrBlack),
                                     m_light_color(clrDimGray)
  {
  }

Nos queda añadir los métodos para crear el control y el método en el que la línea separadora va a dibujarse sobre el lienzo. Al método público (public) de creación del control CreateSeparateLine() que va a llamarse en la aplicación del usuario hay que pasar los siguientes parámetros:

class CSeparateLine : public CElement
  {
public:
   //--- Crear la línea separadora
   bool              CreateSeparateLine(const long chart_id,const int subwin,const int index,
                                        const int x,const int y,const int x_size,const int y_size);
   //---
private:
   //--- Crea el lienzo para dibujar la línea separadora
   bool              CreateSepLine(void);
   //--- Dibuja la línea separadora
   void              DrawSeparateLine(void);
   //---
  };

El código del método CreateSeparateLine() no se diferencia casi en nada de los métodos semejantes (por ejemplo, en la clase CMenuItem), por eso pasaremos al código del método CreateSepLine(). 

Al principio, como en todos los métodos de este tipo, se forma el nombre del objeto gráfico. Luego se crea el objeto gráfico (lienzo) en el que podemos dibujar. Aquí hay que tener en cuenta que para la creación del objeto tipo OBJ_BITMAP_LABEL se utiliza el método CreateBitmapLabel() que pertenece a la clase CCustomCanvas. En esta clase, durante la creación de objetos, no está previsto adjuntar el objeto al gráfico como eso ha sido implementado en la clase CChartObjectBmpLabel, donde después de crear un objeto, inmediatamente se utiliza el método CChartObject::Attach() de la clase base. Por eso aquí tenemos que ocuparnos de ello personalmente. Puesto que antes ya hemos creado la clase CCustomCanvas derivada de la clase CChartObjectBmpLabel, ya tenemos disponible el método CChartObject::Attach() de su clase base. No se puede manejar el objeto sin adjuntarlo al gráfico.

Después de crear el objeto, adjuntarlo al gráfico y establecer las propiedades, se puede dibujar la línea separadora en nuestro lienzo usando el método DrawSeparateLine(). Véase el código de abajo. Luego el puntero del objeto se guarda en el array de la clase base CElement.

//+------------------------------------------------------------------+
//| Creación del lienzo para dibujar la línea separadora             |
//+------------------------------------------------------------------+
bool CSeparateLine::CreateSepLine(void)
  {
//--- Formación del nombre del objeto  
   string name=CElement::ProgramName()+"_separate_line_"+(string)CElement::Index()+"__"+(string)CElement::Id();
//--- Creación del objeto
   if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,m_x,m_y,m_x_size,m_y_size,COLOR_FORMAT_ARGB_NORMALIZE))
      return(false);
//--- Adjuntar al gráfico
   if(!m_canvas.Attach(m_chart_id,name,m_subwin,1))
      return(false);
//--- Propiedades
   m_canvas.Background(false);
//--- Sangrías desde el punto extremo
   m_canvas.XGap(m_x-m_wnd.X());
   m_canvas.YGap(m_y-m_wnd.Y());
//--- Dibujar la línea separadora
   DrawSeparateLine();
//--- Añadir al array
   CElement::AddToArray(m_canvas);
   return(true);
  }

El código del método DrawSeparateLine() es fácil de comprender. Primero obtenemos los tamaños del lienzo. Luego limpiamos el lienzo haciéndolo transparente usando el método CCustomCanvas::Erase(). Luego, dependiendo del tipo de la línea a dibujar (horizontal o vertical), el programa se dirigirá al bloque del código correspondiente. Para dar un ejemplo, describiremos la creación de una línea horizontal. Primero, se determinan las coordenadas para dos puntos de la línea, luego se dibuja la primera línea en la parte superior del lienzo. Las coordenadas para la segunda línea se determinan en la parte inferior del lienzo. Si el alto del lienzo es de dos píxeles, las líneas van a ir una detrás de otra. Pero Usted puede separarlas poniendo el alto del lienzo más de dos píxeles. Al final del método, para mostrar los cambios, hay que actualizar obligatoriamente el lienzo mediante el método CCustomCanvas::Update().

//+------------------------------------------------------------------+
//|  Dibuja la línea separadora                                      |
//+------------------------------------------------------------------+
void CSeparateLine::DrawSeparateLine(void)
  {
//--- Coordenadas para la línea
   int x1=0,x2=0,y1=0,y2=0;
//--- Tamaños del lienzo
   int   x_size =m_canvas.X_Size()-1;
   int   y_size =m_canvas.Y_Size()-1;
//--- Vaciar el lienzo
   m_canvas.Erase(::ColorToARGB(clrNONE,0));
//--- Si la línea es horizontal
   if(m_type_sep_line==H_SEP_LINE)
     {
      //--- Línea oscura arriba
      x1=0;
      y1=0;
      x2=x_size;
      y2=0;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color));
      //--- Línea clara abajo
      x1=0;
      x2=x_size;
      y1=y_size;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color));
     }
//--- Si la línea es vertical
   else
     {
      //--- Línea oscura a la izquierda
      x1=0;
      x2=0;
      y1=0;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color));
      //--- Línea clara a la derecha
      x1=x_size;
      y1=0;
      x2=x_size;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color));
     }
//--- Actualizar lienzo
   m_canvas.Update();
  }

 



Prueba de colocación de la línea separadora

Ahora podemos probar cómo funciona todo eso. Antes ya hemos adjuntado un elemento del menú al formulario. Usando el mismo ejemplo, se puede adjuntar la línea separadora como un control de la interfaz separado. 

Recordaré en breve el proceso de adjuntar un control al formulario.

Si todo ha sido hecho de forma correcta, entonces después de compilar el programa y cargarlo en el gráfico, obtenemos aproximadamente el siguiente resultado:

Fig. 2. Prueba del control de la interfaz “línea separadora”.

Fig. 2. Prueba del control de la interfaz “línea separadora”.


Hemos terminado el desarrollo de la clase CSeparateLine, y ahora tenemos todo listo para empezar la implementación de la clase para crear un menú contextual.

 


Desarrollando la clase para crear un menú contextual

Hasta este momento, en el proceso del desarrollo de nuestra librería, hemos creado tres controles de la interfaz: (1) formulario para los controles (CWindow), (2) control “elemento del menú” (CMenuItem) y (3) control «línea separadora» (CSeparateLine). Cada uno de ellos puede ser asignado al tipo simple de los controles porque se crean usando sólo los objetos primitivos. Pero el menú contextual ya se refiere al tipo complicado (compuesto) de los controles. Va a construirse no sólo de objetos primitivos, sino también de otros controles cuya clase base también es CElement

Cree el archivo ContextMenu.mqh en la carpeta Controls del directorio de nuestra librería y incluya en este archivo los archivos que va a necesitar para crear el menú contextual:

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

Luego hay que crear la clase CContextMenu con el conjunto estándar de los métodos virtuales para todos los controles de la librería, el puntero al formulario y el método para su almacenamiento. Como el fondo del control es necesario el objeto tipo OBJ_RECTANGLE_LABEL, por eso para su creación vamos a usar la clase CRectLabel del archivo Object.mqh. Para los elementos del menú, antes hemos creado la clase CMenuItem. Puesto que su cantidad en el menú contextual suele ser más de uno, al mismo tiempo su número no sabe de antemano, entonces hay que declarar un array dinámico de las instancias de esta clase. Lo mismo se refiere a las líneas separadoras (CSeparateLine) del menú contextual.

//+------------------------------------------------------------------+
//| Clase para crear un menú contextual                              |
//+------------------------------------------------------------------+
class CContextMenu : public CElement
  {
private:
   //--- Puntero al formulario al que está adjuntado el control
   CWindow          *m_wnd;
   //--- Objetos para crear el elemento del menú
   CRectLabel        m_area;
   CMenuItem         m_items[];
   CSeparateLine     m_sep_line[];
   //---
public:
                     CContextMenu(void);
                    ~CContextMenu(void);
   //--- Guarda el puntero del formulario pasado
   void              WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }

   //--- Manejador de eventos del gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Temporizador
   virtual void      OnEventTimer(void);
   //--- Desplazamiento del control
   virtual void      Moving(const int x,const int y);
   //--- (1) Mostrar, (2) ocultar, (3) resetear, (4) eliminar
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Establecer, (2) poner a cero las prioridades para el clic izquierdo del ratón
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void)
  {
//--- Guardamos el nombre de la clase del control en la clase base
   CElement::ClassName(CLASS_NAME);
//--- El menú contextual es desplegable
   CElement::m_is_dropdown=true;
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CContextMenu::~CContextMenu(void)
  {
  }
//+------------------------------------------------------------------+

Para configurar la apariencia del menú contextual vamos a necesitar los campos y métodos correspondientes:

class CContextMenu : public CElement
  {
private:
   //--- Propiedades del fondo
   int               m_area_zorder;
   color             m_area_color;
   color             m_area_border_color;
   color             m_area_color_hover;
   color             m_area_color_array[];
   //--- Propiedades del elemento del menú
   int               m_item_y_size;
   color             m_item_back_color;
   color             m_item_border_color;
   color             m_item_back_color_hover;
   color             m_item_back_color_hover_off;
   color             m_label_color;
   color             m_label_color_hover;
   string            m_right_arrow_file_on;
   string            m_right_arrow_file_off;
   //--- Propiedades de la línea separadora
   color             m_sepline_dark_color;
   color             m_sepline_light_color;
   //---
public:
   //--- Número de elementos del menú
   int               ItemsTotal(void)                         const { return(::ArraySize(m_items));         }
   //--- Métodos para configurar la apariencia del menú contextual:
   //    Color del fondo del menú contextual
   void              MenuBackColor(const color clr)                 { m_area_color=clr;                     }
   void              MenuBorderColor(const color clr)               { m_area_border_color=clr;              }
   //--- (1) Alto, (2) color del fondo y (3) color del marco del menú contextual
   void              ItemYSize(const int y_size)                    { m_item_y_size=y_size;                 }
   void              ItemBackColor(const color clr)                 { m_item_back_color=clr;                }
   void              ItemBorderColor(const color clr)               { m_item_border_color=clr;              }
   //--- Color del fondo del elemento del menú (1) disponible y (2) bloqueado al situar el cursor del ratón
   void              ItemBackColorHover(const color clr)            { m_item_back_color_hover=clr;          }
   void              ItemBackColorHoverOff(const color clr)         { m_item_back_color_hover_off=clr;      }
   //--- Color del texto (1) normal (2) en el foco
   void              LabelColor(const color clr)                    { m_label_color=clr;                    }
   void              LabelColorHover(const color clr)               { m_label_color_hover=clr;              }
   //--- Determinar la imagen para el indicio de la la existencia del menú contextual en el elemento
   void              RightArrowFileOn(const string file_path)       { m_right_arrow_file_on=file_path;      }
   void              RightArrowFileOff(const string file_path)      { m_right_arrow_file_off=file_path;     }
   //--- Color (1) oscuro y (2) claro de la línea separadora
   void              SeparateLineDarkColor(const color clr)         { m_sepline_dark_color=clr;             }
   void              SeparateLineLightColor(const color clr)        { m_sepline_light_color=clr;            }
   //---
  };

Entre el menú contextual y el elemento que lo contiene tiene que haber una vinculación, de lo contrario no se podrá manejar estos controles de forma debida. Para ser más preciso, el menú contextual y los elementos de los que se compone tiene que haber acceso a este elemento del menú en el que se encuentran, es decir al nodo anterior. Entonces, en la clase CContextMenu y en la clase CMenuItem tiene que haber el puntero tipo CMenuItem, así como los métodos que permiten guardar y obtenerlo. Este puntero también va a utilizarse para verificar la sucesión correcta de la creación de la interfaz gráfica del programa. Lo demostraremos más tarde, cuando vamos a considerar los métodos de la creación del menú contextual. 

Adición del puntero y los métodos para guardar y obtenerlo en la clase CContextMenu:

class CContextMenu : public CElement
  {
private:
   //--- Puntero al nodo anterior
   CMenuItem        *m_prev_node;
   //---
public:
   //--- Obtener y guardar el puntero del nodo anterior
   CMenuItem        *PrevNodePointer(void)                    const { return(m_prev_node);                  }
   void              PrevNodePointer(CMenuItem &object)             { m_prev_node=::GetPointer(object);     }
   //---
  };

Lo mismo hay que añadir a la clase CMenuItem:

class CMenuItem : public CElement
  {
private:
   //--- Puntero al nodo anterior
   CMenuItem        *m_prev_node;
   //---
public:
   //--- Obtener y guardar el puntero del nodo anterior
   CMenuItem        *PrevNodePointer(void)                    const { return(m_prev_node);                  }
   void              PrevNodePointer(CMenuItem &object)             { m_prev_node=::GetPointer(object);     }
   //---
  };

La interfaz gráfica va a crearse en la clase personalizada de la aplicación (CProgram). En esta clase vamos a necesitar un método que permite indicar el número de elementos en el menú contextual a antes de su creación, así como los valores únicos de algunos parámetros que no son comunes para todos los elementos. Vamos a escribir el método CContextMenu::AddItem() que recibe los siguientes parámetros: (1) texto del elemento del menú, (2) ruta hacia la imagen para el icono del elemento disponible, (3) ruta hacia la imagen para el icono del elemento bloqueado y (4) tipo del elemento del menú. Además, harán falta los arrays en los que van a almacenarse los valores pasados. El tamaño de estos arrays va a aumentarse a un control con cada llamada al método CContextMenu::AddItem().

class CContextMenu : public CElement
  {
private:
   //--- Arrays de las propiedades de los elementos del menú
   //    (1) Texto, (2) icono del elemento disponible, (3) icono del elemento bloqueado
   string            m_label_text[];
   string            m_path_bmp_on[];
   string            m_path_bmp_off[];
   //---
public:
   //--- Añade un elemento del menú con propiedades especificadas antes de la creación del menú contextual
   void              AddItem(const string text,const string path_bmp_on,const string path_bmp_off,const ENUM_TYPE_MENU_ITEM type);
   //---
  };
//+------------------------------------------------------------------+
//| Añade un elemento del menú                                       |
//+------------------------------------------------------------------+
void CContextMenu::AddItem(const string text,const string path_bmp_on,const string path_bmp_off,const ENUM_TYPE_MENU_ITEM type)
  {
//--- Aumentamos el tamaño del array a un control
   int array_size=::ArraySize(m_items);
   ::ArrayResize(m_items,array_size+1);
   ::ArrayResize(m_label_text,array_size+1);
   ::ArrayResize(m_path_bmp_on,array_size+1);
   ::ArrayResize(m_path_bmp_off,array_size+1);
//--- Guardamos los valores de parámetros pasados
   m_label_text[array_size]   =text;
   m_path_bmp_on[array_size]  =path_bmp_on;
   m_path_bmp_off[array_size] =path_bmp_off;
//--- Determinar el tipo del elemento del menú
   m_items[array_size].TypeMenuItem(type);
  }

Para añadir las líneas separadoras al menú contextual, necesitaremos un array donde va a guardarse el número del índice del elemento del menú después del que se colocará esta línea. El número del índice del elemento del menú va a pasarse al método CContextMenu::AddSeparateLine() cuyo código se muestra abajo:

class CContextMenu : public CElement
  {
private:
   //--- Array para los números de los índices de los elementos del menú tras los cuales hay que colocar la línea separadora
   int               m_sep_line_index[];
   //---
public:
   //--- Añade la línea separadora después del elemento especificado antes de crear el menú contextual
   void              AddSeparateLine(const int item_index);
   //---
  };
//+------------------------------------------------------------------+
//| Añade la línea separadora                                        |
//+------------------------------------------------------------------+
void CContextMenu::AddSeparateLine(const int item_index)
  {
//--- Aumentamos el tamaño del array a un control
   int array_size=::ArraySize(m_sep_line);
   ::ArrayResize(m_sep_line,array_size+1);
   ::ArrayResize(m_sep_line_index,array_size+1);
//--- Guardamos el número del índice
   m_sep_line_index[array_size]=item_index;
  }

Vamos a necesitar los métodos que permiten obtener los siguientes parámetros, indicando el número del índice del elemento del menú: (1) puntero del elemento del menú, (2) descripción (texto a mostrar) y (3) tipo. En cada método antes de devolver el valor de la propiedad, primero se comprueba la superación del rango del array y se realiza la corrección del índice. Está implementado de tal manera que si el índice pasado es más grande que el tamaño del array, se llama al último elemento, y si es menos de cero, al primero. 

class CContextMenu : public CElement
  {
public:
   //--- Devuelve el puntero del elemento del menú contextual
   CMenuItem        *ItemPointerByIndex(const int index);
   //--- Devuelve la descripción (texto a mostrar)
   string            DescriptionByIndex(const int index);
   //--- Devuelve el tipo del elemento del menú
   ENUM_TYPE_MENU_ITEM TypeMenuItemByIndex(const int index);
   //---
  };
//+------------------------------------------------------------------+
//| Devuelve el puntero del elemento del menú según el índice        |
//+------------------------------------------------------------------+
CMenuItem *CContextMenu::ItemPointerByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Si el menú contextual no tiene elementos, avisar sobre ello
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > ¡La llamada a este método debe realizarse "
              " cuando en el menú contextual hay por lo menos un elemento!");
     }
//--- Corrección en caso de superar el rango
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Devolver el puntero
   return(::GetPointer(m_items[i]));
  }
//+------------------------------------------------------------------+
//| Devuelve el nombre del elemento según el índice                  |
//+------------------------------------------------------------------+
string CContextMenu::DescriptionByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Si el menú contextual no tiene elementos, avisar sobre ello
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > ¡La llamada a este método debe realizarse "
              " cuando en el menú contextual hay por lo menos un elemento!");
     }
//--- Corrección en caso de superar el rango
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Devolver la descripción del elemento
   return(m_items[i].LabelText());
  }
//+------------------------------------------------------------------+
//| Devuelve el tipo del elemento según el índice                    |
//+------------------------------------------------------------------+
ENUM_TYPE_MENU_ITEM CContextMenu::TypeMenuItemByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Si el menú contextual no tiene elementos, avisar sobre ello
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > ¡La llamada a este método debe realizarse "
              " cuando en el menú contextual hay por lo menos un elemento!");
     }
//--- Corrección en caso de superar el rango
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Devolver el tipo del elemento
   return(m_items[i].TypeMenuItem());
  }

En un menú contextual puede haber varios grupos de elementos de radio. Para que no hay confusión con determinar qué elemento ha sido pulsado, cada elemento de radio ha de tener su propio identificador del grupo y su índice respecto a la lista de este grupo. En el esquema de abajo se muestra que los elementos de radio aparte de los índices comunes e identificador del elemento del menú también tienen sus distintivos. 

Fig. 3. Esquema de identificadores e índices de diferentes grupos en el menú contextual.

Fig. 3. Esquema de identificadores e índices de diferentes grupos en el menú contextual.

 

Al generar el menú contextual, antes de su colocación en el gráfico es necesario determinar por su propia cuenta qué tipo va a tener uno u otro elemento, así como, si se trata de los elementos de radio, a qué grupo pertenece. En otras palabras, vamos a necesitar un método que permite determinar el identificador de cada elemento de radio. Por defecto, los identificadores de los elementos de radio serán iguales a cero. Y si lo dejamos todo tal como es, eso va a significar que el menú contextual tiene sólo un grupo de los elementos de radio, sin importar la cantidad que ha sido añadida. Van a surgir situaciones cuando habrá que saber el identificador de un elemento de radio, y cuál de ellos está marcado en este momento. Tampoco podemos prescindir de la posibilidad de conmutar los elementos de radio. 

Aparte de eso, vamos a necesitar lo métodos para trabajar con las casillas de verificación (chekbox). Eso será necesario para tener la posibilidad de saber en qué estado se encuentra ahora una casilla de verificación, además de poder cambiar su estado. En el listado del código de abajo se muestra la declaración e implementación de todos estos métodos:

class CContextMenu : public CElement
  {
public:
   //--- (1) Obtener y (2) determinar el estado del chekbox
   bool              CheckBoxStateByIndex(const int index);
   void              CheckBoxStateByIndex(const int index,const bool state);
   //--- (1) Devuelve y (2) determina id del elemento de radio por el índice
   int               RadioItemIdByIndex(const int index);
   void              RadioItemIdByIndex(const int item_index,const int radio_id);
   //--- (1) Devuelve el elemento de radio marcado, (2) cambia el elemento de radio
   int               SelectedRadioItem(const int radio_id);
   void              SelectedRadioItem(const int radio_index,const int radio_id);
   //---
  };
//+------------------------------------------------------------------+
//| Devuelve el estado del chekbox según el índice                   |
//+------------------------------------------------------------------+
bool CContextMenu::CheckBoxStateByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Si el menú contextual no tiene elementos, avisar sobre ello
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > ¡La llamada a este método debe realizarse "
              " cuando en el menú contextual hay por lo menos un elemento!");
     }
//--- Corrección en caso de superar el rango
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Devolver el estado del elemento
   return(m_items[i].CheckBoxState());
  }
//+------------------------------------------------------------------+
//| Determina el estado del chekbox según el índice                  |
//+------------------------------------------------------------------+
void CContextMenu::CheckBoxStateByIndex(const int index,const bool state)
  {
//--- Comprobar la superación del rango
   int size=::ArraySize(m_items);
   if(size<1 || index<0 || index>=size)
      return;
//--- Determinar el estado
   m_items[index].CheckBoxState(state);
  }
//+------------------------------------------------------------------+
//| Devuelve id del elemento de radio según el índice                |
//+------------------------------------------------------------------+
int CContextMenu::RadioItemIdByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Si el menú contextual no tiene elementos, avisar sobre ello
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > ¡La llamada a este método debe realizarse "
              " cuando en el menú contextual hay por lo menos un elemento!");
     }
//--- Corrección en caso de superar el rango
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Devolver el identificador
   return(m_items[i].RadioButtonID());
  }
//+------------------------------------------------------------------+
//| Determina id para el elemento de radio según el índice           |
//+------------------------------------------------------------------+
void CContextMenu::RadioItemIdByIndex(const int index,const int id)
  {
//--- Comprobar la superación del rango
   int array_size=::ArraySize(m_items);
   if(array_size<1 || index<0 || index>=array_size)
      return;
//--- Determinar el identificador
   m_items[index].RadioButtonID(id);
  }
//+------------------------------------------------------------------+
//| Devuelve el índice del elemento de radio según id                |
//+------------------------------------------------------------------+
int CContextMenu::SelectedRadioItem(const int radio_id)
  {
//--- Contador de elementos de radio
   int count_radio_id=0;
//--- Recorremos en el ciclo la lista de los elementos del menú contextual
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Ir al siguiente si no es un elemento de radio
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- Si los identificadores coinciden
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- Si es un elemento de radio activo, salimos del ciclo
         if(m_items[i].RadioButtonState())
            break;
         //--- Aumentar el contador de elementos de radio
         count_radio_id++;
        }
     }
//--- Devolver el índice
   return(count_radio_id);
  }
//+------------------------------------------------------------------+
//| Conmuta el elemento de radio según el índice y id                |
//+------------------------------------------------------------------+
void CContextMenu::SelectedRadioItem(const int radio_index,const int radio_id)
  {
//--- Contador de elementos de radio
   int count_radio_id=0;
//--- Recorremos en el ciclo la lista de los elementos del menú contextual
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Ir al siguiente si no es un elemento de radio
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- Si los identificadores coinciden
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- Conmutar el elemento de radio
         if(count_radio_id==radio_index)
            m_items[i].RadioButtonState(true);
         else
            m_items[i].RadioButtonState(false);
         //--- Aumentar el contador de elementos de radio
         count_radio_id++;
        }
     }
  }

Tenemos todo listo para crear los métodos que permitirán colocar el menú contextual en el gráfico. La colocación va a realizarse en tres pasos:

Para cada paso necesitamos su propio método privado (private). A continuación, van a llamarse en el método público (private) común. Vamos a declararlos en el cuerpo de la clase:

class CContextMenu : public CElement
  {
public:
   //--- Métodos para crear el menú contextual
   bool              CreateContextMenu(const long chart_id,const int window,const int x=0,const int y=0);
   //---
private:
   bool              CreateArea(void);
   bool              CreateItems(void);
   bool              CreateSeparateLine(const int line_number,const int x,const int y);
   //---
  };

El alto del fondo del menú contextual depende del número de elementos y líneas separadoras. Por eso no tiene sentido establecer este valor en la clase de la aplicación porque en el método CContextMenu::CreateArea() que sirve para establecer el fondo del menú contextual este valor será reemplazado. El alto del área para la línea separadora será de nueve píxeles. Por eso el número de líneas debe ser multiplicado por este valor para calcular el alto total que ocupan.

//+------------------------------------------------------------------+
//| Crea el área total del menú contextual                           |
//+------------------------------------------------------------------+
bool CContextMenu::CreateArea(void)
  {
//--- Formación del nombre del objeto
   string name=CElement::ProgramName()+"_contextmenu_bg_"+(string)CElement::Id();
//--- El cálculo del alto del menú contextual depende del número de elementos y líneas separadoras
   int items_total =ItemsTotal();
   int sep_y_size  =::ArraySize(m_sep_line)*9;
   m_y_size        =(m_item_y_size*items_total+2)+sep_y_size-(items_total-1);
//--- Establecemos el fondo del menú contextual
   if(!m_area.Create(m_chart_id,name,m_subwin,m_x,m_y,m_x_size,m_y_size))
      return(false);
//--- Establecemos las propiedades
   m_area.BackColor(m_area_color);
   m_area.Color(m_area_border_color);
   m_area.BorderType(BORDER_FLAT);
   m_area.Corner(m_corner);
   m_area.Selectable(false);
   m_area.Z_Order(m_area_zorder);
   m_area.Tooltip("\n");
//--- Sangrías desde el punto extremo
   m_area.XGap(m_x-m_wnd.X());
   m_area.YGap(m_y-m_wnd.Y());
//--- Tamaños del fondo
   m_area.XSize(m_x_size);
   m_area.YSize(m_y_size);
//--- Guardamos el puntero del objeto
   CElement::AddToArray(m_area);
   return(true);
  }

Para establecer las líneas separadoras, usaremos el método CContextMenu::CreateSeparateLine(). Como parámetros, le pasaremos el número de la línea y coordendas:

//+------------------------------------------------------------------+
//| Crea la línea separadora                                         |
//+------------------------------------------------------------------+
bool CContextMenu::CreateSeparateLine(const int line_number,const int x,const int y)
  {
//--- Guardamos el puntero del formulario
   m_sep_line[line_number].WindowPointer(m_wnd);
//--- Establecemos las propiedades
   m_sep_line[line_number].TypeSepLine(H_SEP_LINE);
   m_sep_line[line_number].DarkColor(m_sepline_dark_color);
   m_sep_line[line_number].LightColor(m_sepline_light_color);
//--- Crear la línea separadora
   if(!m_sep_line[line_number].CreateSeparateLine(m_chart_id,m_subwin,line_number,x,y,m_x_size-10,2))
      return(false);
//--- Guardamos el puntero del objeto
   CElement::AddToArray(m_sep_line[line_number].Object(0));
   return(true);
  }

El método CContextMenu::CreateSeparateLine() va a llamarse dentro del método que sirve para establecer los elementos del menú— CContextMenu::CreateItems(). Las coordenadas y la sucesión de colocación de estos controles se determinan en el mismo ciclo. Antes hemos considerado el array m_sep_line_index[]. Durante la formación del menú contextual, en este array se almacenan los números de los índices de los elementos del menú después de los cuales se coloca la línea separadora. Comparando el número de la iteración actual del ciclo con los números de los índices de los elementos del menú en el array m_sep_line_index[], se puede identificar el elemento del menú después del cual hay que colocar la línea separadora. 

Además, antes de colocar cada elemento en el menú contextual, hay que guardar el puntero al nodo anterior. Abajo se muestra el código del método CContextMenu::CreateItems() con comentarios detallados:

//+------------------------------------------------------------------+
//| Crea la lista de elementos del menú                              |
//+------------------------------------------------------------------+
bool CContextMenu::CreateItems(void)
  {
   int s =0;     // Para determinar la posición de líneas separadoras
   int x =m_x+1; // Coordenada X
   int y =m_y+1; // Coordenada Y. Va calcularse en ciclo para cada elemento del menú.
//--- Número de líneas separadoras
   int sep_lines_total=::ArraySize(m_sep_line_index);
//---
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Cálculo de la coordenada Y
      y=(i>0)? y+m_item_y_size-1 : y;
      //--- Guardamos el puntero del formulario
      m_items[i].WindowPointer(m_wnd);
       //--- Añadimos el puntero al nodo anterior
      m_items[i].PrevNodePointer(m_prev_node);
      //--- Establecemos las propiedades
      m_items[i].XSize(m_x_size-2);
      m_items[i].YSize(m_item_y_size);
      m_items[i].IconFileOn(m_path_bmp_on[i]);
      m_items[i].IconFileOff(m_path_bmp_off[i]);
      m_items[i].AreaBackColor(m_area_color);
      m_items[i].AreaBackColorOff(m_item_back_color_hover_off);
      m_items[i].AreaBorderColor(m_area_color);
      m_items[i].LabelColor(m_label_color);
      m_items[i].LabelColorHover(m_label_color_hover);
      m_items[i].RightArrowFileOn(m_right_arrow_file_on);
      m_items[i].RightArrowFileOff(m_right_arrow_file_off);
      m_items[i].IsDropdown(m_is_dropdown);
      //--- Sangrías desde el punto extremo del formulario
      m_items[i].XGap(x-m_wnd.X());
      m_items[i].YGap(y-m_wnd.Y());
      //--- Creación del elemento del menú
      if(!m_items[i].CreateMenuItem(m_chart_id,m_subwin,i,m_label_text[i],x,y))
         return(false);
      //--- Ir al siguiente si todas las líneas separadoras están colocadas
      if(s>=sep_lines_total)
         continue;
      //--- Si los índices coinciden, entonces después de este elemento hay que colocar una línea separadora
      if(i==m_sep_line_index[s])
        {
         //--- Coordenadas
         int l_x=x+5;
         y=y+m_item_y_size+2;
         //--- Colocación de la línea separadora
         if(!CreateSeparateLine(s,l_x,y))
            return(false);
         //--- Corrección de la coordenada Y para el elemento siguiente
         y=y-m_item_y_size+7;
         //--- Aumentar el contador de líneas separadoras
         s++;
        }
     }
   return(true);
  }

Luego hay que implementar el método CContextMenu::CreateContextMenu() que sirve para el uso externo. En esta fase del desarrollo hablaremos de la opción cuando el menú contextual tiene que estar vinculado obligatoriamente a un elemento del menú externo o a un elemento independiente separado. Por eso antes de crear el menú contextual, hay que pasar el puntero al nodo anterior, como ya ha sido dicho antes. Además de la comprobación de la presencia del puntero al formulario, también vamos a comprobar la existencia del puntero al nodo anterior. Para el usuario de la librería eso será un punto de referencia adicional cuando se excluye la sucesión incorrecta de la formación de la interfaz gráfica. 

Habitualmente, el menú contextual siempre se oculta después de su creación. Pues, está destinado para que lo llamen: pulsando algún otro control, o al haciendo clic en el área de trabajo. Para ocultar los objetos en cada control, antes ya hemos determinado el método Hide(). En la clase CContextMenu hay este método. Primero se ocultan los objetos del menú contextual, es decir: el fondo y las líneas separadoras. Luego en el ciclo se ocultan todos los elementos del menú. Además, para los elementos del menú se llaman sus métodos CMenuItem::Hide(). Las líneas separadoras también podrían ocultarse de la misma manera porque este control tiene su propio método CSeparateLine::Hide(). Pero puesto que es sólo un elemento del diseño, se compone de un solo objeto gráfico y no está destinado para interactuar con el usuario, durante su creación ha sido añadido al array común de objetos del menú contextual y va a ocultarse en el ciclo correspondiente.

//+------------------------------------------------------------------+
//| Oculta el menú contextual                                        |
//+------------------------------------------------------------------+
void CContextMenu::Hide(void)
  {
//--- Salir si el control está ocultado
   if(!CElement::m_is_visible)
      return;
//--- Ocultar los objetos del menú contextual
   for(int i=0; i<ObjectsElementTotal(); i++)
      Object(i).Timeframes(OBJ_NO_PERIODS);
 //--- Ocultar los elementos del menú
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].Hide();
//--- Poner a cero el foco
   CElement::MouseFocus(false);
//--- Asignar el estatus del control invisible
   CElement::m_is_visible=false;
  }

De la misma manera estarán organizados todos los métodos que sirven para manejar el menú contextual. Por eso omitimos su código aquí. Se puede verlo en los archivos adjuntos al artículo. Aquí sólo analizaremos el código del método CContextMenu::Delete() que se usa para eliminar un control. En este método, además de la eliminación de todos los objetos gráficos, se vacían todos los arrays destinados para la formación del menú contextual. Si no lo hacemos, cada vez que cambiamos el símbolo o período de tiempo del gráfico, la lista de los elementos del menú va a crecer. Cuando pasemos a las pruebas, Usted podrá experimentar con eso simplemente dejando inactivas estas líneas.

//+------------------------------------------------------------------+
//| Eliminación                                                      |
//+------------------------------------------------------------------+
void CContextMenu::Delete(void)
  {
//--- Eliminación de objetos  
   m_area.Delete();
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].Delete();
//--- Eliminar líneas separadoras
   int sep_total=::ArraySize(m_sep_line);
   for(int i=0; i<sep_total; i++)
      m_sep_line[i].Delete();
//--- Vaciar el array del control
   ::ArrayFree(m_items);
   ::ArrayFree(m_sep_line);
   ::ArrayFree(m_sep_line_index);
   ::ArrayFree(m_label_text);
   ::ArrayFree(m_path_bmp_on);
   ::ArrayFree(m_path_bmp_off);
//--- Vaciar el array de objetos
   CElement::FreeObjectsArray();
  }

Volviendo al método de la creación del menú contextual, cabe mencionar que las coordenadas se establecen respecto al nodo anterior. Haremos que el usuario de la librería también pueda establecer sus coordenadas si le surge esta necesidad. Por defecto, en el método de la creación del menú contextual CContextMenu::CreateContextMenu() las coordenadas serán nulas. Las coordenadas van a calcularse automáticamente si no se especifica por lo menos una coordenada. Si ambas coordenadas no están especificadas, el cálculo automático se cancela.

Para los menús contextuales que se abren desde otros menús contextuales, las coordenadas van a calcularse automáticamente desde la parte derecha del elemento al que este menú está vinculado. Para los menús contextuales que están vinculados a lo elementos del menú principal, el cálculo de las coordenadas se realiza desde la parte inferior de los elementos. Para manejar este sistema, necesitamos un campo más y el método para la clase CContextMenu. Añadimos la nueva enumeración al archivo Enums.mqh:

//+------------------------------------------------------------------+
//| Enumeración de los lados del anclaje del menú                    |
//+------------------------------------------------------------------+
enum ENUM_FIX_CONTEXT_MENU
  {
   FIX_RIGHT  =0,
   FIX_BOTTOM =1
  };

A la clase del menú contextual hay que añadirle el campo correspondiente y el método para establecer el modo del cálculo de coordenadas. Por defecto, será establecido el modo para calcular las coordenadas desde la parte derecha del elemento.

class CContextMenu : public CElement
  {
private:
   //--- Lado del anclaje del menú contextual
   ENUM_FIX_CONTEXT_MENU m_fix_side;
   //---
public:
   //--- Establecer el modo del anclaje del menú contextual
   void              FixSide(const ENUM_FIX_CONTEXT_MENU side)      { m_fix_side=side;                      }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void) : m_fix_side(FIX_RIGHT)
  {
  }

Abajo se muestra el código del método CContextMenu::CreateContextMenu(). Ya que la creación del control es posible sólo si hay un puntero a él, entonces una vez realizada la comprobación mencionada anteriormente, las propiedades de este nodo estarán disponibles, lo que hará posible calcular las coordenadas relativas automáticamente. La ocultación del menú contextual en el código debe ubicarse después de su creación.

//+------------------------------------------------------------------+
//| Crea el menú contextual                                          |
//+------------------------------------------------------------------+
bool CContextMenu::CreateContextMenu(const long chart_id,const int subwin,const int x=0,const int y=0)
  {
//--- Salir si no hay puntero al formulario
   if(::CheckPointer(m_wnd)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Antes de crear el menú contextual desplegable, hay que pasarle "
              " el objeto de la ventana usando el método WindowPointer(CWindow &object).");
      return(false);
     }
//--- Salir si no hay puntero al nodo anterior
   if(::CheckPointer(m_prev_node)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Antes de crear el menú contextual, hay que pasarle "
              " el puntero al nodo anterior usando el método CContextMenu::PrevNodePointer(CMenuItem &object).");
      return(false);
     }
//--- Inicialización de variables
   m_id       =m_wnd.LastId()+1;
   m_chart_id =chart_id;
   m_subwin   =subwin;
//--- Si las coordenadas no están especificadas
   if(x==0 || y==0)
     {
      m_x =(m_fix_side==FIX_RIGHT)? m_prev_node.X2()-3 : m_prev_node.X()+1;
      m_y =(m_fix_side==FIX_RIGHT)? m_prev_node.Y()-1  : m_prev_node.Y2()-1;
     }
//--- Si las coordenadas están especificadas
   else
     {
      m_x =x;
      m_y =y;
     } 
//--- Sangrías desde el punto extremo
   CElement::XGap(m_x-m_wnd.X());
   CElement::YGap(m_y-m_wnd.Y());
//--- Creando menú contextual
   if(!CreateArea())
      return(false);
   if(!CreateItems())
      return(false);
//--- Ocultar control
   Hide();
   return(true);
  }

En la clase CMenuItem en el método CreateMenuItem() también hay que insertar la comprobación de la presencia del puntero al nodo anterior, pero con una condición adicional. Si no hay puntero, eso va a significar que se supone un elemento del menú independiente (es decir, que no forma parte del menú contextual). Estos elementos pueden ser sólo del tipo simple (MI_SIMPLE) o los elementos que contienen el menú contextual (MI_HAS_CONTEXT_MENU). Puede que ahora eso sea algo difícil de comprender, pero todo va a ser bastante claro después de que analicemos los ejemplos al final del artículo.

Coloque este código dentro del método CMenuItem::CreateMenuItem() inmediatamente después de comprobar la presencia del puntero al formulario:

//--- Si no hay puntero al nodo anterior, entonces
//    se supone un elemento del menú independiente, es decir que no forma parte del menú contextual
   if(::CheckPointer(m_prev_node)==POINTER_INVALID)
     {
      //--- Salir si el tipo establecido no corresponde
      if(m_type_menu_item!=MI_SIMPLE && m_type_menu_item!=MI_HAS_CONTEXT_MENU)
        {
         ::Print(__FUNCTION__," > El tipo del elemento del menú independiente puede ser sólo MI_SIMPLE o MI_HAS_CONTEXT_MENU, ",
                 "es decir sólo con la presencia del menú contextual.\n",
                 __FUNCTION__," > Se puede establecer el tipo del menú usando el método CMenuItem::TypeMenuItem()");
         return(false);
        }
     }



Prueba de colocación del menú contextual

Ahora podemos probar la colocación del menú contextual en el gráfico. Incluimos el archivo ContextMenu.mqh con la clase del menú contextual CContextMenu en la librería:

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

Creamos la instancia de la clase CContextMenu en la clase personalizada de la aplicación CProgram y declaramos el método para crear el menú contextual. No hace falta especificar las sangrías desde el punto extremo del formulario ya que van a calcularse respecto al elemento del menú al que serán adjuntadas.

class CProgram : public CWndEvents
  {
private:
   //--- Elemento del menú y menú contextual
   CMenuItem         m_menu_item1;
   CContextMenu      m_mi1_contextmenu1;
   //---
private:
#define MENU_ITEM1_GAP_X (6)
#define MENU_ITEM1_GAP_Y (25)
   bool              CreateMenuItem1(const string item_text);
   bool              CreateMI1ContextMenu1(void);
  };

Ahora vamos a formar el menú contextual. Que tenga cinco elementos: tres simples (MI_SIMPLE) y dos elementos tipo “checbox” (MI_CHECKBOX). Para los elementos simples conectamos los recursos con imágenes para sus iconos (fuera del cuerpo del método). Para el elemento disponible, la imagen será en color, y para el bloqueado será descolorida. Se puede descargarlas siguiendo los enlaces al final del artículo. Luego, al principio del método hay que guardar el puntero al formulario y al nodo anterior en el menú contextual. Recordamos que sin estas acciones no se puede crear la interfaz gráfica, y el programa será descargado del gráfico. Luego ponemos los arrays con (1) la descripción de elementos (texto a mostrar), con imágenes de estados (2) disponibles y (3) bloqueados, así como con (4) el tipo de elementos. Después de eso, hay que indicar las propiedades comunes para todos los elementos, y luego añadirlas en el ciclo a la clase del menú contextual usando el método CContextMenu::AddItem(). Insertamos la línea separadora después del segundo elemento (índice 1). Después de eso, por fin podemos colocar el menú contextual sobre el gráfico. Al final del método hay que añadir el puntero al control a la base. A continuación, se muestra el código detallado:

//+------------------------------------------------------------------+
//| Crea el menú contextual                                          |
//+------------------------------------------------------------------+
#resource "\\Images\\Controls\\coins.bmp"
#resource "\\Images\\Controls\\coins_colorless.bmp"
#resource "\\Images\\Controls\\line_chart.bmp"
#resource "\\Images\\Controls\\line_chart_colorless.bmp"
#resource "\\Images\\Controls\\safe.bmp"
#resource "\\Images\\Controls\\safe_colorless.bmp"
//---
bool CProgram::CreateMI1ContextMenu1(void)
  {
//--- Cinco elementos en el menú contextual
#define CONTEXTMENU_ITEMS1 5
//--- Guardamos el puntero a la ventana
   m_mi1_contextmenu1.WindowPointer(m_window);
 //--- Guardamos el puntero al nodo anterior
   m_mi1_contextmenu1.PrevNodePointer(m_menu_item1);
 //--- Arrays de los nombres de los elementos del menú
   string items_text[CONTEXTMENU_ITEMS1]=
     {
      "ContextMenu 1 Item 1",
      "ContextMenu 1 Item 2",
      "ContextMenu 1 Item 3",
      "ContextMenu 1 Item 4",
      "ContextMenu 1 Item 5"
     };
//--- Array de los iconos para el modo disponible
   string items_bmp_on[CONTEXTMENU_ITEMS1]=
     {
      "Images\\Controls\\coins.bmp",
      "Images\\Controls\\line_chart.bmp",
      "Images\\Controls\\safe.bmp",
      "",""
     };
//--- Array de los iconos para el modo bloqueado
   string items_bmp_off[CONTEXTMENU_ITEMS1]=
     {
      "Images\\Controls\\coins_colorless.bmp",
      "Images\\Controls\\line_chart_colorless.bmp",
      "Images\\Controls\\safe_colorless.bmp",
      "",""
     };
//--- Arrays de los tipos de los elementos del menú
   ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS1]=
     {
      MI_SIMPLE,
      MI_SIMPLE,
      MI_SIMPLE,
      MI_CHECKBOX,
      MI_CHECKBOX
     };
//--- Establecemos las propiedades antes de la creación
   m_mi1_contextmenu1.XSize(160);
   m_mi1_contextmenu1.ItemYSize(24);
   m_mi1_contextmenu1.AreaBackColor(C'240,240,240');
   m_mi1_contextmenu1.AreaBorderColor(clrSilver);
   m_mi1_contextmenu1.ItemBackColorHover(C'240,240,240');
   m_mi1_contextmenu1.ItemBackColorHoverOff(clrLightGray);
   m_mi1_contextmenu1.ItemBorderColor(C'240,240,240');
   m_mi1_contextmenu1.LabelColor(clrBlack);
   m_mi1_contextmenu1.LabelColorHover(clrWhite);
   m_mi1_contextmenu1.RightArrowFileOff("Images\\EasyAndFastGUI\\Controls\\RightTransp_black.bmp");
   m_mi1_contextmenu1.SeparateLineDarkColor(C'160,160,160');
   m_mi1_contextmenu1.SeparateLineLightColor(clrWhite);
//--- Añadimos los elementos en el menú contextual
   for(int i=0; i<CONTEXTMENU_ITEMS1; i++)
      m_mi1_contextmenu1.AddItem(items_text[i],items_bmp_on[i],items_bmp_off[i],items_type[i]);
//--- Línea separadora tras el segundo elemento
   m_mi1_contextmenu1.AddSeparateLine(1);
//--- Crear el menú contextual
   if(!m_mi1_contextmenu1.CreateContextMenu(m_chart_id,m_subwin))
      return(false);
//--- Añadimos el puntero al control a la base
   CWndContainer::AddToElementsArray(0,m_mi1_contextmenu1);
   return(true);
  }

Ahora añadimos la llamada al método de creación del menú contextual al método de creación de la interfaz gráfica. Puesto que durante la colocación, el menú contextual se oculta según las reglas, hay que mostrarlo forzosamente para esta prueba. Abajo se muestra qué líneas hay que insertar en el método CProgram::CreateTradePanel().

//+------------------------------------------------------------------+
//| Crea el panel de trading                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Creación del formulario para los controles
   if(!CreateWindow("EXPERT PANEL"))
      return(false);
//--- Creación de controles:
//    Elemento del menú
   if(!CreateMenuItem1("Menu item"))
      return(false);
   if(!CreateMI1ContextMenu1())
      return(false);
//--- Línea separadora
   if(!CreateSepLine())
      return(false);
//--- Mostrar el menú contextual
   m_mi1_contextmenu1.Show();
 //--- Redibujar el gráfico
   m_chart.Redraw();
   return(true);
  }

Compile los archivos e inicie el programa en el gráfico. Debe obtener el resultado como en la captura de pantalla de abajo:

Fig. 4. Prueba del control “menú contextual”.

Fig. 4. Prueba del control “menú contextual”.


En esta fase, cuando situamos el cursor sobre los elementos del menú contextual, ellos no cambian de color. Podemos implementar esta funcionalidad en la clase del menú contextual CContextMenu, o podemos usar la funcionalidad ya hecha en la clase del elemento del menú CMenuItem. Después de la colocación del menú contextual sobre el gráfico, su puntero se añade a la base pero los punteros a cada elemento del menú quedan inaccesibles para el uso en la clase del manejo de eventos CWndEvents con la implementación actual de inserción de punteros en el array común de controles. Para cada control complejo que se compone de varios controles, en la clase CWndContainer vamos a crear un método que permite obtener los punteros a estos controles. Precisamente para eso antes en la clase CContextMenu ha sido implementado el método ItemPointerByIndex() a través del cual se puede obtener el puntero a un elemento del menú indicando su índice.

 


Mejorando la clase para el almacenamiento de punteros a todos los controles

En la clase CWndContainer implementamos el método AddContextMenuElements() para trabajar con el control “menú contextual”. Vamos a pasar el índice del formulario y el objeto del control como parámetros. Al inicio del método, se comprueba si el control pasado es un menú contextual. Si es así, vamos a necesitar el puntero al menú contextual (CContextMenu) para obtener el acceso a sus métodos. ¿Pero cómo hacerlo si se pasa el objeto de la clase base (CElement)? Para eso basta con declarar el puntero con el tipo CContextMenu y asignarle el puntero del objeto pasado (en el código de abajo se marca con el color amarillo). De esta manera, se abre el acceso a los elementos del menú contextual, y luego se añaden en el ciclo al array común de los controles de su formulario. Al final de cada iteración, los elementos del menú se pasan en el método CWndContainer::AddToObjectsArray() para el almacenamiento de los punteros a objetos de los que se componen en el array de objetos tipo CChartObject.

//+------------------------------------------------------------------+
//| Clase para almacenar todos los objetos de la interfaz            |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
  //--- Guarda los punteros a los controles del menú contextual en la base
   bool              AddContextMenuElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Guarda los punteros a los objetos del menú contextual en la base |
//+------------------------------------------------------------------+
bool CWndContainer::AddContextMenuElements(const int window_index,CElement &object)
  {
//--- Salimos si no es menú contextual
   if(object.ClassName()!="CContextMenu")
      return(false);
//--- Obtenemos el puntero al menú contextual
   CContextMenu *cm=::GetPointer(object);
//--- Guardamos los punteros a sus objetos en la base
   int items_total=cm.ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
       //--- Aumentar el array de controles
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      //--- Obtener el puntero al elemento del menú
      CMenuItem *mi=cm.ItemPointerByIndex(i);
      //--- Guardamos el puntero en el array
      m_wnd[window_index].m_elements[size]=mi;
       //--- Añadimos los punteros a todos los objetos del elemento del menú al array común
      AddToObjectsArray(window_index,mi);
     }
//---
   return(true);
  }

Va a llamarse en el método CWndContainer::AddToElementsArray() inmediatamente después del aumento del contador de controles. El código de abajo ha sido reducido para ahorrar el espacio del artículo. Se puede ver su versión completa en los archivos adjuntos al artículo.

//+------------------------------------------------------------------+
//| Añade el puntero al array de controles                           | 
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElement &object)
  {
//--- Si en la base no hay formularios para los controles
//--- Si se solicita el formulario no existente
//--- Añadimos al array común de controles
 //--- Añadir los objetos del control al array común de objetos
//--- Recordamos id del último control en todos los formularios
//--- Aumentamos el contador de los identificadores de controles

//--- Guarda los punteros a los objetos del menú contextual en la base
   if(AddContextMenuElements(window_index,object))
      return;
  }

De la misma manera, luego vamos a completar el código del método CWndContainer::AddToElementsArray() con otros métodos parecidos para los controles complejos (compuestos).

Si ahora compilamos todos los archivos y iniciamos el programa en el gráfico, cuando situamos el cursor sobre los elementos del menú contextual, ellos van a cambiar su apariencia:

Fig. 5. Prueba de elementos en el menú contextual.

Fig. 5. Prueba de elementos en el menú contextual.


Hemos terminado el desarrollo de la clase para crear el menú contextual. Ahora hay que configurar su manejador de eventos, así como el manejador de eventos de los elementos del menú. Nos ocuparemos de eso en el siguiente artículo.

 


Conclusión

Para este momento, nuestra librería ya incluye tres clases para la creación de los siguientes controles:

En el siguiente artículo vamos a configurar los manejadores de eventos de la librería en la clase principal y en las clases de los controles creados anteriormente.

Más abajo se puede descargar los archivos comprimidos con los ficheros de la librería en esta fase del desarrollo, imágenes y ficheros de los programas examinados en el artículo (EA, indicadores y script) para realizar las pruebas en los terminales  MetaTrader 4  y  MetaTrader 5. Si le surgen algunas preguntas sobre el uso del material de estos archivos, puede dirigirse a la descripción detallada del proceso de desarrollo de la librería en uno de los artículos listados más abajo, o bien hacer su pregunta en los comentarios para el artículo. 

Lista de artículos (Capítulos) de la segunda parte: