Tablas en el paradigma MVC en MQL5: integramos el componente Model en el componente View
Índice
- Introducción
- Perfeccionamos las clases de la biblioteca
- Clase de celda de la tabla (View)
- Clase de fila de la tabla (View)
- Clase de encabezado de columna de la tabla (View)
- Clase del encabezado de la tabla (View)
- Clase de la tabla (View)
- Probamos el resultado
- Conclusión
Introducción
En el artículo «Clases de tabla y encabezado basadas en el modelo de tabla de MQL5: Aplicación del concepto MVC», hemos completado la creación de la clase del modelo de la tabla (en el concepto MVC, el componente Modelo). A continuación, nos dedicamos a desarrollar una biblioteca de controles simples que permiten crear, a partir de ellos, controles completamente distintos en cuanto a finalidad y complejidad. En concreto, el componente Vista para crear el control TableView.
Este artículo estará dedicado a la implementación de la interacción entre el componente Modelo y el componente Vista. En otras palabras: hoy vamos a unir los datos tabulares con su representación gráfica en un único control.
El control se creará a partir de la clase del objeto Panel y estará compuesto por varios elementos:
- Panel: el panel base o soporte al que se adjuntan el encabezado de la tabla y el área de datos de la tabla;
- Panel: el encabezado de la tabla, compuesto por una serie de elementos —encabezados de columnas— creados a partir de la clase del objeto Button;
- Container: contenedor de datos tabulares con posibilidad de desplazamiento del contenido;
- Panel: panel para colocar en él las filas de la tabla (adjunto al contenedor (punto 3) y, al salir de los límites del contenedor, se desplaza mediante las barras de desplazamiento del contenedor);
- Panel: fila de la tabla; panel para dibujar en él las celdas de la tabla, adjunto al panel (punto 4); se crean tantos objetos de este tipo como filas tenga la tabla;
- La clase de celda de la tabla es una nueva clase que permite dibujar en las coordenadas especificadas sobre el lienzo indicado (CCanvas). Un objeto de este tipo se adjunta a un objeto de fila de la tabla (punto 5); los lienzos del objeto de fila de la tabla se especifican como elementos de dibujo, y la celda de la tabla se dibuja en este panel en las coordenadas indicadas. El área de cada celda de la tabla se define en el objeto de fila de la tabla (punto 5) mediante una instancia de la clase CBound, y a este objeto se le fija un objeto de la clase de celda de la tabla.
Este algoritmo de construcción de filas y celdas de la tabla, en el que cada fila se distribuye en áreas horizontales a las que se fijan objetos de la clase de celdas, permite ordenar y cambiar fácilmente la posición de las celdas con solo reasignarlas a las áreas necesarias de la fila.
A la vista de lo anterior, entendemos que necesitamos una nueva clase de objeto a la que se le pase un puntero al control y en cuyo lienzo se realice el dibujo. Actualmente, todos los objetos de la biblioteca, al crearse mediante el operador new, crean sus propios objetos de lienzo (de fondo y de primer plano). Nosotros necesitamos un objeto que permita establecer y obtener su tamaño, así como asignarle un puntero al control en el que se realizará el dibujo.
La clase CBound permite establecer y obtener las dimensiones del objeto creado, y forma parte del objeto base de todos los controles. En este mismo objeto se crean también los lienzos del fondo y del primer plano. Por lo tanto, tenemos que crear otro objeto intermedio que herede del objeto base y que incluya la clase CBound para establecer y obtener las dimensiones. La clase en la que se crean los objetos CCanvas heredará de este objeto. De este modo, a partir de esta clase intermedia podremos derivar la clase a la que pasaremos el puntero al control sobre el que vamos a dibujar.
Vamos a modificar los archivos de las clases ya creadas y, después, escribiremos las nuevas.
Perfeccionamos las clases de la biblioteca
Todos los archivos de la biblioteca que se está desarrollando se encuentran en la ruta \MQL5\Indicators\Tables\Controls\. Si aún no los tienes, puedes obtener la versión anterior de todos los archivos en el artículo anterior. Además, para el proyecto se necesitará el archivo de clases de tablas (componente Model), que se puede obtener en el artículo, donde finalizamos su desarrollo. El archivo Tables.mqh debe guardarse en la carpeta \MQL5\Indicators\Tables\. Se modificarán los archivos Base.mqh y Controls.mqh.
Al principio del archivo Tables.mqh, en la sección de macros, escribiremos el identificador del archivo:
//+------------------------------------------------------------------+ //| Macros | //+------------------------------------------------------------------+ #define __TABLES__ // Identificador de este archivo #define MARKER_START_DATA -1 // Marcador de inicio de datos en el archivo #define MAX_STRING_LENGTH 128 // Longitud máxima de la cadena en la celda #define CELL_WIDTH_IN_CHARS 19 // Anchura de la celda en caracteres
Esto es necesario para que la macro MARKER_START_DATA pueda declararse en dos archivos de inclusión diferentes, de modo que puedan compilarse por separado.
Abramos el archivo Base.mqh. Incluyamos en él el archivo de clases del modelo de tabla y añadamos la declaración anticipada de las nuevas clases:
//+------------------------------------------------------------------+ //| Base.mqh | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Bibliotecas incluidas | //+------------------------------------------------------------------+ #include <Canvas\Canvas.mqh> // Clase SB CCanvas #include <Arrays\List.mqh> // Clase SB CList #include "..\Tables.mqh" //--- Declaración anticipada de las clases de los controles class CBoundedObj; // Clase base que almacena las dimensiones del objeto class CCanvasBase; // Clase base del lienzo de elementos gráficos class CCounter; // Clase del contador de retardo class CAutoRepeat; // Clase de repetición automática de eventos class CImagePainter; // Clase de dibujo de imágenes class CVisualHint; // Clase de ayuda emergente class CLabel; // Clase de etiqueta de texto class CButton; // Clase de botón simple class CButtonTriggered; // Clase de botón de dos posiciones class CButtonArrowUp; // Clase de botón con flecha hacia arriba class CButtonArrowDown; // Clase de botón con flecha hacia abajo class CButtonArrowLeft; // Clase de botón con flecha hacia la izquierda class CButtonArrowRight; // Clase de botón con flecha hacia la derecha class CCheckBox; // Clase del control CheckBox class CRadioButton; // Clase del control RadioButton class CScrollBarThumbH; // Clase del control deslizante de la barra de desplazamiento horizontal class CScrollBarThumbV; // Clase del control deslizante de la barra de desplazamiento vertical class CScrollBarH; // Clase de barra de desplazamiento horizontal class CScrollBarV; // Clase de barra de desplazamiento vertical class CTableRowView; // Clase de representación visual de una fila de la tabla class CPanel; // Clase del control Panel class CGroupBox; // Clase del control GroupBox class CContainer; // Clase del control Container //+------------------------------------------------------------------+ //| Definiciones de macros | //+------------------------------------------------------------------+
En la sección de definiciones de macros, la declaración de la macro MARKER_START_DATA la envolveremos con una comprobación de su existencia:
//+------------------------------------------------------------------+ //| Definiciones de macros | //+------------------------------------------------------------------+ #define clrNULL 0x00FFFFFF // Color transparente para CCanvas #ifndef __TABLES__ #define MARKER_START_DATA -1 // Marcador de inicio de datos en el archivo #endif #define DEF_FONTNAME "Calibri" // Fuente por defecto #define DEF_FONTSIZE 10 // Tamaño de la fuente por defecto #define DEF_EDGE_THICKNESS 3 // Grosor de la zona de captura de límite/ángulo
Ahora, declarar la misma sustitución de macro en dos archivos distintos no provocará errores de compilación, ni al compilar cada archivo por separado ni al compilar ambos juntos.
En la enumeración de tipos de elementos gráficos añadiremos nuevos tipos y estableceremos un nuevo tipo máximo para el elemento activo:
//+------------------------------------------------------------------+ //| Enumeraciones | //+------------------------------------------------------------------+ enum ENUM_ELEMENT_TYPE // Enumeración de tipos de elementos gráficos { ELEMENT_TYPE_BASE = 0x10000, // Objeto básico de los elementos gráficos ELEMENT_TYPE_COLOR, // Objeto de color ELEMENT_TYPE_COLORS_ELEMENT, // Objeto de colores de un elemento de objeto gráfico ELEMENT_TYPE_RECTANGLE_AREA, // Área rectangular del elemento ELEMENT_TYPE_IMAGE_PAINTER, // Objeto para dibujar imágenes ELEMENT_TYPE_COUNTER, // Objeto contador ELEMENT_TYPE_AUTOREPEAT_CONTROL, // Objeto de repetición automática de eventos ELEMENT_TYPE_BOUNDED_BASE, // Objeto básico de las dimensiones de los elementos gráficos ELEMENT_TYPE_CANVAS_BASE, // Objeto básico del lienzo de los elementos gráficos ELEMENT_TYPE_ELEMENT_BASE, // Objeto básico de los elementos gráficos ELEMENT_TYPE_HINT, // Ayuda emergente ELEMENT_TYPE_LABEL, // Etiqueta de texto ELEMENT_TYPE_BUTTON, // Botón simple ELEMENT_TYPE_BUTTON_TRIGGERED, // Botón de dos posiciones ELEMENT_TYPE_BUTTON_ARROW_UP, // Botón con flecha hacia arriba ELEMENT_TYPE_BUTTON_ARROW_DOWN, // Botón con flecha hacia abajo ELEMENT_TYPE_BUTTON_ARROW_LEFT, // Botón con flecha hacia la izquierda ELEMENT_TYPE_BUTTON_ARROW_RIGHT, // Botón con flecha hacia la derecha ELEMENT_TYPE_CHECKBOX, // Control CheckBox ELEMENT_TYPE_RADIOBUTTON, // Control RadioButton ELEMENT_TYPE_SCROLLBAR_THUMB_H, // Deslizador de la barra de desplazamiento horizontal ELEMENT_TYPE_SCROLLBAR_THUMB_V, // Deslizador de la barra de desplazamiento vertical ELEMENT_TYPE_SCROLLBAR_H, // Control ScrollBarHorisontal ELEMENT_TYPE_SCROLLBAR_V, // Control ScrollBarVertical ELEMENT_TYPE_TABLE_CELL, // Celda de la tabla (Vista) ELEMENT_TYPE_TABLE_ROW, // Fila de la tabla (Vista) ELEMENT_TYPE_TABLE_COLUMN_CAPTION, // Encabezado de columna de la tabla (Vista) ELEMENT_TYPE_TABLE_HEADER, // Encabezado de la tabla (Vista) ELEMENT_TYPE_TABLE, // Tabla (Vista) ELEMENT_TYPE_PANEL, // Control Panel ELEMENT_TYPE_GROUPBOX, // Control GroupBox ELEMENT_TYPE_CONTAINER, // Control Container }; #define ACTIVE_ELEMENT_MIN ELEMENT_TYPE_LABEL // Valor mínimo de la lista de elementos activos #define ACTIVE_ELEMENT_MAX ELEMENT_TYPE_TABLE_HEADER // Valor máximo de la lista de elementos activos
Añadamos nuevos valores devueltos a la función que retorna el nombre abreviado del elemento según su tipo:
//+------------------------------------------------------------------+ //| Devuelve el nombre abreviado del elemento según su tipo | //+------------------------------------------------------------------+ string ElementShortName(const ENUM_ELEMENT_TYPE type) { switch(type) { case ELEMENT_TYPE_ELEMENT_BASE : return "BASE"; // Objeto básico de los elementos gráficos case ELEMENT_TYPE_HINT : return "HNT"; // Ayuda emergente case ELEMENT_TYPE_LABEL : return "LBL"; // Etiqueta de texto case ELEMENT_TYPE_BUTTON : return "SBTN"; // Botón simple case ELEMENT_TYPE_BUTTON_TRIGGERED : return "TBTN"; // Botón de dos posiciones case ELEMENT_TYPE_BUTTON_ARROW_UP : return "BTARU"; // Botón con flecha hacia arriba case ELEMENT_TYPE_BUTTON_ARROW_DOWN : return "BTARD"; // Botón con flecha hacia abajo case ELEMENT_TYPE_BUTTON_ARROW_LEFT : return "BTARL"; // Botón con flecha hacia la izquierda case ELEMENT_TYPE_BUTTON_ARROW_RIGHT : return "BTARR"; // Botón con flecha hacia la derecha case ELEMENT_TYPE_CHECKBOX : return "CHKB"; // Control CheckBox case ELEMENT_TYPE_RADIOBUTTON : return "RBTN"; // Control RadioButton case ELEMENT_TYPE_SCROLLBAR_THUMB_H : return "THMBH"; // Deslizador de la barra de desplazamiento horizontal case ELEMENT_TYPE_SCROLLBAR_THUMB_V : return "THMBV"; // Deslizador de la barra de desplazamiento vertical case ELEMENT_TYPE_SCROLLBAR_H : return "SCBH"; // Control ScrollBarHorisontal case ELEMENT_TYPE_SCROLLBAR_V : return "SCBV"; // Control ScrollBarVertical case ELEMENT_TYPE_TABLE_CELL : return "TCELL"; // Celda de la tabla (Vista) case ELEMENT_TYPE_TABLE_ROW : return "TROW"; // Fila de la tabla (Vista) case ELEMENT_TYPE_TABLE_COLUMN_CAPTION : return "TCAPT"; // Encabezado de columna de la tabla (Vista) case ELEMENT_TYPE_TABLE_HEADER : return "THDR"; // Encabezado de la tabla (Vista) case ELEMENT_TYPE_TABLE : return "TABLE"; // Tabla (Vista) case ELEMENT_TYPE_PANEL : return "PNL"; // Control Panel case ELEMENT_TYPE_GROUPBOX : return "GRBX"; // Control GroupBox case ELEMENT_TYPE_CONTAINER : return "CNTR"; // Control Container default : return "Unknown"; // Desconocido } }
En la clase base de los elementos gráficos, haremos virtual el método de establecimiento del identificador, ya que será necesario redefinirlo en las nuevas clases:
//+------------------------------------------------------------------+ //| Clase base de los elementos gráficos | //+------------------------------------------------------------------+ class CBaseObj : public CObject { protected: int m_id; // Identificador ushort m_name[]; // Nombre public: //--- Establece (1) el nombre y (2) el identificador void SetName(const string name) { ::StringToShortArray(name,this.m_name); } virtual void SetID(const int id) { this.m_id=id; } //--- Devuelve (1) el nombre y (2) el identificador string Name(void) const { return ::ShortArrayToString(this.m_name); } int ID(void) const { return this.m_id; } //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_BASE); } //--- (1) Devuelve, (2) muestra en el registro la descripción del objeto virtual string Description(void); virtual void Print(void); //--- Constructor/destructor CBaseObj (void) : m_id(-1) { this.SetName(""); } ~CBaseObj (void) {} };
Cada objeto contiene un objeto de la clase de área rectangular CBound, que devuelve el tamaño del control. Además, cada control tiene una lista de objetos CBound. Esta lista permite almacenar múltiples objetos CBound, que a su vez permiten definir múltiples áreas diferentes en la superficie del elemento gráfico.
Cada una de estas áreas puede utilizarse para distintos escenarios. Por ejemplo, se puede definir una zona determinada dentro de un elemento gráfico, detectar si el cursor se encuentra dentro de esa área y reaccionar ante la interacción de dicha área con el cursor del ratón. Es posible colocar un control dentro de un área determinada para que se muestre dentro de dicha área. Para ello, es necesario permitir almacenar en el objeto CBound un puntero al control.
Vamos a implementar esta posibilidad en la clase CBound:
//+------------------------------------------------------------------+ //| Clase de área rectangular | //+------------------------------------------------------------------+ class CBound : public CBaseObj { protected: CBaseObj *m_assigned_obj; // Objeto asignado al área CRect m_bound; // Estructura del área rectangular public: //--- Modifica (1) la anchura, (2) la altura y (3) el tamaño del rectángulo delimitador void ResizeW(const int size) { this.m_bound.Width(size); } void ResizeH(const int size) { this.m_bound.Height(size); } void Resize(const int w,const int h) { this.m_bound.Width(w); this.m_bound.Height(h); } //--- Establece (1) la coordenada X, (2) la coordenada Y o (3) ambas coordenadas del rectángulo delimitador void SetX(const int x) { this.m_bound.left=x; } void SetY(const int y) { this.m_bound.top=y; } void SetXY(const int x,const int y) { this.m_bound.LeftTop(x,y); } //--- (1) Establece, (2) desplaza el rectángulo delimitador a las coordenadas indicadas / según el tamaño de desplazamiento especificado void Move(const int x,const int y) { this.m_bound.Move(x,y); } void Shift(const int dx,const int dy) { this.m_bound.Shift(dx,dy); } //--- Devuelve las coordenadas, las dimensiones y los límites del objeto int X(void) const { return this.m_bound.left; } int Y(void) const { return this.m_bound.top; } int Width(void) const { return this.m_bound.Width(); } int Height(void) const { return this.m_bound.Height(); } int Right(void) const { return this.m_bound.right-(this.m_bound.Width() >0 ? 1 : 0);} int Bottom(void) const { return this.m_bound.bottom-(this.m_bound.Height()>0 ? 1 : 0);} //--- Devuelve un indicador que señala si el cursor se encuentra dentro del área bool Contains(const int x,const int y) const { return this.m_bound.Contains(x,y); } //--- (1) Asigna, (2) devuelve un puntero al elemento asignado void AssignObject(CBaseObj *obj) { this.m_assigned_obj=obj; } CBaseObj *GetAssignedObj(void) { return this.m_assigned_obj; } //--- Devuelve la descripción del objeto virtual string Description(void); //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_RECTANGLE_AREA); } //--- Constructores/destructor CBound(void) { ::ZeroMemory(this.m_bound); } CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h); } ~CBound(void) { ::ZeroMemory(this.m_bound); } };
Actualmente, el objeto CBound de área rectangular está declarado en la clase CCanvasBase, en la que se crean dos objetos CCanvas para dibujar el fondo y el primer plano del elemento gráfico. Es decir, todos los elementos gráficos constan de dos lienzos. Pero debemos crear una clase que permita indicar el control en cuyos lienzos se debe dibujar. Sin crear lienzos propios y, al mismo tiempo, sin apartarnos del concepto general de creación de elementos gráficos.
Por lo tanto, debemos trasladar el objeto de la clase CBound de la clase CCanvasBase a otra clase —la clase padre—, que no creará objetos de lienzo y de la que heredará la clase CCanvasBase. Y en la clase CCanvasBase no hay que declarar instancias de dos CCanvas, sino punteros a los lienzos que se van a crear, y crear un método que cree dos objetos de lienzo. También es necesario declarar un indicador que indique si el elemento gráfico gestiona o no los objetos de lienzo de fondo y de primer plano. Esto es necesario para gestionar la eliminación de los objetos de lienzo creados.
En el archivo Base.mqh, justo después de la clase CAutoRepeat y antes de la clase CCanvasBase, escribiremos una nueva clase CBoundedObj, que hereda de CBaseObj, a la que simplemente trasladaremos todos los métodos relacionados con el objeto CBound de la clase CCanvasBase:
//+------------------------------------------------------------------+ //| Clase base que almacena las dimensiones del objeto | //+------------------------------------------------------------------+ class CBoundedObj : public CBaseObj { protected: CBound m_bound; // Límites del objeto bool m_canvas_owner; // Bandera de propiedad de los lienzos public: //--- Devuelve las coordenadas, las dimensiones y los límites del objeto int X(void) const { return this.m_bound.X(); } int Y(void) const { return this.m_bound.Y(); } int Width(void) const { return this.m_bound.Width(); } int Height(void) const { return this.m_bound.Height(); } int Right(void) const { return this.m_bound.Right(); } int Bottom(void) const { return this.m_bound.Bottom(); } //--- Modifica (1) la anchura, (2) la altura y (3) el tamaño del rectángulo delimitador void BoundResizeW(const int size) { this.m_bound.ResizeW(size); } void BoundResizeH(const int size) { this.m_bound.ResizeH(size); } void BoundResize(const int w,const int h) { this.m_bound.Resize(w,h); } //--- Establece (1) la coordenada X, (2) la coordenada Y o (3) ambas coordenadas del rectángulo delimitador void BoundSetX(const int x) { this.m_bound.SetX(x); } void BoundSetY(const int y) { this.m_bound.SetY(y); } void BoundSetXY(const int x,const int y) { this.m_bound.SetXY(x,y); } //--- (1) Establece, (2) desplaza el rectángulo delimitador a las coordenadas indicadas / según el tamaño de desplazamiento especificado void BoundMove(const int x,const int y) { this.m_bound.Move(x,y); } void BoundShift(const int dx,const int dy) { this.m_bound.Shift(dx,dy); } //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto //virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_BOUNDED_BASE); } CBoundedObj (void) : m_canvas_owner(true) {} CBoundedObj (const string user_name,const int id,const int x,const int y,const int w,const int h); ~CBoundedObj (void){} }; //+------------------------------------------------------------------+ //| CBoundedObj::Constructor | //+------------------------------------------------------------------+ CBoundedObj::CBoundedObj(const string user_name,const int id,const int x,const int y,const int w,const int h) : m_canvas_owner(true) { //--- Obtenemos el identificador corregido del gráfico y la distancia en píxeles a lo largo del eje vertical Y this.m_bound.SetName(user_name); this.m_bound.SetID(id); this.m_bound.SetXY(x,y); this.m_bound.Resize(w,h); } //+------------------------------------------------------------------+ //| CBoundedObj::Guardado en un archivo | //+------------------------------------------------------------------+ bool CBoundedObj::Save(const int file_handle) { //--- Guardamos los datos del objeto padre if(!CBaseObj::Save(file_handle)) return false; //--- Guardamos el indicador de propiedad de los lienzos if(::FileWriteInteger(file_handle,this.m_canvas_owner,INT_VALUE)!=INT_VALUE) return false; //--- Guardamos las dimensiones return this.m_bound.Save(file_handle); } //+------------------------------------------------------------------+ //| CBoundedObj::Carga desde un archivo | //+------------------------------------------------------------------+ bool CBoundedObj::Load(const int file_handle) { //--- Cargamos los datos del objeto padre if(!CBaseObj::Load(file_handle)) return false; //--- Cargamos el indicador de propiedad de los lienzos this.m_canvas_owner=::FileReadInteger(file_handle,INT_VALUE); //--- Cargamos las dimensiones return this.m_bound.Load(file_handle); }
La clase presentada simplemente hereda las propiedades de la clase base CBaseObj, permite especificar los límites del objeto (clase CBound) y tiene un indicador de propiedad de los lienzos.
La clase base del lienzo de los elementos gráficos ahora debe heredar de la nueva clase CBoundedObj, y no de la clase CBaseObj, como ocurría anteriormente. Todo lo relacionado con el manejo del objeto de la clase CBound para establecer y recuperar las dimensiones del elemento ya se ha trasladado a la nueva clase CBoundedObj y se ha eliminado de la clase CCanvasBase. Ahora, los objetos de los lienzos no se declaran como instancias de la clase, sino que se declaran como punteros a los objetos CCanvas que se van a crear:
//+------------------------------------------------------------------+ //| Clase base del lienzo de elementos gráficos | //+------------------------------------------------------------------+ class CCanvasBase : public CBoundedObj { private: bool m_chart_mouse_wheel_flag; // Indicador de envío de mensajes de desplazamiento de la rueda del ratón bool m_chart_mouse_move_flag; // Indicador de envío de mensajes de movimiento del cursor del ratón bool m_chart_object_create_flag; // Indicador de envío de mensajes sobre el evento de creación de un objeto gráfico bool m_chart_mouse_scroll_flag; // Indicador de desplazamiento del gráfico con el botón izquierdo y la rueda del ratón bool m_chart_context_menu_flag; // Indicador de acceso al menú contextual al pulsar el botón derecho del ratón bool m_chart_crosshair_tool_flag; // Indicador de acceso a la herramienta «Cruz» al pulsar el botón central del ratón bool m_flags_state; // Estado de los indicadores de desplazamiento del gráfico con la rueda del ratón, del menú contextual y de la Cruz //--- Configuración de restricciones para el gráfico (desplazamiento con la rueda del ratón, menú contextual y Cruz) void SetFlags(const bool flag); protected: CCanvas *m_background; // Lienzo para dibujar el fondo CCanvas *m_foreground; // Lienzo para dibujar el primer plano CCanvasBase *m_container; // Objeto contenedor padre CColorElement m_color_background; // Objeto de control del color de fondo CColorElement m_color_foreground; // Objeto de control del color de primer plano CColorElement m_color_border; // Objeto de control del color del borde
Los métodos para obtener las coordenadas y las dimensiones de los objetos gráficos de los lienzos se han hecho públicos, y los métodos para cambiar las dimensiones de los lienzos se han hecho virtuales:
//--- Obtención de las coordenadas, los límites y las dimensiones de un objeto gráfico public: int ObjectX(void) const { return this.m_obj_x; } int ObjectY(void) const { return this.m_obj_y; } int ObjectWidth(void) const { return this.m_background.Width(); } int ObjectHeight(void) const { return this.m_background.Height(); } int ObjectRight(void) const { return this.ObjectX()+this.ObjectWidth()-1; } int ObjectBottom(void) const { return this.ObjectY()+this.ObjectHeight()-1; } //--- Modifica (1) la anchura, (2) la altura y (3) el tamaño del objeto gráfico protected: virtual bool ObjectResizeW(const int size); virtual bool ObjectResizeH(const int size); bool ObjectResize(const int w,const int h); //--- Establece la coordenada (1) X, (2) Y y (3) ambas coordenadas de un objeto gráfico virtual bool ObjectSetX(const int x); virtual bool ObjectSetY(const int y); bool ObjectSetXY(const int x,const int y) { return(this.ObjectSetX(x) && this.ObjectSetY(y)); }
En la sección protegida de la clase se ha declarado un método para crear lienzos:
//--- Establece un puntero al objeto contenedor padre void SetContainerObj(CCanvasBase *obj); protected: //--- Crea lienzos de fondo y de primer plano bool CreateCanvasObjects(void); //--- Crea OBJ_BITMAP_LABEL bool Create(const long chart_id,const int wnd,const string object_name,const int x,const int y,const int w,const int h); public: //--- (1) Establece, (2) devuelve el estado void SetState(ENUM_ELEMENT_STATE state) { this.m_state=state; this.ColorsToDefault(); } ENUM_ELEMENT_STATE State(void) const { return this.m_state; }
El método que establece el indicador de permiso para recortar el elemento según los límites del contenedor se ha hecho virtual:
//--- Establece en el objeto el indicador de (1) movilidad, (2) objeto principal y (3) posibilidad de cambiar el tamaño //--- recorte según los límites del contenedor void SetMovable(const bool flag) { this.m_movable=flag; } void SetAsMain(void) { this.m_main=true; } virtual void SetResizable(const bool flag) { this.m_resizable=flag; } void SetAutorepeat(const bool flag) { this.m_autorepeat_flag=flag; } void SetScrollable(const bool flag) { this.m_scroll_flag=flag; } virtual void SetTrimmered(const bool flag) { this.m_trim_flag=flag; }
En el constructor de la clase ahora se invoca el método para crear los objetos de los lienzos de fondo y de primer plano:
//+------------------------------------------------------------------+ //| CCanvasBase::Constructor | //+------------------------------------------------------------------+ CCanvasBase::CCanvasBase(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)), m_wnd(wnd<0 ? 0 : wnd), m_alpha_bg(0), m_alpha_fg(255), m_hidden(false), m_blocked(false), m_focused(false), m_movable(false), m_resizable(false), m_main(false), m_autorepeat_flag(false), m_trim_flag(true), m_scroll_flag(false), m_border_width_lt(0), m_border_width_rt(0), m_border_width_up(0), m_border_width_dn(0), m_z_order(0), m_state(0), m_cursor_delta_x(0), m_cursor_delta_y(0) { //--- Obtenemos el identificador corregido del gráfico y la distancia en píxeles a lo largo del eje vertical Y //--- entre el borde superior de la subventana del indicador y el borde superior de la ventana principal del gráfico this.m_chart_id=this.CorrectChartID(chart_id); //--- Si no hemos podido crear los lienzos, salimos if(!this.CreateCanvasObjects()) return; //--- Si se han creado el recurso gráfico y el objeto gráfico if(this.Create(this.m_chart_id,this.m_wnd,object_name,x,y,w,h)) { //--- Limpiamos los lienzos de fondo y de primer plano y establecemos los valores iniciales de las coordenadas, //--- los nombres de los objetos gráficos y las propiedades del texto que se dibuja en primer plano this.Clear(false); this.m_obj_x=x; this.m_obj_y=y; this.m_color_background.SetName("Background"); this.m_color_foreground.SetName("Foreground"); this.m_color_border.SetName("Border"); this.m_foreground.FontSet(DEF_FONTNAME,-DEF_FONTSIZE*10,FW_MEDIUM); this.m_bound.SetName("Perimeter"); //--- Guardamos los permisos del ratón y de las herramientas del gráfico this.Init(); } }
Método para crear lienzos de fondo y de primer plano:
//+------------------------------------------------------------------+ //| CCanvasBase::Crea lienzos de fondo y de primer plano | //+------------------------------------------------------------------+ bool CCanvasBase::CreateCanvasObjects(void) { //--- Si ambos lienzos ya se han creado, o si la clase no gestiona los lienzos, devolvemos true if((this.m_background!=NULL && this.m_foreground!=NULL) || !this.m_canvas_owner) return true; //--- Creamos el lienzo de fondo this.m_background=new CCanvas(); if(this.m_background==NULL) { ::PrintFormat("%s: Error! Failed to create background canvas",__FUNCTION__); return false; } //--- Creamos el lienzo de primer plano this.m_foreground=new CCanvas(); if(this.m_foreground==NULL) { ::PrintFormat("%s: Error! Failed to create foreground canvas",__FUNCTION__); return false; } //--- Todo correcto return true; }
En el método que destruye el objeto, ahora comprobamos el indicador de control de los lienzos:
//+------------------------------------------------------------------+ //| CCanvasBase::Destruye el objeto | //+------------------------------------------------------------------+ void CCanvasBase::Destroy(void) { if(this.m_canvas_owner) { this.m_background.Destroy(); this.m_foreground.Destroy(); delete this.m_background; delete this.m_foreground; this.m_background=NULL; this.m_foreground=NULL; } }
Los lienzos solo se eliminan si el objeto gestiona dichos lienzos. Sin embargo, si al objeto se le pasan los lienzos de otro control para dibujar, no los eliminará: solo debe eliminarlos el objeto al que pertenecen.
En el método de inicialización predeterminada de la clase, para el objeto se establece el indicador de control de los lienzos:
//+------------------------------------------------------------------+ //| CCanvasBase::Inicialización de la clase | //+------------------------------------------------------------------+ void CCanvasBase::Init(void) { //--- Guardamos los permisos del ratón y de las herramientas del gráfico this.m_chart_mouse_wheel_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL); this.m_chart_mouse_move_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE); this.m_chart_object_create_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE); this.m_chart_mouse_scroll_flag = ::ChartGetInteger(this.m_chart_id, CHART_MOUSE_SCROLL); this.m_chart_context_menu_flag = ::ChartGetInteger(this.m_chart_id, CHART_CONTEXT_MENU); this.m_chart_crosshair_tool_flag= ::ChartGetInteger(this.m_chart_id, CHART_CROSSHAIR_TOOL); //--- Configuramos los permisos para el ratón y el gráfico ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL, true); ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE, true); ::ChartSetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE, true); //--- Inicializamos los colores predeterminados del objeto this.InitColors(); //--- Inicializamos un temporizador en milisegundos ::EventSetMillisecondTimer(16); //--- Indicador de propiedad de los lienzos this.m_canvas_owner=true; }
Solo para aquellos objetos que no tengan lienzos propios y que hereden de la clase CBoundedObj será necesario restablecer este indicador a false.
Ahora abriremos el archivo Controls.mqh y realizaremos en él las mejoras correspondientes.
En la sección de sustituciones de macro, añadiremos dos nuevas, que definen la altura predeterminada de la fila de la tabla y del encabezado:
//+------------------------------------------------------------------+ //| Definiciones de macros | //+------------------------------------------------------------------+ #define DEF_LABEL_W 50 // Anchura de la etiqueta de texto por defecto #define DEF_LABEL_H 16 // Altura de la etiqueta de texto por defecto #define DEF_BUTTON_W 60 // Anchura del botón por defecto #define DEF_BUTTON_H 16 // Altura del botón por defecto #define DEF_TABLE_ROW_H 16 // Altura de la línea por defecto #define DEF_TABLE_HEADER_H 20 // Altura del encabezado de la tabla por defecto #define DEF_PANEL_W 80 // Anchura del panel por defecto #define DEF_PANEL_H 80 // Altura del panel por defecto #define DEF_PANEL_MIN_W 60 // Anchura mínima del panel #define DEF_PANEL_MIN_H 60 // Altura mínima del panel #define DEF_SCROLLBAR_TH 13 // Grosor de la barra de desplazamiento por defecto #define DEF_THUMB_MIN_SIZE 8 // Grosor mínimo del deslizador #define DEF_AUTOREPEAT_DELAY 500 // Retardo antes del inicio de la auto-repetición #define DEF_AUTOREPEAT_INTERVAL 100 // Frecuencia de las auto-repeticiones
El archivo contiene una clase de lista enlazada de objetos, CListObj, destinada a almacenar una lista de elementos gráficos. Lo hemos copiado a este archivo desde el archivo de clases del modelo de la tabla. Si se deja el mismo nombre de clase, se producirá un conflicto de nombres, mientras que los objetos almacenados en estas dos listas, en archivos diferentes, son completamente distintos. Así que aquí dejaremos la clase tal cual, pero le cambiaremos el nombre. Será una clase de lista enlazada de elementos gráficos:
//+------------------------------------------------------------------+ //| Clase de lista enlazada de elementos gráficos | //+------------------------------------------------------------------+ class CListElm : public CList { protected: ENUM_ELEMENT_TYPE m_element_type; // Tipo del objeto que se crea en CreateElement() public: //--- Configuración del tipo de elemento void SetElementType(const ENUM_ELEMENT_TYPE type) { this.m_element_type=type; } //--- Método virtual para (1) cargar una lista desde un archivo y (2) crear un elemento de la lista virtual bool Load(const int file_handle); virtual CObject *CreateElement(void); };
Vamos a sustituir la cadena «CListObj» por la cadena «CListElm» en todo el archivo.
En el método para crear un elemento de la lista, añadiremos nuevos tipos de elementos gráficos:
//+------------------------------------------------------------------+ //| Método para crear un elemento de la lista | //+------------------------------------------------------------------+ CObject *CListElm::CreateElement(void) { //--- En función del tipo de objeto en m_element_type, creamos un nuevo objeto switch(this.m_element_type) { case ELEMENT_TYPE_BASE : return new CBaseObj(); // Objeto básico de los elementos gráficos case ELEMENT_TYPE_COLOR : return new CColor(); // Objeto de color case ELEMENT_TYPE_COLORS_ELEMENT : return new CColorElement(); // Objeto de colores de un elemento de objeto gráfico case ELEMENT_TYPE_RECTANGLE_AREA : return new CBound(); // Área rectangular del elemento case ELEMENT_TYPE_IMAGE_PAINTER : return new CImagePainter(); // Objeto para dibujar imágenes case ELEMENT_TYPE_CANVAS_BASE : return new CCanvasBase(); // Objeto básico del lienzo de los elementos gráficos case ELEMENT_TYPE_ELEMENT_BASE : return new CElementBase(); // Objeto básico de los elementos gráficos case ELEMENT_TYPE_HINT : return new CVisualHint(); // Ayuda emergente case ELEMENT_TYPE_LABEL : return new CLabel(); // Etiqueta de texto case ELEMENT_TYPE_BUTTON : return new CButton(); // Botón simple case ELEMENT_TYPE_BUTTON_TRIGGERED : return new CButtonTriggered(); // Botón de dos posiciones case ELEMENT_TYPE_BUTTON_ARROW_UP : return new CButtonArrowUp(); // Botón con flecha hacia arriba case ELEMENT_TYPE_BUTTON_ARROW_DOWN : return new CButtonArrowDown(); // Botón con flecha hacia abajo case ELEMENT_TYPE_BUTTON_ARROW_LEFT : return new CButtonArrowLeft(); // Botón con flecha hacia la izquierda case ELEMENT_TYPE_BUTTON_ARROW_RIGHT : return new CButtonArrowRight(); // Botón con flecha hacia la derecha case ELEMENT_TYPE_CHECKBOX : return new CCheckBox(); // Control CheckBox case ELEMENT_TYPE_RADIOBUTTON : return new CRadioButton(); // Control RadioButton case ELEMENT_TYPE_TABLE_CELL : return new CTableCellView(); // Celda de la tabla (Vista) case ELEMENT_TYPE_TABLE_ROW : return new CTableRowView(); // Fila de la tabla (Vista) case ELEMENT_TYPE_TABLE_COLUMN_CAPTION : return new CColumnCaptionView(); // Encabezado de columna de la tabla (Vista) case ELEMENT_TYPE_TABLE_HEADER : return new CTableHeaderView(); // Encabezado de la tabla (Vista) case ELEMENT_TYPE_TABLE : return new CTableView(); // Tabla (Vista) case ELEMENT_TYPE_PANEL : return new CPanel(); // Control Panel case ELEMENT_TYPE_GROUPBOX : return new CGroupBox(); // Control GroupBox case ELEMENT_TYPE_CONTAINER : return new CContainer(); // Control GroupBox default : return NULL; } }
En la clase base del elemento gráfico añadiremos un método que devuelve un puntero al objeto:
public: //--- Devuelve el propio objeto CElementBase *GetObject(void) { return &this; } //--- Devuelve un puntero a (1) una clase de dibujo y (2) una lista de ayudas emergentes CImagePainter *Painter(void) { return &this.m_painter; } CListElm *GetListHints(void) { return &this.m_list_hints; }
En la clase de la etiqueta de texto haremos virtual el método que muestra el texto en el lienzo:
//--- Establece la coordenada (1) X, (2) Y del texto void SetTextShiftH(const int x) { this.ClearText(); this.m_text_x=x; } void SetTextShiftV(const int y) { this.ClearText(); this.m_text_y=y; } //--- Muestra el texto virtual void DrawText(const int dx, const int dy, const string text, const bool chart_redraw); //--- Dibuja el aspecto externo virtual void Draw(const bool chart_redraw);
En la clase CPanel perfeccionaremos los métodos de cambio de tamaño:
//+------------------------------------------------------------------+ //| CPanel::Modifica el ancho del objeto | //+------------------------------------------------------------------+ bool CPanel::ResizeW(const int w) { if(!this.ObjectResizeW(w)) return false; this.BoundResizeW(w); this.SetImageSize(w,this.Height()); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } //--- Obtenemos un puntero al elemento base y, si existe y su tipo es contenedor, //--- comprobamos la relación entre las dimensiones del elemento actual y las dimensiones del contenedor //--- para mostrar las barras de desplazamiento en el contenedor cuando sea necesario CContainer *base=this.GetContainer(); if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER) base.CheckElementSizes(&this); //--- En el bucle que recorre los elementos adjuntos, recortamos cada elemento según los límites del contenedor int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.ObjectTrim(); } //--- Todo correcto return true; } //+------------------------------------------------------------------+ //| CPanel::Modifica la altura del objeto | //+------------------------------------------------------------------+ bool CPanel::ResizeH(const int h) { if(!this.ObjectResizeH(h)) return false; this.BoundResizeH(h); this.SetImageSize(this.Width(),h); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } //--- Obtenemos un puntero al elemento base y, si existe y su tipo es contenedor, //--- comprobamos la relación entre las dimensiones del elemento actual y las dimensiones del contenedor //--- para mostrar las barras de desplazamiento en el contenedor cuando sea necesario CContainer *base=this.GetContainer(); if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER) base.CheckElementSizes(&this); //--- En el bucle que recorre los elementos adjuntos, recortamos cada elemento según los límites del contenedor int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.ObjectTrim(); } //--- Todo correcto return true; } //+------------------------------------------------------------------+ //| CPanel::Modifica las dimensiones del objeto | //+------------------------------------------------------------------+ bool CPanel::Resize(const int w,const int h) { if(!this.ObjectResize(w,h)) return false; this.BoundResize(w,h); this.SetImageSize(w,h); if(!this.ObjectTrim()) { this.Update(false); this.Draw(false); } //--- Obtenemos un puntero al elemento base y, si existe y su tipo es contenedor, //--- comprobamos la relación entre las dimensiones del elemento actual y las dimensiones del contenedor //--- para mostrar las barras de desplazamiento en el contenedor cuando sea necesario CContainer *base=this.GetContainer(); if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER) base.CheckElementSizes(&this); //--- En el bucle que recorre los elementos adjuntos, recortamos cada elemento según los límites del contenedor int total=this.m_list_elm.Total(); for(int i=0;i<total;i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) elm.ObjectTrim(); } //--- Todo correcto return true; }
La esencia de la mejora de los métodos radica en que, si un elemento forma parte de un contenedor y se han modificado sus dimensiones, es necesario comprobar las nuevas dimensiones del elemento con respecto al contenedor. Si el elemento supera las dimensiones del contenedor, se recorta al tamaño de este (esto ya se había implementado anteriormente), pero, además, deben aparecer barras de desplazamiento en el contenedor. Esta comprobación, mediante el método del objeto contenedor CheckElementSizes(), es la que hemos añadido a los métodos de cambio de tamaño del elemento gráfico.
En el método que dibuja la apariencia de un objeto de panel en la clase CPanel, dibujamos el borde solo si al menos uno de los cuatro lados tiene un valor distinto de cero para el parámetro BorderWidth:
//+------------------------------------------------------------------+ //| CPanel::Dibuja la apariencia | //+------------------------------------------------------------------+ void CPanel::Draw(const bool chart_redraw) { //--- Rellenamos el objeto con el color de fondo this.Fill(this.BackColor(),false); //--- Limpiamos el área de dibujo this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false); //--- Establecemos el color de las líneas oscuras y claras y dibujamos el borde del panel color clr_dark =(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(),-20,-20,-20)); color clr_light=(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(), 6, 6, 6)); if(this.BorderWidthBottom()+this.BorderWidthLeft()+this.BorderWidthRight()+this.BorderWidthTop()!=0) this.m_painter.FrameGroupElements(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()), this.m_painter.Width(),this.m_painter.Height(),this.Text(), this.ForeColor(),clr_dark,clr_light,this.AlphaFG(),true); //--- Actualizamos el lienzo de fondo sin redibujar el gráfico this.m_background.Update(false); //--- Dibujamos los elementos de la lista for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL && elm.Type()!=ELEMENT_TYPE_SCROLLBAR_H && elm.Type()!=ELEMENT_TYPE_SCROLLBAR_V) elm.Draw(false); } //--- Si se indica, actualizamos el gráfico if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
En el método para crear y añadir un nuevo elemento a la lista en la clase del objeto de panel, añadiremos la creación de nuevos controles:
//+------------------------------------------------------------------+ //| CPanel::Crea y añade un nuevo elemento a la lista | //+------------------------------------------------------------------+ CElementBase *CPanel::InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h) { //--- Creamos el nombre de un objeto gráfico int elm_total=this.m_list_elm.Total(); string obj_name=this.NameFG()+"_"+ElementShortName(type)+(string)elm_total; //--- Calculamos las coordenadas int x=this.X()+dx; int y=this.Y()+dy; //--- Dependiendo del tipo de objeto, creamos un nuevo objeto CElementBase *element=NULL; switch(type) { case ELEMENT_TYPE_LABEL : element = new CLabel(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Etiqueta de texto case ELEMENT_TYPE_BUTTON : element = new CButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Botón simple case ELEMENT_TYPE_BUTTON_TRIGGERED : element = new CButtonTriggered(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Botón de dos posiciones case ELEMENT_TYPE_BUTTON_ARROW_UP : element = new CButtonArrowUp(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Botón con flecha hacia arriba case ELEMENT_TYPE_BUTTON_ARROW_DOWN : element = new CButtonArrowDown(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Botón con flecha hacia abajo case ELEMENT_TYPE_BUTTON_ARROW_LEFT : element = new CButtonArrowLeft(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Botón con flecha hacia la izquierda case ELEMENT_TYPE_BUTTON_ARROW_RIGHT : element = new CButtonArrowRight(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Botón con flecha hacia la derecha case ELEMENT_TYPE_CHECKBOX : element = new CCheckBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Control CheckBox case ELEMENT_TYPE_RADIOBUTTON : element = new CRadioButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Control RadioButton case ELEMENT_TYPE_SCROLLBAR_THUMB_H : element = new CScrollBarThumbH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Barra de desplazamiento del ScrollBar horizontal case ELEMENT_TYPE_SCROLLBAR_THUMB_V : element = new CScrollBarThumbV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Barra de desplazamiento del ScrollBar vertical case ELEMENT_TYPE_SCROLLBAR_H : element = new CScrollBarH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Control ScrollBar horizontal case ELEMENT_TYPE_SCROLLBAR_V : element = new CScrollBarV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Control ScrollBar vertical case ELEMENT_TYPE_TABLE_ROW : element = new CTableRowView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Objeto de representación visual de una fila de la tabla case ELEMENT_TYPE_TABLE_COLUMN_CAPTION : element = new CColumnCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Objeto de representación visual del encabezado de una columna de la tabla case ELEMENT_TYPE_TABLE_HEADER : element = new CTableHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Objeto de representación visual del encabezado de la tabla case ELEMENT_TYPE_TABLE : element = new CTableView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Objeto de representación visual de la tabla case ELEMENT_TYPE_PANEL : element = new CPanel(obj_name,"",this.m_chart_id,this.m_wnd,x,y,w,h); break; // Control Panel case ELEMENT_TYPE_GROUPBOX : element = new CGroupBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Control GroupBox case ELEMENT_TYPE_CONTAINER : element = new CContainer(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break; // Control Container default : element = NULL; } //--- Si no se ha creado el nuevo elemento, lo indicamos y devolvemos NULL if(element==NULL) { ::PrintFormat("%s: Error. Failed to create graphic element %s",__FUNCTION__,ElementDescription(type)); return NULL; } //--- Establecemos el identificador, el nombre, el contenedor y el orden de superposición (z-order) del elemento element.SetID(elm_total); element.SetName(user_name); element.SetContainerObj(&this); element.ObjectSetZOrder(this.ObjectZOrder()+1); //--- Si el elemento creado no se ha añadido a la lista, lo notificamos, eliminamos el elemento creado y devolvemos NULL if(!this.AddNewElement(element)) { ::PrintFormat("%s: Error. Failed to add %s element with ID %d to list",__FUNCTION__,ElementDescription(type),element.ID()); delete element; return NULL; } //--- Obtenemos el elemento padre al que están vinculados los elementos hijos CElementBase *elm=this.GetContainer(); //--- Si el elemento padre es de tipo «Contenedor», significa que tiene barras de desplazamiento if(elm!=NULL && elm.Type()==ELEMENT_TYPE_CONTAINER) { //--- Convertimos CElementBase a CContainer CContainer *container_obj=elm; //--- Si la barra de desplazamiento horizontal está visible, if(container_obj.ScrollBarHorzIsVisible()) { //--- obtenemos un puntero a la barra de desplazamiento horizontal y la llevamos al primer plano CScrollBarH *sbh=container_obj.GetScrollBarH(); if(sbh!=NULL) sbh.BringToTop(false); } //--- Si la barra de desplazamiento vertical está visible, if(container_obj.ScrollBarVertIsVisible()) { //--- obtenemos un puntero a la barra de desplazamiento vertical y la llevamos al primer plano CScrollBarV *sbv=container_obj.GetScrollBarV(); if(sbv!=NULL) sbv.BringToTop(false); } } //--- Devolvemos un puntero al elemento creado y adjunto return element; }
En el método que coloca el objeto en primer plano, añadiremos el recorte de los objetos adjuntos a las dimensiones del contenedor:
//+------------------------------------------------------------------+ //| CPanel::Coloca el objeto en primer plano | //+------------------------------------------------------------------+ void CPanel::BringToTop(const bool chart_redraw) { //--- Colocamos el panel en primer plano CCanvasBase::BringToTop(false); //--- Colocamos en primer plano los objetos adjuntos for(int i=0;i<this.m_list_elm.Total();i++) { CElementBase *elm=this.GetAttachedElementAt(i); if(elm!=NULL) { if(elm.Type()==ELEMENT_TYPE_SCROLLBAR_H || elm.Type()==ELEMENT_TYPE_SCROLLBAR_V) continue; elm.BringToTop(false); elm.ObjectTrim(); } } //--- Si así se indica, redibujamos el gráfico if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
En la clase de la barra de desplazamiento horizontal hay un indicador que determina el permiso para recortar el elemento según los límites del contenedor. El objeto de la barra de desplazamiento se compone de varios objetos, y hay que establecer el mismo valor del indicador para cada uno de ellos. Lo haremos todo en un único método. Redefiniremos en la clase el método que establece el indicador de recorte según los límites del contenedor:
//+------------------------------------------------------------------+ //| Clase de la barra de desplazamiento horizontal | //+------------------------------------------------------------------+ class CScrollBarH : public CPanel { protected: CButtonArrowLeft *m_butt_left; // Botón con flecha hacia la izquierda CButtonArrowRight*m_butt_right; // Botón con flecha hacia la derecha CScrollBarThumbH *m_thumb; // Control deslizante de la barra de desplazamiento public: //--- Devuelve un puntero al (1) botón izquierdo, al (2) botón derecho y al (3) control deslizante CButtonArrowLeft *GetButtonLeft(void) { return this.m_butt_left; } CButtonArrowRight*GetButtonRight(void) { return this.m_butt_right; } CScrollBarThumbH *GetThumb(void) { return this.m_thumb; } //--- (1) Establece, (2) devuelve el indicador de actualización del gráfico void SetChartRedrawFlag(const bool flag) { if(this.m_thumb!=NULL) this.m_thumb.SetChartRedrawFlag(flag); } bool ChartRedrawFlag(void) const { return(this.m_thumb!=NULL ? this.m_thumb.ChartRedrawFlag() : false); } //--- Devuelve (1) la longitud, (2) el inicio de la pista y (3) la posición del control deslizante int TrackLength(void) const; int TrackBegin(void) const; int ThumbPosition(void) const; //--- Establece la posición del control deslizante bool SetThumbPosition(const int pos) const { return(this.m_thumb!=NULL ? this.m_thumb.MoveX(pos) : false); } //--- Cambia el tamaño del control deslizante bool SetThumbSize(const uint size) const { return(this.m_thumb!=NULL ? this.m_thumb.ResizeW(size) : false); } //--- Modifica la anchura del objeto virtual bool ResizeW(const int size); //--- Establece el indicador de visibilidad en el contenedor virtual void SetVisibleInContainer(const bool flag); //--- Establece el indicador de recorte por los límites del contenedor virtual void SetTrimmered(const bool flag); //--- Dibuja el aspecto externo virtual void Draw(const bool chart_redraw); //--- Tipo de objeto virtual int Type(void) const { return(ELEMENT_TYPE_SCROLLBAR_H); } //--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto void Init(void); virtual void InitColors(void); //--- Controlador de desplazamiento de la rueda (Wheel) virtual void OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam); //--- Constructores/destructor CScrollBarH(void); CScrollBarH(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CScrollBarH(void) {} };
Implementación del método:
//+------------------------------------------------------------------+ //| CScrollBarH::Establece el indicador de recorte por los límites del contenedor | //+------------------------------------------------------------------+ void CScrollBarH::SetTrimmered(const bool flag) { this.m_trim_flag=flag; if(this.m_butt_left!=NULL) this.m_butt_left.SetTrimmered(flag); if(this.m_butt_right!=NULL) this.m_butt_right.SetTrimmered(flag); if(this.m_thumb!=NULL) this.m_thumb.SetTrimmered(flag); }
Para cada uno de los objetos que componen la barra de desplazamiento, se establece el indicador pasado al método.
En el método de inicialización del objeto, ya no es necesario establecer el indicador de recorte por los límites del contenedor para cada uno de los elementos, sino que basta con llamar al método SetTrimmered() al final del método:
//+------------------------------------------------------------------+ //| CScrollBarH::Inicialización | //+------------------------------------------------------------------+ void CScrollBarH::Init(void) { //--- Inicialización de la clase padre CPanel::Init(); //--- Fondo: opaco this.SetAlphaBG(255); //--- Ancho del borde y texto this.SetBorderWidth(0); this.SetText(""); //--- Creamos los botones de desplazamiento int w=this.Height(); int h=this.Height(); this.m_butt_left = this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_LEFT, "","ButtL",0,0,w,h); this.m_butt_right= this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_RIGHT,"","ButtR",this.Width()-w,0,w,h); if(this.m_butt_left==NULL || this.m_butt_right==NULL) { ::PrintFormat("%s: Init failed",__FUNCTION__); return; } //--- Configuramos los colores y el aspecto del botón con la flecha hacia la izquierda this.m_butt_left.SetImageBound(1,1,w-2,h-4); this.m_butt_left.InitBackColors(this.m_butt_left.BackColorFocused()); this.m_butt_left.ColorsToDefault(); this.m_butt_left.InitBorderColors(this.BorderColor(),this.m_butt_left.BackColorFocused(),this.m_butt_left.BackColorPressed(),this.m_butt_left.BackColorBlocked()); this.m_butt_left.ColorsToDefault(); //--- Configuramos los colores y el aspecto del botón con la flecha hacia la derecha this.m_butt_right.SetImageBound(1,1,w-2,h-4); this.m_butt_right.InitBackColors(this.m_butt_right.BackColorFocused()); this.m_butt_right.ColorsToDefault(); this.m_butt_right.InitBorderColors(this.BorderColor(),this.m_butt_right.BackColorFocused(),this.m_butt_right.BackColorPressed(),this.m_butt_right.BackColorBlocked()); this.m_butt_right.ColorsToDefault(); //--- Creamos un control deslizante int tsz=this.Width()-w*2; this.m_thumb=this.InsertNewElement(ELEMENT_TYPE_SCROLLBAR_THUMB_H,"","ThumbH",w,1,tsz-w*4,h-2); if(this.m_thumb==NULL) { ::PrintFormat("%s: Init failed",__FUNCTION__); return; } //--- Configuramos los colores del control deslizante y le establecemos el indicador de movilidad this.m_thumb.InitBackColors(this.m_thumb.BackColorFocused()); this.m_thumb.ColorsToDefault(); this.m_thumb.InitBorderColors(this.m_thumb.BackColor(),this.m_thumb.BackColorFocused(),this.m_thumb.BackColorPressed(),this.m_thumb.BackColorBlocked()); this.m_thumb.ColorsToDefault(); this.m_thumb.SetMovable(true); //--- Prohibimos que el gráfico se redibuje de forma independiente this.m_thumb.SetChartRedrawFlag(false); //--- Inicialmente, no se muestra en el contenedor ni se recorta por sus límites this.SetVisibleInContainer(false); this.SetTrimmered(false); }
Se han realizado exactamente las mismas modificaciones en la clase de la barra de desplazamiento vertical. Son totalmente idénticas a las que ya hemos analizado, así que no vamos a repetirlas.
En la clase del objeto contenedor, haremos que el método CheckElementSizes sea público:
//+------------------------------------------------------------------+ //| Clase Contenedor | //+------------------------------------------------------------------+ class CContainer : public CPanel { private: bool m_visible_scrollbar_h; // Indicador de visibilidad de la barra de desplazamiento horizontal bool m_visible_scrollbar_v; // Indicador de visibilidad de la barra de desplazamiento vertical int m_init_border_size_top; // Tamaño inicial del borde superior int m_init_border_size_bottom; // Tamaño inicial del borde inferior int m_init_border_size_left; // Tamaño inicial del borde izquierdo int m_init_border_size_right; // Tamaño inicial del borde derecho //--- Devuelve el tipo del elemento que ha enviado el evento ENUM_ELEMENT_TYPE GetEventElementType(const string name); protected: CScrollBarH *m_scrollbar_h; // Puntero a la barra de desplazamiento horizontal CScrollBarV *m_scrollbar_v; // Puntero a la barra de desplazamiento vertical //--- Procesador de arrastre de los bordes y las esquinas de un elemento virtual void ResizeActionDragHandler(const int x, const int y); public: //--- Comprueba las dimensiones del elemento para mostrar las barras de desplazamiento void CheckElementSizes(CElementBase *element); protected: //--- Calcula y devuelve el tamaño (1) del control deslizante, (2) el tamaño total y (3) el tamaño útil de la pista de la barra de desplazamiento horizontal int ThumbSizeHorz(void); int TrackLengthHorz(void) const { return(this.m_scrollbar_h!=NULL ? this.m_scrollbar_h.TrackLength() : 0); } int TrackEffectiveLengthHorz(void) { return(this.TrackLengthHorz()-this.ThumbSizeHorz()); }
Este método se invoca desde los elementos adjuntos al contenedor y, para poder acceder a él, debe ser público.
Ya hemos analizado las principales modificaciones de las clases de la biblioteca. Quedaron fuera del alcance algunas modificaciones y correcciones menores. Los códigos completos se pueden consultar en los archivos adjuntos al artículo.
El modelo de tabla se compone de una celda de la tabla, una fila de la tabla y una lista de filas que, en conjunto, forman la tabla. Además de lo mencionado, la tabla cuenta también con un encabezado formado por los encabezados de las columnas de la tabla.
- Una celda de la tabla es un elemento independiente que contiene el número de fila, el número de columna y el contenido;
- Una fila de la tabla es una lista de celdas de la tabla y tiene su propio número;
- Una tabla es una lista de filas que puede incluir un encabezado, que a su vez consiste en una lista de encabezados de columnas.
El componente Vista para mostrar los datos del modelo de tabla tendrá aproximadamente la misma estructura. El objeto de la tabla se construirá, por ejemplo, a partir de un array de datos y un array de encabezados de dichos datos. A continuación, al objeto creado de representación visual de la tabla (componente Vista) le pasaremos un puntero al modelo de la tabla (componente Modelo), y todos los datos se escribirán en las celdas de la tabla.
En esta fase se creará una tabla estática sencilla sin posibilidad de controlar su visualización con el cursor del ratón.
Clase de celda de la tabla (Vista)
La tabla está formada por una lista de filas. Las filas, a su vez, están formadas por objetos que cuentan con dos lienzos para dibujar el fondo y el primer plano. Las celdas de la tabla se disponen horizontalmente en sus respectivas filas. No tiene sentido dotar a las celdas de sus propios lienzos. Se puede simplemente «dividir» cada fila en áreas, de modo que cada área se corresponda con la ubicación de su celda. Y dibujar esas celdas en el lienzo del objeto de la fila de la tabla.
Precisamente con este fin, al principio creamos una clase intermedia que solo contiene el tamaño del objeto con sus coordenadas y un puntero al control en cuyo lienzo se realizará el dibujo. Y ese objeto será una celda de la tabla. A ella le pasaremos un puntero al objeto de la fila de la tabla para dibujar en sus lienzos.
En el archivo Controls.mqh, crearemos una nueva clase:
//+------------------------------------------------------------------+ //| Clase de representación visual de una celda de la tabla | //+------------------------------------------------------------------+ class CTableCellView : public CBoundedObj { protected: CTableCell *m_table_cell_model; // Puntero al modelo de celda CImagePainter *m_painter; // Puntero al objeto de dibujo CTableRowView *m_element_base; // Puntero al elemento base (fila de la tabla) CCanvas *m_background; // Puntero al lienzo de fondo CCanvas *m_foreground; // Puntero al lienzo de primer plano int m_index; // Índice en la lista de celdas ENUM_ANCHOR_POINT m_text_anchor; // Punto de anclaje del texto (alineación en la celda) int m_text_x; // Coordenada X del texto (desplazamiento respecto al borde izquierdo del área del objeto) int m_text_y; // Coordenada Y del texto (desplazamiento respecto al borde superior del área del objeto) ushort m_text[]; // Texto //--- Devuelve los desplazamientos de las coordenadas iniciales de dibujo en el lienzo con respecto al lienzo y a las coordenadas del elemento base int CanvasOffsetX(void) const { return(this.m_element_base.ObjectX()-this.m_element_base.X()); } int CanvasOffsetY(void) const { return(this.m_element_base.ObjectY()-this.m_element_base.Y()); } //--- Devuelve la coordenada ajustada de un punto en el lienzo, teniendo en cuenta el desplazamiento del lienzo con respecto al elemento base int AdjX(const int x) const { return(x-this.CanvasOffsetX()); } int AdjY(const int y) const { return(y-this.CanvasOffsetY()); } //--- Devuelve las coordenadas X e Y del texto en función del punto de anclaje bool GetTextCoordsByAnchor(int &x, int&y); public: //--- (1) Establece, (2) devuelve el texto de la celda void SetText(const string text) { ::StringToShortArray(text,this.m_text); } string Text(void) const { return ::ShortArrayToString(this.m_text); } //--- Establece el identificador virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Establece, (2) devuelve el índice de la celda void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Establece, (2) devuelve el desplazamiento del texto en el eje X void SetTextShiftX(const int shift) { this.m_text_x=shift; } int TextShiftX(void) const { return this.m_text_x; } //--- (1) Establece, (2) devuelve el desplazamiento del texto en el eje Y void SetTextShiftY(const int shift) { this.m_text_y=shift; } int TextShiftY(void) const { return this.m_text_y; } //--- (1) Establece, (2) devuelve el punto de anclaje del texto void SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw); int TextAnchor(void) const { return this.m_text_anchor; } //--- Establece el punto de anclaje y los desplazamientos del texto void SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw); //--- Asigna el elemento base (fila de la tabla) void RowAssign(CTableRowView *base_element); //--- (1) Asigna, (2) devuelve el modelo de celda bool TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h); CTableCell *GetTableCellModel(void) { return this.m_table_cell_model; } //--- Imprime en el registro el modelo de celda asignado void TableCellModelPrint(void); //--- (1) Rellena el objeto con el color de fondo, (2) actualiza el objeto para mostrar los cambios, (3) dibuja la apariencia virtual void Clear(const bool chart_redraw); virtual void Update(const bool chart_redraw); virtual void Draw(const bool chart_redraw); //--- Muestra el texto virtual void DrawText(const int dx, const int dy, const string text, const bool chart_redraw); //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto virtual int Compare(const CObject *node,const int mode=0)const { return CBaseObj::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_CELL); } //--- Inicialización del objeto de la clase void Init(const string text); //--- Devuelve la descripción del objeto virtual string Description(void); //--- Constructores/destructor CTableCellView(void); CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h); ~CTableCellView (void){} };
Todas las variables y métodos están comentados en el código. El índice de la celda y su identificador son lo mismo en este caso. Por eso se ha redefinido el método virtual SetID(), en el que se asignan los mismos valores al índice y al identificador. Veamos la implementación de los métodos declarados.
En los constructores de la clase se invoca el método de inicialización del objeto y se establecen el identificador de la celda y su nombre:
//+------------------------------------------------------------------+ //| CTableCellView::Constructor por defecto. Construye un objeto en la ventana principal| //| del gráfico actual, en las coordenadas 0,0 y con las dimensiones predeterminadas | //+------------------------------------------------------------------+ CTableCellView::CTableCellView(void) : CBoundedObj("TableCell",-1,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1),m_text_anchor(ANCHOR_LEFT) { //--- Inicialización this.Init(""); this.SetID(-1); this.SetName("TableCell"); } //+------------------------------------------------------------------+ //| CTableCellView::Constructor paramétrico. Construye un objeto | //| en la ventana indicada del gráfico indicado, con el texto indicado, | //| las coordenadas y las dimensiones | //+------------------------------------------------------------------+ CTableCellView::CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h) : CBoundedObj(user_name,id,x,y,w,h), m_index(-1),m_text_anchor(ANCHOR_LEFT) { //--- Inicialización this.Init(text); this.SetID(id); this.SetName(user_name); }
Método de inicialización del objeto de la clase:
//+------------------------------------------------------------------+ //| CTableCellView::Inicialización | //+------------------------------------------------------------------+ void CTableCellView::Init(const string text) { //--- La clase no gestiona los lienzos this.m_canvas_owner=false; //--- Texto de la celda this.SetText(text); //--- Desplazamientos predeterminados del texto this.m_text_x=2; this.m_text_y=0; }
Es imprescindible establecer en el objeto el indicador de que no gestiona los lienzos para que, al destruirse el objeto, no elimine lienzos ajenos.
Método que devuelve la descripción de un objeto:
//+------------------------------------------------------------------+ //| CTableCellView::Devuelve la descripción del objeto | //+------------------------------------------------------------------+ string CTableCellView::Description(void) { string nm=this.Name(); string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm); return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height()); }
El método muestra en el registro una línea con el nombre del tipo de objeto, el identificador, las coordenadas y las dimensiones de la celda.
Método que asigna a una celda una fila de la tabla y los lienzos de fondo y de primer plano:
//+------------------------------------------------------------------+ //| CTableCellView::Asigna fila y lienzos de fondo y de primer plano | //+------------------------------------------------------------------+ void CTableCellView::RowAssign(CTableRowView *base_element) { if(base_element==NULL) { ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__); return; } this.m_element_base=base_element; this.m_background=this.m_element_base.GetBackground(); this.m_foreground=this.m_element_base.GetForeground(); this.m_painter=this.m_element_base.Painter(); }
Al método se le pasa un puntero a un objeto de fila de la tabla (la descripción de la clase aparecerá más adelante) y, si el puntero no es válido, se informa de ello y el método finaliza. A continuación, se guardan en las variables de la clase el puntero a la fila de la tabla y, desde el objeto de la fila de la tabla, los punteros a los lienzos de fondo y de primer plano, así como al objeto de dibujo.
El objeto del modelo de tabla también tiene sus propias filas y celdas. A esta clase se le asigna un modelo de celda de la tabla, y esta dibuja en los lienzos del objeto de la fila los datos del modelo de celda.
Método que asigna un modelo de celda:
//+------------------------------------------------------------------+ //| CTableCellView::Asigna el modelo de celda | //+------------------------------------------------------------------+ bool CTableCellView::TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h) { //--- Si se pasa un objeto del modelo de celda no válido, se informa de ello y se devuelve false if(cell_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Si el elemento base (fila de la tabla) no está asignado, se informa de ello y se devuelve false if(this.m_element_base==NULL) { ::PrintFormat("%s: Error. Base element not assigned. Please use RowAssign() method first",__FUNCTION__); return false; } //--- Guardamos el modelo de celda this.m_table_cell_model=cell_model; //--- Establecemos las coordenadas y las dimensiones de la representación visual de la celda this.BoundSetXY(dx,dy); this.BoundResize(w,h); //--- Establecemos las dimensiones del área de dibujo de la representación visual de la celda this.m_painter.SetBound(dx,dy,w,h); //--- Todo correcto return true; }
En primer lugar, se comprueba la validez del puntero pasado a un modelo de celda; a continuación, se comprueba que la fila de la tabla ya esté asignada a este objeto de celda. Si alguna de estas condiciones no se cumple, se informa de ello en el registro y se devuelve false. A continuación, el puntero al modelo de celda se guarda en una variable y se establecen las coordenadas y las dimensiones del área de la celda en la fila que le corresponde.
Al ordenar las filas y columnas de la tabla, simplemente se reasignará el puntero al objeto de la celda, y este objeto, que tiene sus coordenadas exactas en la tabla, dibujará los datos de la nueva celda que se le haya asignado.
Método que devuelve las coordenadas X e Y del texto en función del punto de anclaje:
//+------------------------------------------------------------------+ //| CTableCellView::Devuelve las coordenadas X e Y del texto | //| en función del punto de anclaje | //+------------------------------------------------------------------+ bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y) { //--- Obtenemos las dimensiones del texto en la celda int text_w=0, text_h=0; this.m_foreground.TextSize(this.Text(),text_w,text_h); if(text_w==0 || text_h==0) return false; //--- Dependiendo del punto de anclaje del texto en la celda //--- calculamos sus coordenadas iniciales (esquina superior izquierda) switch(this.m_text_anchor) { //--- Punto de anclaje a la izquierda, centrado case ANCHOR_LEFT : x=0; y=(this.Height()-text_h)/2; break; //--- Punto de anclaje en la esquina inferior izquierda case ANCHOR_LEFT_LOWER : x=0; y=this.Height()-text_h; break; //--- Punto de anclaje abajo, centrado case ANCHOR_LOWER : x=(this.Width()-text_w)/2; y=this.Height()-text_h; break; //--- Punto de anclaje en la esquina inferior derecha case ANCHOR_RIGHT_LOWER : x=this.Width()-text_w; y=this.Height()-text_h; break; //--- Punto de anclaje a la derecha, centrado case ANCHOR_RIGHT : x=this.Width()-text_w; y=(this.Height()-text_h)/2; break; //--- Punto de anclaje en la esquina superior derecha case ANCHOR_RIGHT_UPPER : x=this.Width()-text_w; y=0; break; //--- Punto de anclaje arriba, centrado case ANCHOR_UPPER : x=(this.Width()-text_w)/2; y=0; break; //--- Punto de anclaje estrictamente en el centro del objeto case ANCHOR_CENTER : x=(this.Width()-text_w)/2; y=(this.Height()-text_h)/2; break; //--- Punto de anclaje en la esquina superior izquierda //---ANCHOR_LEFT_UPPER default: x=0; y=0; break; } return true; }
Al método se le pasan por referencia las variables en las que se almacenarán las coordenadas calculadas de la esquina superior izquierda del texto que se va a mostrar. Si no se pueden obtener las dimensiones del texto de la celda, el método devuelve false.
Tras calcular las coordenadas de la esquina superior izquierda, las coordenadas calculadas del texto se escriben en las variables y se devuelve true.
Método que establece el punto de anclaje del texto:
//+------------------------------------------------------------------+ //| CTableCellView::Establece el punto de anclaje del texto | //+------------------------------------------------------------------+ void CTableCellView::SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw) { if(this.m_text_anchor==anchor) return; this.m_text_anchor=anchor; if(cell_redraw) this.Draw(chart_redraw); }
En primer lugar, se asigna el nuevo punto de anclaje a la variable; a continuación, si está activado el indicador de redibujado de esta celda, se invoca su método de dibujo con el indicador especificado de redibujado del gráfico.
Método que establece el punto de anclaje y los desplazamientos del texto:
//+------------------------------------------------------------------+ //| CTableCellView::Establece el punto de anclaje y los desplazamientos textuales | //+------------------------------------------------------------------+ void CTableCellView::SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw) { this.SetTextShiftX(shift_x); this.SetTextShiftY(shift_y); this.SetTextAnchor(anchor,cell_redraw,chart_redraw); }
En este método, además de establecer el punto de anclaje, se definen los desplazamientos iniciales del texto en los ejes X e Y.
Método que rellena un objeto con un color:
//+------------------------------------------------------------------+ //| CTableCellView::Rellena el objeto con un color | //+------------------------------------------------------------------+ void CTableCellView::Clear(const bool chart_redraw) { //--- Establecemos las coordenadas correctas de las esquinas de la celda int x1=this.AdjX(this.m_bound.X()); int y1=this.AdjY(this.m_bound.Y()); int x2=this.AdjX(this.m_bound.Right()); int y2=this.AdjY(this.m_bound.Bottom()); //--- Borramos el fondo y el primer plano dentro del área rectangular que ocupa la celda if(this.m_background!=NULL) this.m_background.FillRectangle(x1,y1,x2,y2-1,::ColorToARGB(this.m_element_base.BackColor(),this.m_element_base.AlphaBG())); if(this.m_foreground!=NULL) this.m_foreground.FillRectangle(x1,y1,x2,y2-1,clrNULL); }
Obtenemos las coordenadas correctas de las cuatro esquinas del área rectangular y, a continuación, rellenamos el fondo con el color de fondo de la fila de la tabla, y el primer plano con un color transparente.
Método que actualiza el objeto para mostrar los cambios:
//+------------------------------------------------------------------+ //| CTableCellView::Actualiza el objeto para mostrar los cambios | //+------------------------------------------------------------------+ void CTableCellView::Update(const bool chart_redraw) { if(this.m_background!=NULL) this.m_background.Update(false); if(this.m_foreground!=NULL) this.m_foreground.Update(chart_redraw); }
Si los lienzos de fondo y de primer plano son válidos, se invocan sus métodos de actualización con el indicador especificado de redibujado del gráfico.
Método que dibuja el aspecto de la celda:
//+------------------------------------------------------------------+ //| CTableCellView::Dibuja el aspecto | //+------------------------------------------------------------------+ void CTableCellView::Draw(const bool chart_redraw) { //--- Obtenemos las coordenadas del texto en función del punto de anclaje int text_x=0, text_y=0; if(!this.GetTextCoordsByAnchor(text_x,text_y)) return; //--- Corregimos las coordenadas del texto int x=this.AdjX(this.X()+text_x); int y=this.AdjY(this.Y()+text_y); //--- Establecemos las coordenadas de la línea divisoria int x1=this.AdjX(this.X()); int x2=this.AdjX(this.X()); int y1=this.AdjY(this.Y()); int y2=this.AdjY(this.Bottom()); //--- Mostramos el texto en el lienzo de primer plano sin actualizar el gráfico this.DrawText(x+this.m_text_x,y+this.m_text_y,this.Text(),false); //--- Si no se trata de la celda situada en el extremo derecho, dibujamos una línea divisoria vertical en el lado derecho de la celda. if(this.m_element_base!=NULL && this.Index()<this.m_element_base.CellsTotal()-1) { int line_x=this.AdjX(this.Right()); this.m_background.Line(line_x,y1,line_x,y2,::ColorToARGB(this.m_element_base.BorderColor(),this.m_element_base.AlphaBG())); } //--- Actualizamos el lienzo de fondo con el indicador especificado de redibujado del gráfico this.m_background.Update(chart_redraw); }
La lógica del método se explica en los comentarios. La corrección de las coordenadas es necesaria cuando el objeto queda recortado por los límites del contenedor; en este caso, las coordenadas iniciales no se calculan a partir de las coordenadas reales del objeto gráfico del lienzo, sino a partir de las coordenadas del elemento gráfico, que se sitúan virtualmente fuera de los límites del contenedor.
Las líneas divisorias se dibujan únicamente en el lado derecho de la celda y, cuando no se trata de la celda situada en el extremo derecho, esta queda delimitada por su contenedor, por lo que no conviene dibujar allí una línea adicional.
Método que muestra el texto de una celda:
//+------------------------------------------------------------------+ //| CTableCellView::Muestra el texto | //+------------------------------------------------------------------+ void CTableCellView::DrawText(const int dx,const int dy,const string text,const bool chart_redraw) { //--- Comprobamos el elemento base if(this.m_element_base==NULL) return; //--- Limpiamos la celda y establecemos el texto this.Clear(false); this.SetText(text); //--- Mostramos el texto establecido en el lienzo de primer plano this.m_foreground.TextOut(dx,dy,this.Text(),::ColorToARGB(this.m_element_base.ForeColor(),this.m_element_base.AlphaFG())); //--- Si el texto sobresale por el límite derecho del área de la celda if(this.Right()-dx<this.m_foreground.TextWidth(text)) { //--- Obtenemos las dimensiones del texto «puntos suspensivos» int w=0,h=0; this.m_foreground.TextSize("... ",w,h); if(w>0 && h>0) { //--- Borramos el texto junto al límite derecho del objeto según el tamaño del texto «puntos suspensivos» y sustituimos por puntos suspensivos el final del texto de la etiqueta this.m_foreground.FillRectangle(this.AdjX(this.Right())-w,this.AdjY(this.Y()),this.AdjX(this.Right()),this.AdjY(this.Y())+h,clrNULL); this.m_foreground.TextOut(this.AdjX(this.Right())-w,this.AdjY(dy),"...",::ColorToARGB(this.m_element_base.ForeColor(),this.m_element_base.AlphaFG())); } } //--- Actualizamos el lienzo de primer plano con el indicador especificado de redibujado del gráfico this.m_foreground.Update(chart_redraw); }
La lógica del método se explica detalladamente en los comentarios del código. En caso de que el texto mostrado sobrepase los límites del área de la celda, se recorta por los límites del área y su final se sustituye por puntos suspensivos («...»), lo que indica que no todo el texto cabe en el ancho de la celda.
Método que imprime en el registro el modelo de fila asignado:
//+------------------------------------------------------------------+ //| CTableCellView::Imprime en el registro el modelo de fila asignado| //+------------------------------------------------------------------+ void CTableCellView::TableCellModelPrint(void) { if(this.m_table_cell_model!=NULL) this.m_table_cell_model.Print(); }
Este método permite comprobar qué celda del componente Modelo está asignada a la celda del componente Vista.
Métodos para trabajar con archivos:
//+------------------------------------------------------------------+ //| CTableCellView::Guardado en un archivo | //+------------------------------------------------------------------+ bool CTableCellView::Save(const int file_handle) { //--- Guardamos los datos del objeto padre if(!CBaseObj::Save(file_handle)) return false; //--- Guardamos el número de celda if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE) return false; //--- Guardamos el punto de anclaje del texto if(::FileWriteInteger(file_handle,this.m_text_anchor,INT_VALUE)!=INT_VALUE) return false; //--- Guardamos la coordenada X del texto if(::FileWriteInteger(file_handle,this.m_text_x,INT_VALUE)!=INT_VALUE) return false; //--- Guardamos la coordenada Y del texto if(::FileWriteInteger(file_handle,this.m_text_y,INT_VALUE)!=INT_VALUE) return false; //--- Guardamos el texto if(::FileWriteArray(file_handle,this.m_text)!=sizeof(this.m_text)) return false; //--- Todo correcto return true; } //+------------------------------------------------------------------+ //| CTableCellView::Carga desde un archivo | //+------------------------------------------------------------------+ bool CTableCellView::Load(const int file_handle) { //--- Cargamos los datos del objeto padre if(!CBaseObj::Load(file_handle)) return false; //--- Cargamos el número de celda this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE); //--- Cargamos el punto de anclaje del texto this.m_text_anchor=(ENUM_ANCHOR_POINT)::FileReadInteger(file_handle,INT_VALUE); //--- Cargamos la coordenada X del texto this.m_text_x=::FileReadInteger(file_handle,INT_VALUE); //--- Cargamos la coordenada Y del texto this.m_text_y=::FileReadInteger(file_handle,INT_VALUE); //--- Cargamos el texto if(::FileReadArray(file_handle,this.m_text)!=sizeof(this.m_text)) return false; //--- Todo correcto return true; }
Estos métodos permiten guardar en un archivo y cargar desde un archivo los datos de la celda de la tabla.
Ahora veamos la clase de representación visual de una fila de la tabla.
Clase de fila de la tabla (Vista)
Una fila de la tabla es un objeto heredado de la clase de panel. Contiene una lista de objetos de celda de la clase CTableCellView.
Sigamos escribiendo el código en el archivo Controls.mqh:
//+------------------------------------------------------------------+ //| Clase de representación visual de una fila de la tabla | //+------------------------------------------------------------------+ class CTableRowView : public CPanel { protected: CTableCellView m_temp_cell; // Objeto temporal de celda para la búsqueda CTableRow *m_table_row_model; // Puntero al modelo de fila CListElm m_list_cells; // Lista de celdas int m_index; // Índice en la lista de filas //--- Crea un nuevo objeto de representación visual de celda y lo añade a la lista CTableCellView *InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h); public: //--- Devuelve (1) una lista y (2) el número de celdas CListElm *GetListCells(void) { return &this.m_list_cells; } int CellsTotal(void) const { return this.m_list_cells.Total(); } //--- Establece el identificador virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Establece, (2) devuelve el índice de la fila void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Establece, (2) devuelve el modelo de fila bool TableRowModelAssign(CTableRow *row_model); CTableRow *GetTableRowModel(void) { return this.m_table_row_model; } //--- Imprime en el registro el modelo de fila asignado void TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); //--- Dibuja el aspecto externo virtual void Draw(const bool chart_redraw); //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto virtual int Compare(const CObject *node,const int mode=0)const { return CLabel::Compare(node,mode);} virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_ROW); } //--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto void Init(void); virtual void InitColors(void); //--- Constructores/destructor CTableRowView(void); CTableRowView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableRowView (void){} };
En una fila de la tabla, al igual que en una celda de la tabla, la propiedad «índice de fila» es igual a su identificador. Aquí también se ha redefinido el método virtual de establecimiento del identificador, que establece simultáneamente ambas propiedades: el índice y el identificador.
En los constructores de la clase se invoca el método de inicialización del objeto:
//+------------------------------------------------------------------+ //| CTableRowView::Constructor por defecto. Construye un objeto en la ventana principal | //| del gráfico actual, en las coordenadas 0,0 y con las dimensiones predeterminadas | //+------------------------------------------------------------------+ CTableRowView::CTableRowView(void) : CPanel("TableRow","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1) { //--- Inicialización this.Init(); } //+------------------------------------------------------------------+ //| CTableRowView::Constructor con parámetros. Construye un objeto en | //| en la ventana indicada del gráfico indicado, con el texto indicado, | //| las coordenadas y las dimensiones | //+------------------------------------------------------------------+ CTableRowView::CTableRowView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,chart_id,wnd,x,y,w,h), m_index(-1) { //--- Inicialización this.Init(); }
Inicialización del objeto:
//+------------------------------------------------------------------+ //| CTableRowView::Inicialización | //+------------------------------------------------------------------+ void CTableRowView::Init(void) { //--- Inicialización del objeto padre CPanel::Init(); //--- Fondo: opaco this.SetAlphaBG(255); //--- Ancho del borde this.SetBorderWidth(1); }
Método para inicializar los colores predeterminados del objeto:
//+------------------------------------------------------------------+ //| CTableRowView::Inicialización de los colores predeterminados del objeto | //+------------------------------------------------------------------+ void CTableRowView::InitColors(void) { //--- Inicializamos los colores de fondo para los estados normal y activado, y establecemos el color de fondo actual this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.BackColorToDefault(); //--- Inicializamos los colores de primer plano para los estados normal y activado, y establecemos el color de texto actual this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver); this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver); this.ForeColorToDefault(); //--- Inicializamos los colores del borde para los estados normal y activado, y establecemos el color actual del borde this.InitBorderColors(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver); this.InitBorderColorsAct(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver); this.BorderColorToDefault(); //--- Inicializamos el color del borde y el color de primer plano para el elemento bloqueado this.InitBorderColorBlocked(clrSilver); this.InitForeColorBlocked(clrSilver); }
Método que crea y añade a la lista un nuevo objeto de representación visual de la celda:
//+------------------------------------------------------------------+ //| CTableRowView::Crea y añade a la lista | //| nuevo objeto de representación visual de la celda | //+------------------------------------------------------------------+ CTableCellView *CTableRowView::InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h) { //--- Comprobamos si hay algún objeto en la lista con el identificador indicado y, si lo hay, lo indicamos y devolvemos NULL this.m_temp_cell.SetIndex(index); //--- Establecemos en la lista el indicador de ordenación por identificador this.m_list_cells.Sort(ELEMENT_SORT_BY_ID); if(this.m_list_cells.Search(&this.m_temp_cell)!=NULL) { ::PrintFormat("%s: Error. The TableCellView object with index %d is already in the list",__FUNCTION__,index); return NULL; } //--- Creamos el nombre y el identificador del objeto de celda string name="TableCellView"+(string)this.Index()+"x"+(string)index; int id=this.m_list_cells.Total(); //--- Creamos un nuevo objeto TableCellView; si falla, lo notificamos y devolvemos NULL CTableCellView *cell_view=new CTableCellView(id,name,""+text,dx,dy,w,h); if(cell_view==NULL) { ::PrintFormat("%s: Error. Failed to create CTableCellView object",__FUNCTION__); return NULL; } //--- Si no se ha podido añadir el nuevo objeto a la lista, lo notificamos, eliminamos el objeto y devolvemos NULL if(this.m_list_cells.Add(cell_view)==-1) { ::PrintFormat("%s: Error. Failed to add CTableCellView object to list",__FUNCTION__); delete cell_view; return NULL; } //--- Asignamos el elemento base y devolvemos un puntero al objeto cell_view.RowAssign(this.GetObject()); return cell_view; }
Aunque cada celda se asigna a su propia área del objeto de la fila de la tabla, de todos modos es necesario almacenar en una lista los objetos creados mediante el operador new, o bien realizar un seguimiento de ellos por cuenta propia y eliminarlos cuando sea necesario.
Es más fácil guardarlos en una lista; así, el subsistema del terminal se encargará de controlar cuándo hay que eliminarlos. El método crea un nuevo objeto de representación visual de la celda y lo añade a la lista de celdas de la fila. Una vez creado el objeto, se le asigna la fila en la que está ubicado y en cuyos lienzos el objeto creado dibujará los datos del modelo de celda de la tabla.
Método que establece el modelo de fila:
//+------------------------------------------------------------------+ //| CTableRowView::Establece el modelo de fila | //+------------------------------------------------------------------+ bool CTableRowView::TableRowModelAssign(CTableRow *row_model) { //--- Si se pasa un objeto vacío, lo indicamos y devolvemos false if(row_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Si en el modelo de fila pasado no hay ninguna celda, lo indicamos y devolvemos false int total=(int)row_model.CellsTotal(); if(total==0) { ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__); return false; } //--- Guardamos un puntero al modelo de fila pasado y calculamos la anchura de la celda this.m_table_row_model=row_model; int cell_w=(int)::round((double)this.Width()/(double)total); //--- En un bucle que recorre el número de celdas del modelo de fila for(int i=0;i<total;i++) { //--- obtenemos el modelo de la celda siguiente, CTableCell *cell_model=this.m_table_row_model.GetCell(i); if(cell_model==NULL) return false; //--- Calculamos la coordenada y creamos un nombre para el área de la celda int x=cell_w*i; string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i; //--- Creamos una nueva área de la celda CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height()); if(cell_bound==NULL) return false; //--- Creamos un nuevo objeto de representación visual de la celda CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height()); if(cell_view==NULL) return false; //--- Asignamos al área actual de la celda el objeto correspondiente de representación visual de la celda cell_bound.AssignObject(cell_view); } //--- Todo correcto return true; }
Al método se le pasa un puntero al modelo de fila y, en un bucle que recorre el número de celdas del modelo, se crean nuevas áreas de ubicación para los objetos de representación visual de las celdas y las propias celdas (clase CTableCellView). Y cada nueva celda de este tipo se asigna a cada área nueva. Como resultado, obtenemos una lista de las áreas de las celdas a las que se han asignado punteros a las celdas correspondientes de la tabla.
Método que dibuja el aspecto de la fila:
//+------------------------------------------------------------------+ //| CTableRowView::Dibuja el aspecto | //+------------------------------------------------------------------+ void CTableRowView::Draw(const bool chart_redraw) { //--- Rellenamos el objeto con el color de fondo, dibujamos la línea de la fila y actualizamos el lienzo de fondo this.Fill(this.BackColor(),false); this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG())); //--- Dibujamos las celdas de la fila int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Obtenemos el área de la siguiente celda CBound *cell_bound=this.GetBoundAt(i); if(cell_bound==NULL) continue; //--- Desde el área de la celda obtenemos el objeto de celda adjunto CTableCellView *cell_view=cell_bound.GetAssignedObj(); //--- Dibujamos la representación visual de la celda if(cell_view!=NULL) cell_view.Draw(false); } //--- Actualizamos los lienzos de fondo y de primer plano con el indicador de redibujado del gráfico especificado this.Update(chart_redraw); }
Primero, la fila se rellena con el color de fondo y, a continuación, se dibuja una línea en la parte inferior. A continuación, en el bucle que recorre las áreas de las celdas de la fila, obtenemos el área de la siguiente celda; a partir de ella, obtenemos un puntero al objeto de la celda y llamamos a su método de dibujo.
Método que imprime en el registro el modelo de fila asignado:
//+------------------------------------------------------------------+ //| CTableRowView::Imprime en el registro el modelo de fila asignado | //+------------------------------------------------------------------+ void CTableRowView::TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS) { if(this.m_table_row_model!=NULL) this.m_table_row_model.Print(detail,as_table,cell_width); }
Este método permite comprobar qué modelo de fila del modelo de tabla se ha asignado a este objeto de representación visual de la fila.
Métodos para trabajar con archivos:
//+------------------------------------------------------------------+ //| CTableRowView::Guardado en archivo | //+------------------------------------------------------------------+ bool CTableRowView::Save(const int file_handle) { //--- Guardamos los datos del objeto padre if(!CPanel::Save(file_handle)) return false; //--- Guardamos la lista de celdas if(!this.m_list_cells.Save(file_handle)) return false; //--- Guardamos el número de fila if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE) return false; //--- Todo correcto return true; } //+------------------------------------------------------------------+ //| CTableRowView::Carga desde archivo | //+------------------------------------------------------------------+ bool CTableRowView::Load(const int file_handle) { //--- Cargamos los datos del objeto padre if(!CPanel::Load(file_handle)) return false; //--- Cargamos la lista de celdas if(!this.m_list_cells.Load(file_handle)) return false; //--- Cargamos el número de fila this.m_id=this.m_index=(uchar)::FileReadInteger(file_handle,INT_VALUE); //--- Todo correcto return true; }
Estos métodos permiten guardar en un archivo y cargar desde un archivo los datos de la fila de la tabla.
Cada tabla debe tener un encabezado. Permite comprender qué datos se muestran en las columnas de la tabla. El encabezado de la tabla es una lista de encabezados de columna. Cada columna de la tabla tiene su propio encabezado, que indica el tipo y el nombre de los datos ubicados en la columna.
Clase del encabezado de columna de la tabla (Vista)
El encabezado de una columna de la tabla es un objeto independiente, derivado del objeto «Botón» (CButton), que hereda el comportamiento de este objeto y lo complementa. En la implementación actual no se añadirán nuevas propiedades al botón; simplemente cambiaremos los colores de los distintos estados. En el siguiente artículo añadiremos al encabezado una indicación de la dirección de ordenación: flechas hacia arriba y hacia abajo en el lado derecho del área del botón, así como un modelo de eventos para iniciar la ordenación al pulsar el botón.
Sigamos escribiendo el código en el archivo Controls.mqh:
//+------------------------------------------------------------------+ //| Clase de representación visual del encabezado de una columna de la tabla | //+------------------------------------------------------------------+ class CColumnCaptionView : public CButton { protected: CColumnCaption *m_column_caption_model; // Puntero al modelo del encabezado de columna int m_index; // Índice en la lista de columnas public: //--- Establece el identificador virtual void SetID(const int id) { this.m_index=this.m_id=id; } //--- (1) Establece, (2) devuelve el índice de la celda void SetIndex(const int index) { this.SetID(index); } int Index(void) const { return this.m_index; } //--- (1) Asigna, (2) devuelve el modelo del encabezado de columna bool ColumnCaptionModelAssign(CColumnCaption *caption_model); CColumnCaption *ColumnCaptionModel(void) { return this.m_column_caption_model; } //--- Muestra en el registro el modelo de encabezado de columna asignado void ColumnCaptionModelPrint(void); //--- Dibuja el aspecto externo virtual void Draw(const bool chart_redraw); //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto virtual int Compare(const CObject *node,const int mode=0)const { return CButton::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_COLUMN_CAPTION); } //--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto void Init(const string text); virtual void InitColors(void); //--- Devuelve la descripción del objeto virtual string Description(void); //--- Constructores/destructor CColumnCaptionView(void); CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CColumnCaptionView (void){} };
El método tiene un índice igual al identificador, al igual que en las dos clases anteriores que hemos visto. Y el método para establecer el identificador es similar al de las clases anteriores.
En los constructores de la clase se invoca el método de inicialización del objeto y se establece un identificador nulo (que cambia tras la creación del objeto):
//+------------------------------------------------------------------+ //| CColumnCaptionView::Constructor predeterminado. Construye un objeto | //| en la ventana principal del gráfico actual, en las coordenadas 0,0 | //| con las dimensiones predeterminadas | //+------------------------------------------------------------------+ CColumnCaptionView::CColumnCaptionView(void) : CButton("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0) { //--- Inicialización this.Init("Caption"); this.SetID(0); this.SetName("ColumnCaption"); } //+------------------------------------------------------------------+ //| CColumnCaptionView::Constructor parametrizado. | //| Construye un objeto en la ventana indicada del gráfico especificado con | //| el texto, las coordenadas y las dimensiones indicados | //+------------------------------------------------------------------+ CColumnCaptionView::CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) : CButton(object_name,text,chart_id,wnd,x,y,w,h), m_index(0) { //--- Inicialización this.Init(text); this.SetID(0); }
Método de inicialización del objeto:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Inicialización | //+------------------------------------------------------------------+ void CColumnCaptionView::Init(const string text) { //--- Desplazamientos predeterminados del texto this.m_text_x=4; this.m_text_y=2; //--- Establecemos los colores de los distintos estados this.InitColors(); }
Método para inicializar los colores predeterminados del objeto:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Inicialización de los colores predeterminados del objeto | //+------------------------------------------------------------------+ void CColumnCaptionView::InitColors(void) { //--- Inicializamos los colores de fondo para los estados normal y activado, y establecemos el color de fondo actual this.InitBackColors(clrWhiteSmoke,this.GetBackColorControl().NewColor(clrWhiteSmoke,-6,-6,-6),clrWhiteSmoke,clrWhiteSmoke); this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.BackColorToDefault(); //--- Inicializamos los colores de primer plano para los estados normal y activado, y establecemos el color de texto actual this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver); this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver); this.ForeColorToDefault(); //--- Inicializamos los colores del borde para los estados normal y activado, y establecemos el color actual del borde this.InitBorderColors(clrLightGray,clrLightGray,clrLightGray,clrLightGray); this.InitBorderColorsAct(clrLightGray,clrLightGray,clrLightGray,clrLightGray); this.BorderColorToDefault(); //--- Inicializamos el color del borde y el color de primer plano para el elemento bloqueado this.InitBorderColorBlocked(clrNULL); this.InitForeColorBlocked(clrSilver); }
Método que dibuja la apariencia del encabezado de una columna:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Dibuja la apariencia | //+------------------------------------------------------------------+ void CColumnCaptionView::Draw(const bool chart_redraw) { //--- Rellenamos el botón con el color de fondo, dibujamos una línea vertical clara a la izquierda y otra oscura a la derecha this.Fill(this.BackColor(),false); color clr_dark =this.BorderColor(); // "Color oscuro" color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100); // "Color claro" this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG())); // Línea a la izquierda this.m_background.Line(this.AdjX(this.Width()-1),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(clr_dark,this.AlphaBG())); // Línea a la derecha //--- Actualizamos el lienzo de fondo this.m_background.Update(false); //--- Mostramos el texto del encabezado CLabel::Draw(false); //--- Si se indica, actualizamos el gráfico if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Primero se rellena el fondo con el color establecido y, a continuación, se dibujan dos líneas verticales: una clara a la izquierda y otra oscura a la derecha. A continuación, se muestra el texto del encabezado en el lienzo de primer plano.
Método que devuelve la descripción de un objeto:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Devuelve la descripción del objeto | //+------------------------------------------------------------------+ string CColumnCaptionView::Description(void) { string nm=this.Name(); string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm); return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height()); }
El método devuelve una descripción del tipo de elemento con el identificador, las coordenadas y las dimensiones del objeto.
Método que asigna el modelo del encabezado de la columna:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Establece el modelo del encabezado de la columna | //+------------------------------------------------------------------+ bool CColumnCaptionView::ColumnCaptionModelAssign(CColumnCaption *caption_model) { //--- Si se ha pasado un objeto no válido del modelo del encabezado de la columna, informamos de ello y devolvemos false if(caption_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Guardamos el modelo del encabezado de la columna this.m_column_caption_model=caption_model; //--- Establecemos las dimensiones del área de dibujo de la representación visual del encabezado de la columna this.m_painter.SetBound(0,0,this.Width(),this.Height()); //--- Todo correcto return true; }
Al método se le pasa un puntero al modelo del encabezado de la columna, que se guarda en una variable. A continuación, se establecen las coordenadas y las dimensiones del área de dibujo del objeto de dibujo.
Método que imprime en el registro el modelo del encabezado de la columna asignado:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Imprime en el registro | //el modelo del encabezado de la columna asignado //+------------------------------------------------------------------+ void CColumnCaptionView::ColumnCaptionModelPrint(void) { if(this.m_column_caption_model!=NULL) this.m_column_caption_model.Print(); }
Permite comprobar la representación visual del encabezado imprimiendo en el registro la descripción del modelo del encabezado de la columna asignado al objeto.
Métodos para trabajar con archivos:
//+------------------------------------------------------------------+ //| CColumnCaptionView::Guardado en un archivo | //+------------------------------------------------------------------+ bool CColumnCaptionView::Save(const int file_handle) { //--- Guardamos los datos del objeto padre if(!CButton::Save(file_handle)) return false; //--- Guardamos el número del encabezado if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE) return false; //--- Todo correcto return true; } //+------------------------------------------------------------------+ //| CColumnCaptionView::Carga desde un archivo | //+------------------------------------------------------------------+ bool CColumnCaptionView::Load(const int file_handle) { //--- Cargamos los datos del objeto padre if(!CButton::Load(file_handle)) return false; //--- Cargamos el número del encabezado this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE); //--- Todo correcto return true; }
Permiten guardar los parámetros del encabezado en un archivo y cargarlos desde él.
Clase del encabezado de la tabla (Vista)
El encabezado de la tabla es una lista convencional de objetos de encabezado de columna, basada en el control Panel (CPanel). Este objeto ofrece, en definitiva, herramientas para gestionar las columnas de la tabla.
En esta implementación, se tratará de un objeto estático normal. Todas las funciones de interacción con el usuario se añadirán más adelante.
Seguiremos escribiendo el código en el mismo archivo:
//+------------------------------------------------------------------+ //| Clase de la representación visual del encabezado de la tabla | //+------------------------------------------------------------------+ class CTableHeaderView : public CPanel { protected: CColumnCaptionView m_temp_caption; // Objeto temporal del encabezado de la columna para la búsqueda CTableHeader *m_table_header_model; // Puntero al modelo del encabezado de la tabla //--- Crea y añade a la lista un nuevo objeto de representación visual del encabezado de la columna CColumnCaptionView *InsertNewColumnCaptionView(const string text, const int x, const int y, const int w, const int h); public: //--- (1) Establece, (2) devuelve el modelo del encabezado de la tabla bool TableHeaderModelAssign(CTableHeader *header_model); CTableHeader *GetTableHeaderModel(void) { return this.m_table_header_model; } //--- Imprime en el registro el modelo del encabezado de la tabla asignado void TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); //--- Dibuja el aspecto externo virtual void Draw(const bool chart_redraw); //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto virtual int Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode); } virtual bool Save(const int file_handle) { return CPanel::Save(file_handle); } virtual bool Load(const int file_handle) { return CPanel::Load(file_handle); } virtual int Type(void) const { return(ELEMENT_TYPE_TABLE_HEADER); } //--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto void Init(void); virtual void InitColors(void); //--- Constructores/destructor CTableHeaderView(void); CTableHeaderView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableHeaderView (void){} };
Al objeto se le asigna el modelo del encabezado de la tabla y, a partir de su contenido, se crean y se añaden a la lista de elementos adjuntos los objetos de encabezado de columna.
En los constructores de la clase se invoca el método de inicialización del objeto:
//+------------------------------------------------------------------+ //| CTableHeaderView::Constructor por defecto. Construye un objeto en | //| en la ventana principal del gráfico actual, en las coordenadas 0,0 | //| con las dimensiones predeterminadas | //+------------------------------------------------------------------+ CTableHeaderView::CTableHeaderView(void) : CPanel("TableHeader","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H) { //--- Inicialización this.Init(); } //+------------------------------------------------------------------+ //| CTableHeaderView::Constructor con parámetros. Construye un objeto en | //| en la ventana indicada del gráfico indicado, con el texto indicado, | //| las coordenadas y las dimensiones | //+------------------------------------------------------------------+ CTableHeaderView::CTableHeaderView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,chart_id,wnd,x,y,w,h) { //--- Inicialización this.Init(); }
Método de inicialización del objeto:
//+------------------------------------------------------------------+ //| CTableHeaderView::Inicialización | //+------------------------------------------------------------------+ void CTableHeaderView::Init(void) { //--- Inicialización del objeto padre CPanel::Init(); //--- Color de fondo: opaco this.SetAlphaBG(255); //--- Ancho del borde this.SetBorderWidth(1); }
Método para inicializar los colores predeterminados del objeto:
//+------------------------------------------------------------------+ //| CTableHeaderView::Inicialización de los colores predeterminados del objeto | //+------------------------------------------------------------------+ void CTableHeaderView::InitColors(void) { //--- Inicializamos los colores de fondo para los estados normal y activado, y establecemos el color de fondo actual this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke); this.BackColorToDefault(); //--- Inicializamos los colores de primer plano para los estados normal y activado, y establecemos el color de texto actual this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver); this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver); this.ForeColorToDefault(); //--- Inicializamos los colores del borde para los estados normal y activado, y establecemos el color actual del borde this.InitBorderColors(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver); this.InitBorderColorsAct(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver); this.BorderColorToDefault(); //--- Inicializamos el color del borde y el color de primer plano para el elemento bloqueado this.InitBorderColorBlocked(clrSilver); this.InitForeColorBlocked(clrSilver); }
Método que crea y añade a la lista un nuevo objeto de representación visual del encabezado de la columna:
//+------------------------------------------------------------------+ //| CTableHeaderView::Crea y añade a la lista | //| nuevo objeto de representación visual del encabezado de la columna | //+------------------------------------------------------------------+ CColumnCaptionView *CTableHeaderView::InsertNewColumnCaptionView(const string text,const int x,const int y,const int w,const int h) { //--- Creamos el nombre del objeto y devolvemos el resultado de la creación de un nuevo encabezado de la columna string user_name="ColumnCaptionView"+(string)this.m_list_elm.Total(); CColumnCaptionView *caption_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_COLUMN_CAPTION,text,user_name,x,y,w,h); return(caption_view!=NULL ? caption_view : NULL); }
El objeto del encabezado de la columna se crea mediante el método InsertNewElement(), estándar para los objetos de la biblioteca, que coloca los objetos creados en la lista de elementos gráficos del objeto.
Método que establece el modelo del encabezado:
//+------------------------------------------------------------------+ //| CTableHeaderView::Establece el modelo del encabezado | //+------------------------------------------------------------------+ bool CTableHeaderView::TableHeaderModelAssign(CTableHeader *header_model) { //--- Si se pasa un objeto vacío, lo indicamos y devolvemos false if(header_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Si en el modelo de encabezado recibido no hay ningún encabezado de columna, lo indicamos y devolvemos false int total=(int)header_model.ColumnsTotal(); if(total==0) { ::PrintFormat("%s: Error. Header model does not contain any columns",__FUNCTION__); return false; } //--- Guardamos un puntero al modelo del encabezado de la tabla que se nos ha pasado y calculamos la anchura de cada encabezado de columna this.m_table_header_model=header_model; int caption_w=(int)::round((double)this.Width()/(double)total); //--- En un bucle según el número de encabezados de columna en el modelo del encabezado de la tabla for(int i=0;i<total;i++) { //--- obtenemos el modelo del encabezado de la columna siguiente, CColumnCaption *caption_model=this.m_table_header_model.GetColumnCaption(i); if(caption_model==NULL) return false; //--- calculamos la coordenada y creamos un nombre para el área del encabezado de la columna int x=caption_w*i; string name="CaptionBound"+(string)i; //--- Creamos una nueva área del encabezado de la columna CBound *caption_bound=this.InsertNewBound(name,x,0,caption_w,this.Height()); if(caption_bound==NULL) return false; //--- Creamos un nuevo objeto de representación visual del encabezado de la columna CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height()); if(caption_view==NULL) return false; //--- Al área actual del encabezado de la columna le asignamos el objeto correspondiente de representación visual del encabezado de la columna caption_bound.AssignObject(caption_view); } //--- Todo correcto return true; }
Al método se le pasa el modelo del encabezado de la tabla. En un bucle según el número de encabezados de columna del modelo, creamos una nueva área para colocar el encabezado de la columna. Creamos un objeto del encabezado de la columna y lo asignamos al área actual. Al finalizar el bucle, tendremos una lista de áreas con los encabezados de columna asignados a cada una de ellas.
Método que dibuja el aspecto del encabezado de la tabla:
//+------------------------------------------------------------------+ //| CTableHeaderView::Dibuja el aspecto | //+------------------------------------------------------------------+ void CTableHeaderView::Draw(const bool chart_redraw) { //--- Rellenamos el objeto con el color de fondo, dibujamos la línea de la fila y actualizamos el lienzo de fondo this.Fill(this.BackColor(),false); this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG())); this.m_background.Update(false); //--- Dibujamos los encabezados de las columnas int total=this.m_list_bounds.Total(); for(int i=0;i<total;i++) { //--- Obtenemos el área del encabezado de la columna siguiente CBound *cell_bound=this.GetBoundAt(i); if(cell_bound==NULL) continue; //--- Del área del encabezado de la columna obtenemos el objeto adjunto del encabezado de la columna CColumnCaptionView *caption_view=cell_bound.GetAssignedObj(); //--- Dibujamos la representación visual del encabezado de la columna if(caption_view!=NULL) caption_view.Draw(false); } //--- Si se indica, actualizamos el gráfico if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
En primer lugar, todo el área del encabezado de la tabla se rellena con el color de fondo y, a continuación, se dibuja una línea divisoria en la parte inferior. A continuación, en el bucle que recorre la lista de áreas de encabezados, obtenemos la siguiente área y, a partir de ella, el objeto del encabezado de la columna asignado a dicha área, cuyo método de dibujo invocamos.
Método que imprime en el registro el modelo del encabezado de la tabla asignado:
//+------------------------------------------------------------------+ //| CTableHeaderView::Imprime en el registro | //| el modelo del encabezado de la tabla asignado | //+------------------------------------------------------------------+ void CTableHeaderView::TableHeaderModelPrint(const bool detail,const bool as_table=false,const int cell_width=CELL_WIDTH_IN_CHARS) { if(this.m_table_header_model!=NULL) this.m_table_header_model.Print(detail,as_table,cell_width); }
Permite imprimir en el registro el modelo del encabezado de la tabla asignado a este objeto.
Una tabla, es decir, su representación visual, es un objeto compuesto que obtiene de su modelo los datos de la tabla y del encabezado, y dibuja esos datos en distintos controles. Se trata de un panel en el que están ubicados el encabezado y un contenedor con datos tabulares (filas de la tabla).
Clase de tabla (Vista)
Sigamos escribiendo el código en el archivo Controls.mqh:
//+------------------------------------------------------------------+ //| Clase de la representación visual de la tabla | //+------------------------------------------------------------------+ class CTableView : public CPanel { protected: //--- Datos obtenidos de la tabla CTable *m_table_obj; // Puntero a un objeto de tabla (incluye los modelos de la tabla y del encabezado de la tabla) CTableModel *m_table_model; // Puntero al modelo de la tabla (obtenido de CTable) CTableHeader *m_header_model; // Puntero al modelo del encabezado de la tabla (obtenido de CTable) //--- Datos del componente Vista CTableHeaderView *m_header_view; // Puntero al encabezado de la tabla (Vista) CPanel *m_table_area; // Panel para colocar las filas de la tabla CContainer *m_table_area_container; // Contenedor para alojar el panel con las filas de la tabla //--- (1) Establece, (2) devuelve el modelo de la tabla bool TableModelAssign(CTableModel *table_model); CTableModel *GetTableModel(void) { return this.m_table_model; } //--- (1) Establece, (2) devuelve el modelo del encabezado de la tabla bool HeaderModelAssign(CTableHeader *header_model); CTableHeader *GetHeaderModel(void) { return this.m_header_model; } //--- Crea, a partir del modelo, un objeto (1) de encabezado y (2) de tabla bool CreateHeader(void); bool CreateTable(void); public: //--- (1) Establece, (2) devuelve un objeto de tabla bool TableObjectAssign(CTable *table_obj); CTable *GetTableObj(void) { return this.m_table_obj; } //--- Muestra en el registro el modelo asignado (1) de la tabla, (2) del encabezado y (3) del objeto de tabla void TableModelPrint(const bool detail); void HeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS); void TablePrint(const int column_width=CELL_WIDTH_IN_CHARS); //--- Dibuja el aspecto externo virtual void Draw(const bool chart_redraw); //--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto virtual int Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode); } virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_TABLE); } //--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto void Init(void); //--- Constructores/destructor CTableView(void); CTableView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); ~CTableView (void){} };
Esta clase hereda de la clase del objeto de panel. Se ven tres componentes «Modelo» y tres componentes «Vista». En la clase hay punteros al modelo de la tabla, al modelo del encabezado y a la tabla. De la tabla obtenemos punteros a los datos de la tabla y al encabezado. Y hay tres punteros al encabezado, al panel de filas de la tabla y al contenedor para alojar este panel.
La idea es que las filas de la tabla se adjuntan a un objeto de panel (se pueden adjuntar varios elementos), y este panel se adjunta a un contenedor (se puede adjuntar un único elemento), donde hay barras de desplazamiento que permiten mover el panel con las filas de la tabla. De este modo, visualmente, el control tiene el aspecto de un encabezado de la tabla, y debajo aparece una tabla desplazable (filas y columnas formadas por celdas).
En los constructores de la clase, llamamos al método de inicialización del objeto:
//+------------------------------------------------------------------+ //| CTableView::Constructor por defecto. | //| Construye un elemento en la ventana principal del gráfico actual | //| en las coordenadas 0,0 con las dimensiones predeterminadas | //+------------------------------------------------------------------+ CTableView::CTableView(void) : CPanel("TableView","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H), m_table_model(NULL),m_header_model(NULL),m_table_obj(NULL),m_header_view(NULL),m_table_area(NULL),m_table_area_container(NULL) { //--- Inicialización this.Init(); } //+------------------------------------------------------------------+ //| CTableView::Constructor paramétrico. | //| Construye un elemento en la ventana indicada del gráfico especificado | //| con el texto, las coordenadas y las dimensiones indicadas | //+------------------------------------------------------------------+ CTableView::CTableView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) : CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_table_model(NULL),m_header_model(NULL),m_table_obj(NULL),m_header_view(NULL),m_table_area(NULL),m_table_area_container(NULL) { //--- Inicialización this.Init(); }
En el método de inicialización creamos en el panel del objeto todos los controles necesarios:
//+------------------------------------------------------------------+ //| CTableView::Inicialización | //+------------------------------------------------------------------+ void CTableView::Init(void) { //--- Inicialización del objeto padre CPanel::Init(); //--- Ancho del borde this.SetBorderWidth(1); //--- Creamos el encabezado de la tabla this.m_header_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_HEADER,"","TableHeader",0,0,this.Width(),DEF_TABLE_HEADER_H); if(this.m_header_view==NULL) return; this.m_header_view.SetBorderWidth(1); //--- Creamos un contenedor en el que se ubicará el panel de filas de la tabla this.m_table_area_container=this.InsertNewElement(ELEMENT_TYPE_CONTAINER,"","TableAreaContainer",0,DEF_TABLE_HEADER_H,this.Width(),this.Height()-DEF_TABLE_HEADER_H); if(this.m_table_area_container==NULL) return; this.m_table_area_container.SetBorderWidth(0); this.m_table_area_container.SetScrollable(true); //--- Adjuntamos al contenedor el panel para almacenar las filas de la tabla int shift_y=0; this.m_table_area=this.m_table_area_container.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableAreaPanel",0,shift_y,this.m_table_area_container.Width(),this.m_table_area_container.Height()-shift_y); if(m_table_area==NULL) return; this.m_table_area.SetBorderWidth(0); }
Método que establece el modelo de la tabla:
//+------------------------------------------------------------------+ //| CTableView::Establece el modelo de la tabla | //+------------------------------------------------------------------+ bool CTableView::TableModelAssign(CTableModel *table_model) { if(table_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } this.m_table_model=table_model; return true; }
Simplemente pasamos al método un puntero al objeto del modelo de la tabla y lo almacenamos en una variable, desde la que luego accederemos a él.
Método que establece el modelo del encabezado de la tabla:
//+------------------------------------------------------------------+ //| CTableView::Establece el modelo del encabezado de la tabla | //+------------------------------------------------------------------+ bool CTableView::HeaderModelAssign(CTableHeader *header_model) { if(header_model==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } this.m_header_model=header_model; return true; }
Pasamos al método un puntero al objeto del modelo del encabezado de la tabla y lo almacenamos en una variable, desde la que accederemos a él.
Método que establece el objeto de la tabla:
//+------------------------------------------------------------------+ //| CTableView::Establece el objeto de la tabla | //+------------------------------------------------------------------+ bool CTableView::TableObjectAssign(CTable *table_obj) { //--- Si se ha pasado un objeto de la tabla vacío, lo notificamos y devolvemos false if(table_obj==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return false; } //--- Guardamos el puntero en una variable this.m_table_obj=table_obj; //--- Registramos el resultado de la asignación del modelo de la tabla y del modelo del encabezado bool res=this.TableModelAssign(this.m_table_obj.GetTableModel()); res &=this.HeaderModelAssign(this.m_table_obj.GetTableHeader()); //--- Si no se ha podido asignar alguno de los modelos, devolvemos false if(!res) return false; //--- Registramos el resultado de crear el encabezado de la tabla y la tabla a partir de sus modelos res=this.CreateHeader(); res&=this.CreateTable(); //--- Devolvemos el resultado return res; }
Se pasa al método un puntero al objeto de la tabla. A partir de este objeto, obtenemos y asignamos el modelo del encabezado y el modelo de la tabla. A continuación, a partir de los datos de los modelos del encabezado y de la tabla, creamos los objetos de representación visual del encabezado y de la tabla.
Método que crea un objeto de tabla a partir de un modelo:
//+------------------------------------------------------------------+ //| CTableView::Crea un objeto de tabla a partir del modelo | //+------------------------------------------------------------------+ bool CTableView::CreateTable(void) { if(this.m_table_area==NULL) return false; //--- En un bucle, creamos RowsTotal filas a partir de elementos TableRowView y las adjuntamos al elemento Panel (m_table_area) int total=(int)this.m_table_model.RowsTotal(); int y=1; // Desplazamiento vertical int table_height=0; // Altura calculada del panel CTableRowView *row=NULL; for(int i=0;i<total;i++) { //--- Creamos y adjuntamos al panel un objeto de fila de la tabla row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H); if(row==NULL) return false; //--- Establecemos el identificador de la fila row.SetID(i); //--- En función del número de fila (par/impar), establecemos el color de su fondo if(row.ID()%2==0) row.InitBackColorDefault(clrWhite); else row.InitBackColorDefault(clrWhiteSmoke); row.BackColorToDefault(); row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4)); //--- Obtenemos el modelo de fila a partir del objeto de la tabla CTableRow *row_model=this.m_table_model.GetRow(i); if(row_model==NULL) return false; //--- Asignamos el modelo de fila obtenido al objeto de fila de la tabla creado row.TableRowModelAssign(row_model); //--- Calculamos el nuevo valor de la altura del panel table_height+=row.Height(); } //--- Devolvemos el resultado de cambiar el tamaño del panel al valor calculado en el bucle return this.m_table_area.ResizeH(table_height+y); }
La lógica del método se explica en los comentarios del código. En un bucle según el número de filas del modelo de la tabla, creamos un nuevo objeto de fila de la tabla y lo adjuntamos al panel, calculando al mismo tiempo la altura futura del panel. Una vez finalizado el bucle y creadas todas las filas necesarias, la altura del panel se modifica de acuerdo con el tamaño calculado en el bucle.
Método que dibuja el aspecto de la tabla:
//+------------------------------------------------------------------+ //| CTableView::Dibuja el aspecto | //+------------------------------------------------------------------+ void CTableView::Draw(const bool chart_redraw) { //--- Dibujamos el encabezado y las filas de la tabla this.m_header_view.Draw(false); this.m_table_area_container.Draw(false); //--- Si se indica, actualizamos el gráfico if(chart_redraw) ::ChartRedraw(this.m_chart_id); }
Primero dibujamos el encabezado de la tabla y, debajo, las filas de la tabla dentro del contenedor.
Método que imprime en el registro el modelo de la tabla asignado:
//+------------------------------------------------------------------+ //| CTableView::Imprime en el registro el modelo de la tabla asignado| //+------------------------------------------------------------------+ void CTableView::TableModelPrint(const bool detail) { if(this.m_table_model!=NULL) this.m_table_model.Print(detail); }
Permite imprimir en el registro el modelo de la tabla asignado.
Método que imprime en el registro el modelo del encabezado de la tabla asignado:
//+------------------------------------------------------------------+ //| CTableView::Imprime en el registro el modelo del encabezado de la tabla asignado | //+------------------------------------------------------------------+ void CTableView::HeaderModelPrint(const bool detail,const bool as_table=false,const int column_width=CELL_WIDTH_IN_CHARS) { if(this.m_header_model!=NULL) this.m_header_model.Print(detail,as_table,column_width); }
Permite imprimir en el registro el modelo del encabezado de la tabla asignado.
Método que imprime en el registro el objeto de tabla asignado:
//+------------------------------------------------------------------+ //| CTableView::Imprime en el registro el objeto de tabla asignado | //+------------------------------------------------------------------+ void CTableView::TablePrint(const int column_width=CELL_WIDTH_IN_CHARS) { if(this.m_table_obj!=NULL) this.m_table_obj.Print(column_width); }
Imprime en el registro toda la tabla asignada al objeto: el encabezado y los datos.
Ya está, hemos creado todas las clases necesarias para generar la representación visual del modelo de la tabla. Veamos cómo se puede crear ahora una tabla sencilla a partir de un array de datos.
Probamos el resultado
En el directorio del terminal \MQL5\Indicators\Tables\ crearemos un nuevo indicador, sin búferes y que se dibuje en una subventana del gráfico:
//+------------------------------------------------------------------+ //| iTestTable.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 0 #property indicator_plots 0
Incluiremos la biblioteca en el archivo del indicador y declararemos globalmente los punteros a los objetos:
//+------------------------------------------------------------------+ //| iTestTable.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_separate_window #property indicator_buffers 0 #property indicator_plots 0 //+------------------------------------------------------------------+ //| Bibliotecas incluidas | //+------------------------------------------------------------------+ #include "Controls\Controls.mqh" // Библиотека элементов управления CPanel *panel=NULL; // Puntero al elemento gráfico Panel CTable *table; // Puntero al objeto de la tabla (Modelo)
A continuación, escribiremos el siguiente código en el controlador OnInit() del indicador:
//+------------------------------------------------------------------+ //| Función de inicialización del indicador personalizado | //+------------------------------------------------------------------+ int OnInit() { //--- Buscamos la subventana del gráfico int wnd=ChartWindowFind(); //--- Creamos el elemento gráfico «Panel» panel=new CPanel("Panel","",0,wnd,100,40,400,192); if(panel==NULL) return INIT_FAILED; //--- Configuramos los parámetros del panel panel.SetID(1); // Identificador panel.SetAsMain(); // En el gráfico debe haber necesariamente un elemento principal panel.SetBorderWidth(1); // Ancho del borde (sangría del área visible de un píxel en cada lado del contenedor) panel.SetResizable(false); // Se ha desactivado la opción de cambiar el tamaño arrastrando los bordes y las esquinas panel.SetName("Main container"); // Nombre //--- Creamos los datos para la tabla //--- Declaramos y rellenamos un array de encabezados de columnas con dimensión 4 string captions[4]={"Column 0","Column 1","Column 2","Column 3"}; //--- Declaramos y rellenamos un array de datos con dimensión 10x4 //--- El tipo de array puede ser double, long, datetime, color o string long array[10][4]={{ 1, 2, 3, 4}, { 5, 6, 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24}, {25, 26, 27, 28}, {29, 30, 31, 32}, {33, 34, 35, 36}, {37, 38, 39, 40}}; //--- Creamos un objeto de tabla a partir del array de tipo long de 10x4 creado anteriormente y del array de tipo string con los encabezados de columnas (componente Modelo) table=new CTable(array,captions); if(table==NULL) return INIT_FAILED; PrintFormat("The [%s] has been successfully created:",table.Description()); //--- En el panel, creamos un nuevo elemento: una tabla (componente Vista) CTableView *table_view=panel.InsertNewElement(ELEMENT_TYPE_TABLE,"","TableView",4,4,panel.Width()-8,panel.Height()-8); //--- Asignamos un objeto de tabla (Modelo) al elemento gráfico «Tabla» (Vista) table_view.TableObjectAssign(table); //--- Imprimiremos en el registro el modelo de la tabla table_view.TablePrint(); //--- Dibujemos la tabla junto con el panel panel.Draw(true); //--- Correcto return(INIT_SUCCEEDED); }
Aquí hay tres bloques de código:
- Creación del panel sobre el que se construirá la tabla;
- Creación de datos tabulares en dos arrays: la tabla y el encabezado (componentes del Modelo);
- Creación de una tabla en el panel (componente Vista).
En esencia, para crear una tabla se necesitan dos pasos: construir los datos (punto 2) y construir la tabla (punto 3). El panel solo es necesario para la presentación y como base para el elemento gráfico.
Vamos a completar el resto del código del indicador:
//+------------------------------------------------------------------+ //| Función de inicialización del indicador personalizado | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Eliminamos el elemento «Panel» y destruimos la tabla y el gestor de recursos comunes de la biblioteca delete panel; delete table; CCommonManager::DestroyInstance(); } //+------------------------------------------------------------------+ //| Función de iteración del indicador personalizado | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- //--- valor de retorno de «prev_calculated» para la siguiente llamada return(rates_total); } //+------------------------------------------------------------------+ //| Función ChartEvent | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Llamamos al controlador OnChartEvent del elemento «Panel» panel.OnChartEvent(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+ //| Temporizador | //+------------------------------------------------------------------+ void OnTimer(void) { //--- Llamamos al controlador OnTimer del elemento «Panel» panel.OnTimer(); }
Compilemos el indicador y ejecutémoslo en el gráfico:

En el registro del terminal aparecerá un mensaje indicando que la tabla se ha creado correctamente, se mostrará la descripción de la tabla y se verá la propia tabla con el encabezado:
The [Table: Rows total: 10, Columns total: 4] has been successfully created: Table: Rows total: 10, Columns total: 4: | n/n | Column 0 | Column 1 | Column 2 | Column 3 | | 0 | 1 | 2 | 3 | 4 | | 1 | 5 | 6 | 7 | 8 | | 2 | 9 | 10 | 11 | 12 | | 3 | 13 | 14 | 15 | 16 | | 4 | 17 | 18 | 19 | 20 | | 5 | 21 | 22 | 23 | 24 | | 6 | 25 | 26 | 27 | 28 | | 7 | 29 | 30 | 31 | 32 | | 8 | 33 | 34 | 35 | 36 | | 9 | 37 | 38 | 39 | 40 |
En el gráfico vemos que los encabezados de las columnas y las filas de la tabla están activos y reaccionan al pasar el puntero del ratón por encima. En el próximo artículo hablaremos sobre cómo gestionar la interacción del usuario con la tabla.
Conclusión
Hemos aprendido a crear tablas sencillas y a representarlas en el gráfico. Pero, por ahora, se trata solo de una tabla estática que muestra datos obtenidos una sola vez. El próximo artículo estará dedicado a hacer dinámicas las tablas: a modificar y visualizar datos dinámicos, así como a interactuar con el usuario para configurar la visualización de las filas y columnas de la tabla. Además, en el próximo artículo simplificaremos aún más la creación de tablas.
Programas utilizados en el artículo:
| # | Nombre | Tipo | Descripción |
|---|---|---|---|
| 1 | Tables.mqh | Biblioteca de clases | Clases para crear el modelo de la tabla |
| 2 | Base.mqh | Biblioteca de clases | Clases para crear el objeto base de los controles |
| 3 | Controls.mqh | Biblioteca de clases | Clases de controles |
| 4 | iTestTable.mq5 | Indicador de prueba | Indicador para probar el funcionamiento del control TableView |
| 5 | MQL5.zip | Archivo comprimido | Archivo comprimido con los archivos mostrados anteriormente, para descomprimir en el directorio MQL5 del terminal de cliente |
Todos los archivos creados se adjuntan al artículo para su estudio independiente. El archivo comprimido se puede descomprimir en la carpeta del terminal, y todos los archivos quedarán ubicados en la carpeta correspondiente: \MQL5\Indicators\Tables\.
El código fuente completo del proyecto, con todos los archivos descritos en el artículo, está disponible en el repositorio.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/19288
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Asesor experto de Forex basado en la red neuronal N-BEATS
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 34): Conversión de datos brutos del mercado en modelos predictivos mediante un pipeline avanzado de ingesta
Particularidades del trabajo con números del tipo double en MQL4
Redes neuronales en el trading: Modelo de difusión adaptativa sobre grafos (SAGDFN)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
Excelente explicación del modelo MVC, pero ¿cómo mejora exactamente esta complejidad el trading, que es para lo que realmente estamos aquí?
¿Se han resuelto ya todos los demás retos del trading, quedando solo la interfaz de usuario?
A veces olvidamos para qué sirve realmente una herramienta —o nos frustramos cuando no funciona como queremos— y acabamos intentando que haga cosas para las que nunca fue diseñada y que nunca hará.
Es una descripción excelente del modelo MVC, pero ¿cómo mejora exactamente esta complejidad el comercio, que es, en realidad, el motivo por el que nos hemos reunido aquí?
¿Acaso se han resuelto todos los demás problemas del comercio y solo queda la interfaz de usuario?
A veces olvidamos para qué sirve realmente una herramienta, o nos frustramos cuando no funciona como queremos y, al final, intentamos obligarla a hacer algo para lo que nunca fue diseñada y que nunca hará.