Interfaces gráficas II: Controles "Línea separadora" y "Menú contextual" (Capítulo 2)
Anatoli Kazharski | 10 marzo, 2016
Índice.
- Introducción
- Desarrollando la clase para crear una línea separadora
- Prueba de colocación de la línea separadora
- Desarrollando la clase para crear un menú contextual
- Prueba de colocación del menú contextual
- Mejorando la clase para el almacenamiento de punteros a todos los controles
- Conclusión
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
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:
- Tipo de la línea separadora: (1) horizontal, (2) vertical.
- Color de la parte oscura.
- Color de la parte clara.
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:
- identificador del gráfico;
- número de la ventana del gráfico;
- número del índice de la línea. Es necesario para los casos cuando en el menú contextual u otros controles de la interfaz hay que crear varias líneas separadoras en el ciclo. En este caso el identificador del control no será suficiente para crear el nombre único del objeto gráfico;
- coordenadas;
- tamaños.
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 la clase del control todavía no existe en la base, hay que incluir su archivo en el archivo WndContainer.mqh.
- Puesto que la clase de usuario de la aplicación (en nuestro caso es CProgram) tiene que ser derivada de CWndContainer -> CWndEvents, entonces después de incluir el archivo del control, la creación de la instancia de la clase del control y su método quedan disponibles.
- Luego hay que llamar al método de la creación del control en el método donde se crea la interfaz gráfica del programa.
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”.
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.
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:
- Creación del fondo del menú contextual.
- Creación de los elementos del menú.
- Creación de las líneas separadoras.
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”.
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.
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:
- elemento del menú;
- línea separadora;
- menú contextual.
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:
- Interfaces gráficas II: Control “Elemento del menú” (Capítulo 2)
- Interfaces gráficas II: Controles “Línea separadora” y “Menú contextual” (Capítulo 2)
- Interfaces gráficas II: Configuración de los manejadores de eventos de la librería (Capítulo 3).
- Interfaces gráficas II: Control “Menú principal” (Capítulo 2)