Interfaces gráficas X: Actualizaciones para la tabla dibujada y optimización del código (build 10)
Índice
- Introducción
- Coordenadas relativas del cursor del ratón en el lienzo especificado para el dibujo
- Cambios en la estructura de la tabla
- Determinación del rango de las filas dentro del área de visibilidad
- Imágenes en las celdas de la tabla
- Resalto de la fila de la tabla al situar el cursor encima
- Métodos para el redibujo rápido de la celda de la tabla
- Aplicación para la prueba del control
- Conclusión
Introducción
El primer artículo de la serie nos cuenta con más detalles para qué sirve esta librería: Interfaces gráficas I: Preparación de la estructura de la librería (Capítulo 1). Al final de cada artículo de la serie se muestra la lista completa de los capítulos con los enlaces. Además, se puede descargar la versión completa de la librería en la fase actual del desarrollo del proyecto. Es necesario colocar los ficheros en los mismos directorios, tal como están ubicados en el archivo.
Seguiremos completando la tabla dibujada (CCanvasTable) con nuevas funcionalidades. Esta vez, vamos a añadir las siguientes posibilidades:
- Resalto de la fila de la tabla al situar el cursor encima.
- Posibilidad de agregar el array de imágenes para cada celda y el método para su conmutación.
- Posibilidad de establecer y editar el texto de las celdas durante la ejecución del programa.
Aparte de eso, optimizaremos el código y algunos algoritmos para acelerar el redibujo de la tabla.
Coordenadas relativas del cursor en el lienzo especificado para el dibujo
Con el fin de evitar el código repetitivo en muchos métodos y clases para el cálculo de las coordenadas relativas en el lienzo para el dibujo, ahora la clase CMouse va a incluir los métodos adicionales CMouse::RelativeX() y CMouse::RelativeY() para obtener estas coordenadas. La referencia al objeto tipo CRectCanvas debe ser pasada a estos métodos para poder calcular la coordenada relativa, tomando en cuenta el desplazamiento actual del área de visibilidad del lienzo para el dibujo.
//| Clase para obtener los parámetros del ratón |
//+------------------------------------------------------------------+
class CMouse
{
public:
//--- Devuelve las coordenadas relativas del cursor del objeto del lienzo pasado para el dibujo
int RelativeX(CRectCanvas &object);
int RelativeY(CRectCanvas &object);
};
//+------------------------------------------------------------------+
//| Devuelve la coordenada relativa X del cursor del ratón |
//| del objeto del lienzo pasado para el dibujo |
//+------------------------------------------------------------------+
int CMouse::RelativeX(CRectCanvas &object)
{
return(m_x-object.X()+(int)object.GetInteger(OBJPROP_XOFFSET));
}
//+------------------------------------------------------------------+
//| Devuelve la coordenada relativa Y del cursor del ratón |
//| del objeto del lienzo pasado para el dibujo |
//+------------------------------------------------------------------+
int CMouse::RelativeY(CRectCanvas &object)
{
return(m_y-object.Y()+(int)object.GetInteger(OBJPROP_YOFFSET));
}
Durante el proceso del desarrollo de nuestra librería, estos métodos van a usarse para obtener las coordenadas relativas de todos los controles dibujados.
Cambios en la estructura de la tabla
Para optimizar al máximo la ejecución del código de la tabla dibujada, tenía que modificar un poco y completar la estructura de la tabla tipo CTOptions, así como añadir nuevas estructuras que permiten construir los arrays multidimensionales. Nuestra tarea consiste en hacer que algunos fragmentos de la tabla se redibujen según los valores calculados anteriormente. Por ejemplo, puede tratarse de las coordenadas de los límites de columnas y filas.
Como ejemplo, calcular y guardar las coordenadas X de los límites de las columnas conviene en el método CCanvasTable::DrawGrid(), que se usa para trazar la cuadrícula, y solamente al dibujar la tabla entera. Y cuando el usuario selecciona alguna fila de la tabla, se puede usar los valores ya preparados. Lo mismo se refiere al resalto de las filas de la tabla al situar el cursor encima (lo veremos a continuación).
Para guardar las coordenadas Y de las filas de la tabla, y tal vez en el futuro, otras propiedades de las filas, crearemos una estructura separada (CTRowOptions) y declararemos el array de sus instancias. Las coordenadas Y de las filas se calculan en el método CCanvasTable::DrawRows(), que sirve para dibujar el fondo de las filas. Puesto que este método se invoca antes del trazado de la cuadrícula, en el método CCanvasTable::DrawGrid() se utilizan los valores ya calculados desde la estructura CTRowOptions.
Para almacenar los valores y las propiedades de las celdas de la tabla, vamos a crear una estructura separada tipo CTCell. Precisamente con este tipo se declara el array de las instancias en la estructura CTRowOptions, como array de las filas de la tabla. En esta estructura van a almacenarse:
- Arrays de imágenes
- Arrays de tamaños de imágenes
- Indice de la imagen seleccionada (mostrada) en la celda
- Texto completo
- Texto reducido
- Color del texto
Puesto que cada imagen representa un array de los píxeles, necesitaremos una estructura separada (CTImage) con el array dinámico para su almacenamiento. A continuación, se muestra el código de las estructuras mencionadas:
{
private:
//--- Array de los píxeles de la imagen
struct CTImage { uint m_image_data[]; };
//--- Propiedades de las celdas de la tabla
struct CTCell
{
CTImage m_images[]; // Arrays de imágenes
uint m_image_width[]; // Array del ancho de las imágenes
uint m_image_height[]; // Array del alto de las imágenes
int m_selected_image; // indice de la imagen seleccionada (mostrada)
string m_full_text; // Texto completo
string m_short_text; // Texto reducido
color m_text_color; // Color del texto
};
//--- Array de las filas y propiedades de las columnas de la tabla
struct CTOptions
{
int m_x; // Coordenada X del lado izquierdo de la columna
int m_x2; // Coordenada X del lado derecho de la columna
int m_width; // Ancho de la columna
ENUM_ALIGN_MODE m_text_align; // Modo de alineación del texto en las celdas de la columna
int m_text_x_offset; // Sangrado del texto
string m_header_text; // Texto del encabezado de la columna
CTCell m_rows[]; // Array de las filas de la tabla
};
CTOptions m_columns[];
//--- Array de las propiedades de las filas de la tabla
struct CTRowOptions
{
int m_y; // Coordenada Y del lado superior de la fila
int m_y2; // Coordenada Y del lado inferior de la fila
};
CTRowOptions m_rows[];
};
Las correcciones correspondientes han sido introducidas en todos los métodos donde se utilizan estos tipos de datos.
Determinación del rango de las filas dentro del área de visibilidad
Dado que la tabla puede contener un gran numero de filas, la búsqueda del foco en una de las filas con el posterior redibujo de la tabla puede ralentizar considerablemente el proceso. Lo mismo se refiere a la selección de la fila y a la corrección del largo del texto al cambiar el ancho de la columna manualmente. Para evitar la ralentización, es necesario determinar el primer y el último índice en el área visible de la tabla y organizar el ciclo del repaso precisamente en este rango. Para eso disponemos del método CCanvasTable::VisibleTableIndexes(). Primero, en este método se determinan los límites del área visible. El límite superior es el desplazamiento del área visible por el eje Y, y el límite inferior se determina como el límite superior + el tamaño del área visible por el eje Y.
Para determinar los índices de la fila superior e inferior del área visible de la tabla, será suficiente dividir los valores de los límites obtenidos por el alto de la fila establecido en las propiedades de la tabla. Al final del método, se realiza la corrección si hemos salido fuera de los límites de la última fila de la tabla.
{
private:
//--- Para determinar los índices de la parte visible de la tabla
int m_visible_table_from_index;
int m_visible_table_to_index;
//---
private:
//--- Determinación de los índices del área visible de la tabla
void VisibleTableIndexes(void);
};
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_visible_table_from_index(WRONG_VALUE),
m_visible_table_to_index(WRONG_VALUE)
{
...
}
//+------------------------------------------------------------------+
//| Determinación de los índices del área visible de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::VisibleTableIndexes(void)
{
//--- Determinamos los límites teniendo en cuenta el desplazamiento del área visible de la tablla
int yoffset1 =(int)m_table.GetInteger(OBJPROP_YOFFSET);
int yoffset2 =yoffset1+m_table_visible_y_size;
//--- Determinamos el primer y el último índice del área visible de la tabla
m_visible_table_from_index =int(double(yoffset1/m_cell_y_size));
m_visible_table_to_index =int(double(yoffset2/m_cell_y_size));
//--- El índice inferior es más grande a uno si no salimos fuera del rango
m_visible_table_to_index=(m_visible_table_to_index+1>m_rows_total)? m_rows_total : m_visible_table_to_index+1;
}
La determinación de los índices va a realizarse en el método CCanvasTable::DrawTable(). Ahora a este método se le puede pasar el argumento para indicar que es necesario redibujar solamente la parte visible de la tabla. Por defecto, el valor del argumento es igual a false, lo que indica en el redibujo de la tabla entera. Abajo se muestra la versión reducida de este método.
//| Dibuja la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTable(const bool only_visible=false)
{
//--- Si no está indicado redibujar sólo la parte visible de la tabla
if(!only_visible)
{
//--- Determinamos los índices de las filas de la tabla entera desde el principio hasta el final
m_visible_table_from_index =0;
m_visible_table_to_index =m_rows_total;
}
//--- Obtenemos los índices de las filas de la parte visible de la tabla
else
VisibleTableIndexes();
//--- Dibujar el fondo de las filas de la tabla
//--- Dibujar la fila seleccionada
//--- Dibujar la cuadrícula
//--- Dibujar la imagen
//--- Dibujar el texto
//--- Mostrar los últimos cambios dibujados
//--- Actualizar los encabezados si están activados
//--- Corrección de la tabla respecto a la barras de desplazamiento
}
La llamada al método CCanvasTable::VisibleTableIndexes() también es necesaria en el método para la determinación del foco en las filas de la tabla:
//| Comprobación del foco en las filas de la tabla |
//+------------------------------------------------------------------+
int CCanvasTable::CheckRowFocus(void)
{
int item_index_focus=WRONG_VALUE;
//--- Obtenemos la coordenada relativa Y debajo del cursor del ratón
int y=m_mouse.RelativeY(m_table);
///--- Obtenemos los índices del área local de la tabla
VisibleTableIndexes();
//--- Buscamos el foco
for(int i=m_visible_table_from_index; i<m_visible_table_to_index; i++)
{
//--- Si el foco de la fila ha cambiado
if(y>m_rows[i].m_y && y<=m_rows[i].m_y2)
{
item_index_focus=i;
break;
}
}
//--- Devolvemos el índice de la fila en el foco
return(item_index_focus);
}
Imágenes en las celdas de la tabla
A cada celda se le puede vincular varias imágenes que se podrá ir cambiando en el proceso de ejecución del programa. Vamos a añadir los campos y los métodos para establecer los márgenes de la imagen desde el lado izquierdo de la celda:
{
private:
//--- Márgenes para las imágenes desde los bordes de la celda
int m_image_x_offset;
int m_image_y_offset;
//---
public:
//--- Márgenes para las imágenes desde los bordes de la celda
void ImageXOffset(const int x_offset) { m_image_x_offset=x_offset; }
void ImageYOffset(const int y_offset) { m_image_y_offset=y_offset; }
};
Para colocar las imágenes dentro de la celda especificada, hay que pasar el array con su ubicación en el directorio local del terminal. Antes de eso, deben estar conectadas a la aplicación MQL como recursos (#resource). Para eso se utiliza el método CCanvasTable::SetImages(). Aquí, si ha sido pasado el array vacío o se ha detectado la salida fuera de los límites del array, el programa sale del método.
Si las comprobaciones han sido superadas, a los arrays de la celda se le asigna nuevo tamaño y luego, usando el método ::ResourceReadImage(), leemos en el ciclo el contenido de la imagen al array unidimensional, guardando dentro los colores de todos los píxeles. Los tamaños de la imagen se guardan en los arrays correspondientes. Serán necesarios para organizar los ciclos en los que las imágenes van a dibujarse sobre el lienzo. Por defecto, en la celda se elegirá la primera imagen del array.
{
public:
//--- Coloca las imágenes en la celda especificada
void SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[]);
};
//+------------------------------------------------------------------+
//| Coloca las imágenes en la celda especificada |
//+------------------------------------------------------------------+
void CCanvasTable::SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[])
{
int total=0;
//--- Salir si ha sido pasado el array con el tamaño cero
if((total=CheckArraySize(bmp_file_path))==WRONG_VALUE)
return;
//--- Comprobar la superación del rango
if(!CheckOutOfRange(column_index,row_index))
return;
//--- Establecer nuevo tamaño para los arrays
::ArrayResize(m_columns[column_index].m_rows[row_index].m_images,total);
::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_width,total);
::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_height,total);
//---
for(int i=0; i<total; i++)
{
Por defecto, se elige la primera imagen del array
. m_columns[column_index].m_rows[row_index].m_selected_image=0;
//--- Guardar la imagen traspasada en el array y recordar sus tamaños
if(!ResourceReadImage(bmp_file_path[i],m_columns[column_index].m_rows[row_index].m_images[i].m_image_data,
m_columns[column_index].m_rows[row_index].m_image_width[i],
m_columns[column_index].m_rows[row_index].m_image_height[i]))
{
Print(__FUNCTION__," > error: ",GetLastError());
return;
}
}
}
Para saber cuántas imágenes contiene una u otra celda, usamos el método CCanvasTable::ImagesTotal():
{
public:
//--- Devuelve el número de imágenes dentro de la celda especificada
int ImagesTotal(const uint column_index,const uint row_index);
};
//+------------------------------------------------------------------+
//| Devuelve el número de imágenes dentro de la celda especificada |
//+------------------------------------------------------------------+
int CCanvasTable::ImagesTotal(const uint column_index,const uint row_index)
{
//--- Comprobar la superación del rango
if(!CheckOutOfRange(column_index,row_index))
return(WRONG_VALUE);
//--- Devolvemos el tamaño del array de imágenes
return(::ArraySize(m_columns[column_index].m_rows[row_index].m_images));
}
Ahora veremos los métodos que van a aplicarse para dibujar las imágenes. En primer lugar, a la clase CColors ha sido añadido el nuevo método CColors::BlendColors() que permitirá mezclar correctamente el color superior e inferior, tomando en cuenta la transparencia de la imagen solapada encima. También ha sido agregado el método adicional CColors::GetA() para obtener el valor de la transparencia del color pasado.
En el método CColors::BlendColors(), los colores pasados primero se dividen en los componentes RGB, y del color superior se extrae el canal alfa. El canal alfa se convierte en los valores de cero a uno. Si en el color pasado no hay transparencia, el mezclado no se realiza. En caso si hay transparencia, cada componente de dos colores pasados se mezcla tomando en cuenta la transparencia del color superior. Después de eso, los valores de los componentes obtenidos se corrigen, en caso de salir fuera del diapasón (255).
//| Clase para trabajar con el color |
//+------------------------------------------------------------------+
class CColors
{
public:
double GetA(const color aColor);
color BlendColors(const uint lower_color,const uint upper_color);
};
//+------------------------------------------------------------------+
//| Obtención del valor del componente A |
//+------------------------------------------------------------------+
double CColors::GetA(const color aColor)
{
return(double(uchar((aColor)>>24)));
}
//+----------------------------------------------------------------------------------------+
//| Mezcla de dos colores tomando en cuenta la transparencia del color superior |
//+----------------------------------------------------------------------------------------+
color CColors::BlendColors(const uint lower_color,const uint upper_color)
{
double r1=0,g1=0,b1=0;
double r2=0,g2=0,b2=0,alpha=0;
double r3=0,g3=0,b3=0;
//--- Conversión de color en el formato ARGB
uint pixel_color=::ColorToARGB(upper_color);
//--- Obtenemos los componentes del color inferior y superior
ColorToRGB(lower_color,r1,g1,b1);
ColorToRGB(pixel_color,r2,g2,b2);
//--- Obtenemos el porciento de 0,00 a 1,00
alpha=GetA(upper_color)/255.0;
//--- Si hay transparencia
if(alpha<1.0)
{
//--- Mezclamos los componentes tomando en cuenta el canal alfa
r3=(r1*(1-alpha))+(r2*alpha);
g3=(g1*(1-alpha))+(g2*alpha);
b3=(b1*(1-alpha))+(b2*alpha);
//--- Corrección de valores obtenidos
r3=(r3>255)? 255 : r3;
g3=(g3>255)? 255 : g3;
b3=(b3>255)? 255 : b3;
}
else
{
r3=r2;
g3=g2;
b3=b2;
}
//--- Juntar los componentes obtenidos y devolver el color
return(RGBToColor(r3,g3,b3));
}
Ahora, no es tan complicado escribir el método para dibujar la imagen. Abajo se muestra el código del método CCanvasTable::DrawImage(). Hay que pasarle los índices de la celda de la tabla en la que es necesario dibujar la imagen. Al principio del método, obtenemos las coordenadas de la imagen teniendo en cuenta los márgenes, así como el índice de la celda seleccionada y sus tamaños. Luego, la imagen se muestra en el ciclo píxel por píxel. Si el píxel especificado está vacío, es decir no tiene color, se produce el cambio al siguiente. Si hay color, determinamos el color del fondo de la celda y el color del píxel actual, y luego mezclamos estos colores, tomando en cuenta la transparencia del color solapado, y dibujamos el color obtenido en el lienzo.
{
private:
//--- Dibuja la imagen dentro de la celda especificada
void DrawImage(const int column_index,const int row_index);
};
//+------------------------------------------------------------------+
//| Dibuja la imagen dentro de la celda especificada |
//+------------------------------------------------------------------+
void CCanvasTable::DrawImage(const int column_index,const int row_index)
{
//--- Cálculo de coordenadas
int x =m_columns[column_index].m_x+m_image_x_offset;
int y =m_rows[row_index].m_y+m_image_y_offset;
//--- Imagen seleccionada en la celda y sus tamaños
int selected_image =m_columns[column_index].m_rows[row_index].m_selected_image;
uint image_height =m_columns[column_index].m_rows[row_index].m_image_height[selected_image];
uint image_width =m_columns[column_index].m_rows[row_index].m_image_width[selected_image];
//--- Dibujamos
for(uint ly=0,i=0; ly<image_height; ly++)
{
for(uint lx=0; lx<image_width; lx++,i++)
{
//--- Si no hay color, ir al siguiente píxel
if(m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]<1)
continue;
//--- Obtenemos el color de la capa inferior (fondo de la celda) y el color del píxel indicado de la imagen
uint background =(row_index==m_selected_item)? m_selected_row_color : m_table.PixelGet(x+lx,y+ly);
uint pixel_color =m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i];
//--- Mezclamos los colores
uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color));
//--- Dibujamos el píxel de la imagen solapada
m_table.PixelSet(x+lx,y+ly,foreground);
}
}
}
Para dibujar todas las imágenes de la tabla a la vez, tomando en cuenta cuando es necesario dibujar solamente el área visible de la tabla, se usa el método CCanvasTable::DrawImages(). En la versión actual de la tabla, se puede dibujar las imágenes sólo si el texto en la columna está alineado por la izquierda. Aparte de eso, en cada iteración se comprueba si la imagen está colocada en la celda, así como si el array de sus píxeles no está vacío. Si todas las comprobaciones han sido superadas, llamamos al método CCanvasTable::DrawImage() para dibujar la imagen.
{
private:
//--- Dibuja todas las imágenes de la tabla
void DrawImages(void);
};
//+------------------------------------------------------------------+
//| Dibuja todas las imágenes de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::DrawImages(void)
{
//--- Para calcular las coordenadas
int x=0,y=0;
//--- Columnas
for(int c=0; c<m_columns_total; c++)
{
//--- Si la alineación del texto no ha sido hecha por la izquierda, ir a la siguiente columna
if(m_columns[c].m_text_align!=ALIGN_LEFT)
continue;
//--- Filas
for(int r=m_visible_table_from_index; r<m_visible_table_to_index; r++)
{
//--- Ir al siguiente si en esta celda no hay imágenes
if(ImagesTotal(c,r)<1)
continue;
//--- Imagen seleccionada en la celda (por defecto, está seleccionada la primera [0])
int selected_image=m_columns[c].m_rows[r].m_selected_image;
//--- Ir al siguiente si el array de los píxeles está vacío
if(::ArraySize(m_columns[c].m_rows[r].m_images[selected_image].m_image_data)<1)
continue;
//--- Dibujar la imagen
DrawImage(c,r);
}
}
}
En la captura de pantalla de abajo se muestra la tabla con imágenes dentro de las celdas:
Fig. 1. Tabla con imágenes dentro de las celdas.
Resalto de la fila de la tabla al situar el cursor encima
Para que las filas de la tabla dibujada se resalten al situar el cursor encima de ellas, vamos a necesitar los campos y métodos adicionales. Utilice el método CCanvasTable::LightsHover() para activar el modo del resalto. El color de la fila puede establecerse a través del métod CCanvasTable::CellColorHover().
{
private:
//--- Color de las celdas en diferentes estados
color m_cell_color;
color m_cell_color_hover;
//--- Modo para resaltar la fila al situar el cursor encima
bool m_lights_hover;
//---
public:
//--- Color de las celdas en diferentes estados
void CellColor(const color clr) { m_cell_color=clr; }
void CellColorHover(const color clr) { m_cell_color_hover=clr; }
//--- Modo para resaltar las filas al situar el cursor encima
void LightsHover(const bool flag) { m_lights_hover=flag; }
};
Para resaltar la fila, no es necesario redibujar la tabla entera una y otra vez al mover el cursor. Es más, no se recomienda insistentemente hacerlo, porque eso ralentizará considerablemente la aplicación y ocupará muchísimos recursos de la CPU. Con la primera entrada del cursor dentro del área de la tabla, será suficiente buscar el foco de la fila sólo una vez (repasando en el ciclo el array entero de las filas). Para eso se utiliza el método CCanvasTable::CheckRowFocus(). Después de encontrar el foco y guardar el índice de la fila, al mover el cursor sólo hay que comprobar si ha cambiado el foco sobre la fila con el índice guardado. El algoritmo descrito está implementado en el método CCanvasTable::ChangeRowsColor(), cuyo código se muestra más abajo. Para cambiar el color de la fila, se usa el método CCanvasTable::RedrawRow(), cuyo código conoceremos más tarde. El método CCanvasTable::ChangeRowsColor() se invoca dentro del método CCanvasTable::ChangeObjectsColor() para el cambio de los objetos de la tabla.
{
private:
//--- Para determinar el foco de la fila
int m_item_index_focus;
//--- Para determinar el momento cuando el cursor pasa de una fila a otra
int m_prev_item_index_focus;
//---
private:
//--- Cambio de color de las filas al situar el cursor encima
void ChangeRowsColor(void);
};
//+------------------------------------------------------------------+
//| Cambio de color de las filas al situar el cursor encima |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeRowsColor(void)
{
//--- Salir si el resalto de las filas al situar el cursor encima está desactivado
if(!m_lights_hover)
return;
//--- Si no se encuentra en el foco
if(!m_table.MouseFocus())
{
//--- Si todavía no está indicado que no se encuentra en el foco
if(m_prev_item_index_focus!=WRONG_VALUE)
{
m_item_index_focus=WRONG_VALUE;
//--- Cambiar el color
RedrawRow();
m_table.Update();
//--- Resetear el foco
m_prev_item_index_focus=WRONG_VALUE;
}
}
//--- Si se encuentra en el foco
else
{
} //--- Comprobar el foco sobre las filas
if(m_item_index_focus==WRONG_VALUE)
{
//--- Obtenemos el índice de la fila en el foco
m_item_index_focus=CheckRowFocus();
//--- Cambiar el color de la fila
RedrawRow();
m_table.Update();
//--- Guardar como el índice anterior en el foco
m_prev_item_index_focus=m_item_index_focus;
return;
}
//--- Obtenemos la coordenada relativa Y debajo del cursor del ratón
int y=m_mouse.RelativeY(m_table);
//--- Comprobación del foco
bool condition=(y>m_rows[m_item_index_focus].m_y && y<=m_rows[m_item_index_focus].m_y2);
//--- Si el foco ha cambiado
if(!condition)
{
//--- Obtenemos el índice de la fila en el foco
m_item_index_focus=CheckRowFocus();
//--- Cambiar el color de la fila
RedrawRow();
m_table.Update();
//--- Guardar como el índice anterior en el foco
m_prev_item_index_focus=m_item_index_focus;
}
}
}
El método para redibujar rápidamente una fila de la tabla CCanvasTable::RedrawRow() trabaja en dos modos:
- al seleccionar la fila
- en el modo para resaltar la fila al situar el cursor encima.
Para indicar el modo, es necesario pasar el valor correspondiente del argumento del método. Por defecto, el argumento tiene asignado el valor false, lo que significa el uso del método en el modo del resalto de las filas de la tabla. Para ambos modos, en la clase hay campos especiales para determinar la fila seleccionada/resaltada actual y anterior. De esta manera, para marcar la siguiente fila, se redibuja sólo la fila anterior y la fila actual, en vez de la tabla entera.
El programa sale del método si los índices no están determinados (WRONG_VALUE). Luego, hay que determinar el número de índices determinados. Si es la primera entrada en la tabla y ha sido determinado un solo índice (actual), el color será cambiado, por consiguiente, sólo para una fila, la fila actual. Si volvemos a entrar en la tabla, el color va a cambiarse para las dos filas (actual y anterior).
Ahora, hay que determinar en qué sucesión va a cambiar el color de las filas. Si el índice de la fila actual es mayor que el de la fila anterior, eso significa que el cursor se desplazará hacia abajo. Entonces, primero cambiamos el color en el índice anterior, y luego en el actual. De lo contrario, hacemos al revés. Aquí también se toma en cuenta el momento de la salida del área de la tabla, cuando el índice de la fila actual no está determinado, y el índice de la fila anterior todavía existe.
Después de que todas las variables locales para el trabajo hayan sido inicializadas, se dibuja el fondo de las filas, la cuadrícula, las imágenes y el texto, en estricta sucesión.
{
private:
//--- Redibuja la fila indicada de acuerdo con el modo especificado
void RedrawRow(const bool is_selected_row=false);
};
//+------------------------------------------------------------------+
//| Redibuja la fila indicada de acuerdo con el modo especificado |
//+------------------------------------------------------------------+
void CCanvasTable::RedrawRow(const bool is_selected_row=false)
{
//--- Indice actual y anterior de las filas
int item_index =WRONG_VALUE;
int prev_item_index =WRONG_VALUE;
//--- Inicialización de los índices de las filas respecto al modo especificado
if(is_selected_row)
{
item_index =m_selected_item;
prev_item_index =m_prev_selected_item;
}
else
{
item_index =m_item_index_focus;
prev_item_index =m_prev_item_index_focus;
}
//--- Salir si los índices no están definidos
if(prev_item_index==WRONG_VALUE && item_index==WRONG_VALUE)
return;
//--- Número de las filas y columnas para el dibujo
int rows_total =(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE)? 2 : 1;
int columns_total =m_columns_total-1;
//--- Coordenadas
int x1=1,x2=m_table_x_size;
int y1[2]={0},y2[2]={0};
//--- Array para los valores en una determinada sucesión
int indexes[2];
//--- Si (1) el cursor se ha desplazado hacia abajo o (2) se encuentra aquí por primera vez
if(item_index>m_prev_item_index_focus || item_index==WRONG_VALUE)
{
indexes[0]=(item_index==WRONG_VALUE || prev_item_index!=WRONG_VALUE)? prev_item_index : item_index;
indexes[1]=item_index;
}
//--- Si el cursor se ha desplazado hacia arriba
else
{
indexes[0]=item_index;
indexes[1]=prev_item_index;
}
//--- Dibujamos el fondo de las filas
for(int r=0; r<rows_total; r++)
{
//--- Cálculo de las coordenadas del límite superior e inferior de la fila
y1[r]=m_rows[indexes[r]].m_y+1;
y2[r]=m_rows[indexes[r]].m_y2-1;
//--- Determinamos el foco en la fila respecto al modo del resalto
bool is_item_focus=false;
if(!m_lights_hover)
is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE);
else
is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index);
//--- Dibujar el fondo de la fila
m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],is_item_focus));
}
//--- Color de la cuadrícula
uint clr=::ColorToARGB(m_grid_color);
//--- Dibujamos los bordes
for(int r=0; r<rows_total; r++)
{
for(int c=0; c<columns_total; c++)
m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],clr);
}
//--- Dibujamos las imágenes
for(int r=0; r<rows_total; r++)
{
for(int c=0; c<m_columns_total; c++)
{
//--- Dibujamos la imagen si (1) ella se encuentra en esta celda y si (2) el texto se alinea por la izquierda en esta columna
if(ImagesTotal(c,r)>0 && m_columns[c].m_text_align==ALIGN_LEFT)
DrawImage(c,indexes[r]);
}
}
//--- Para calcular las coordenadas
int x=0,y=0;
//--- Modo de alineación del texto
uint text_align=0;
//--- Dibujamos el texto
for(int c=0; c<m_columns_total; c++)
{
//--- Obtenemos (1) la coordenada X del texto y (2) el modo de alineación del texto
x =TextX(c);
text_align =TextAlign(c,TA_TOP);
//---
for(int r=0; r<rows_total; r++)
{
//--- (1) Calcular la coordenada y (2) dibujar e texto
y=m_rows[indexes[r]].m_y+m_text_y_offset;
m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align);
}
}
}
Al final, obtenemos el siguiente resultado:
Fig. 2. Demostración del resalto de las filas de la tabla al situar el cursor encima.
Métodos para redibujar rápidamente la celda de la tabla
Hemos considerado el método para redibujar rápidamente las filas de la tabla. Ahora voy a demostrar los métodos para el redibujo rápido de la celda. Por ejemplo, si hace falta cambiar el texto, su color o la imagen en una celda de la tabla, será suficiente redibujar sólo esta celda, en vez de la tabla entera. Para eso se utiliza el método privado CCanvasTable::RedrawCell(). Se redibuja sólo el contenido de la celda, y el borde no se actualiza. El color del fondo se determina tomando en cuenta el modo del resalto, si éste está activado. Una vez determinados los valores e inicializadas las variables locales, en la celda se dibuja el fondo, la imagen (si está establecida y la alineación es por la izquierda) y el texto.
{
private:
//--- Redibuja la celda especificada de la tabla
void RedrawCell(const int column_index,const int row_index);
};
//+------------------------------------------------------------------+
//| Redibuja la celda especificada de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::RedrawCell(const int column_index,const int row_index)
{
//--- Coordenadas
int x1=m_columns[column_index].m_x+1;
int x2=m_columns[column_index].m_x2-1;
int y1=m_rows[row_index].m_y+1;
int y2=m_rows[row_index].m_y2-1;
//--- Para calcular las coordenadas
int x=0,y=0;
//--- Para comprobar el foco
bool is_row_focus=false;
//--- Si el modo del resalto de las filas está activado
if(m_lights_hover)
{
//--- (1) Obtenemos la coordenada relativa Y del cursor y (2) el foco en la fila especificada
y=m_mouse.RelativeY(m_table);
is_row_focus=(y>m_rows[row_index].m_y && y<=m_rows[row_index].m_y2);
}
//--- Dibujar el fondo de la celda
m_table.FillRectangle(x1,y1,x2,y2,RowColorCurrent(row_index,is_row_focus));
//--- Dibujamos la imagen si (1) ella se encuentra en esta celda y si (2) el texto se alinea en esta columna por la izquierda
if(ImagesTotal(column_index,row_index)>0 && m_columns[column_index].m_text_align==ALIGN_LEFT)
DrawImage(column_index,row_index);
//--- Obtenemos el modo de alineación del texto
uint text_align=TextAlign(column_index,TA_TOP);
//--- Dibujamos el texto
for(int c=0; c<m_columns_total; c++)
{
//--- Obtenemos la coordenada X para el texto
x=TextX(c);
//--- Detenemos el ciclo
if(c==column_index)
break;
}
//--- (1) Calcular la coordenada Y y (2) dibujar el texto
y=y1+m_text_y_offset-1;
m_table.TextOut(x,y,m_columns[column_index].m_rows[row_index].m_short_text,TextColor(column_index,row_index),text_align);
}
Ahora vamos a analizar los métodos a través de los cuales se puede cambiar el texto, color del texto y la imagen (seleccionar entre las establecidas) dentro de la celda. Para establecer el texto y su color, hay que usar los métodos públicos CCanvasTable::SetValue() y CCanvasTable::TextColor(). A estos métodos se les pasan los índices de la celda (columna y fila) y el valor que hay que establecer. Para el método CCanvasTable::SetValue() es un valor string que va a mostrarse en la celda. Aquí, en los campos correspondientes de la estructura de la celda (CTCell) se guarda la línea entera que ha sido pasada y su versión reducida si la línea entera no cabe en la celda por su ancho. Para el método CCanvasTable::TextColor(), es necesario pasar el color del texto. En ambos métodos, en calidad del cuarto parámetro, se puede indicar si es necesario redibujar la celda enseguida o eso se hará más tarde usando la llamada al método CCanvasTable::UpdateTable().
{
private:
//--- Establece el valor en la celda especificada de la tabla
void SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false);
//--- Establece el color del texto en la celda especificada de la tabla
void TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false);
};
//+------------------------------------------------------------------+
//| Rellena el array según los índices especificados |
//+------------------------------------------------------------------+
void CCanvasTable::SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false)
{
//--- Comprobar la superación del rango
if(!CheckOutOfRange(column_index,row_index))
return;
//--- Colocar el valor en el array
m_columns[column_index].m_rows[row_index].m_full_text=value;
//--- Corregir y guardar el texto si no cabe en la celda
m_columns[column_index].m_rows[row_index].m_short_text=CorrectingText(column_index,row_index);
//--- Redibujar la celda si se especifica
if(redraw)
RedrawCell(column_index,row_index);
}
//+------------------------------------------------------------------+
//| Llena el array con el color del texto |
//+------------------------------------------------------------------+
void CCanvasTable::TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false)
{
//--- Comprobar la superación del rango
if(!CheckOutOfRange(column_index,row_index))
return;
//--- Establecemos el color del texto en el array común
m_columns[column_index].m_rows[row_index].m_text_color=clr;
//--- Redibujar la celda si se especifica
if(redraw)
RedrawCell(column_index,row_index);
}
Para cambiar la imagen de la celda, puede usar el método CCanvasTable::ChangeImage(). Como el tercer parámetro, aquí se debe indicar el índice de la imagen a la que queremos cambiar. Además, igual que en los métodos para el cambio de las propiedades de la celda que hemos descrito antes, se puede indicar el redibujo ahora o más tarde.
{
private:
//--- Cambia la imagen en la celda especificada
void ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false);
};
//+------------------------------------------------------------------+
//| Cambia la imagen en la celda especificada |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false)
{
//--- Comprobar la superación del rango
if(!CheckOutOfRange(column_index,row_index))
return;
//--- Obtenemos el número de imágenes de la celda
int images_total=ImagesTotal(column_index,row_index);
//--- Salir si (1) no hay imágenes o (2) se supera el rango
if(images_total==WRONG_VALUE || image_index>=(uint)images_total)
return;
//--- Salir si la imagen especificada coincide con la seleccionada
if(image_index==m_columns[column_index].m_rows[row_index].m_selected_image)
return;
//--- Guardar el índice de la imagen seleccionada de la celda
m_columns[column_index].m_rows[row_index].m_selected_image=(int)image_index;
//--- Redibujar la celda si se especifica
if(redraw)
RedrawCell(column_index,row_index);
}
Vamos a necesitar otro método público para redibujar la tabla entera, CCanvasTable::UpdateTable(). Puede invocarse en dos modos:
- Cuando es necesario simplemente actualizar la tabla para mostrar los últimos cambios con los métodos descritos antes.
- Cuando es necesario redibujar completamente la tabla si han sido introducidos algunos cambios.
Por defecto, el único argumento del método tiene asignado el valor false, lo que significa la actualización sin la necesidad de redibujar.
{
private:
//--- Actualización de la tabla
void UpdateTable(const bool redraw=false);
};
//+------------------------------------------------------------------+
//| Actualización de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::UpdateTable(const bool redraw=false)
{
//--- Redibujar la tabla si está especificado
if(redraw)
DrawTable();
//--- Actualizar la tabla
m_table.Update();
}
Abajo se muestra el resultado de nuestro trabajo:
Fig. 3. Demostración de nuevas posibilidades de la tabla dibujada.
Se puede descargar el EA que demuestra este resultado de los archivos adjuntos al artículo. Durante la ejecución del programa, las imágenes de todas las celdas de la tabla (5 columnas y 30 filas) van a cambiar con una frecuencia de 100 milisegundos. En la captura de pantalla de abajo se muestra la carga sobre la CPU sin la interacción del usuario con la interfaz gráfica de la aplicación MQL. La carga sobre la CPU con la frecuencia de la actualización de 100 milisegundos no supera el 3%.
Fig. 4. Carga sobre la CPU durante la ejecución de la aplicación MQL de prueba.
Aplicación para la prueba del control
La versión actual de la tabla dibujada ya es bastante «inteligente» para crear las mismas tablas, por ejemplo, en la ventana «Observación del Mercado». Intentaremos demostrarlo. Como ejemplo, crearemos una tabla que se compone de 5 columnas y 25 filas. Serán 25 símbolo que están presentes en el servidor MetaQuotes-Demo. Los datos serán los siguientes:
- Symbol – instrumentos financieros (pares de divisas).
- Bid – precios Bid.
- Ask – precios Ask.
- Spread (!) – diferencia entre los precios Bid y Ask.
- Time – hora de la llegada de la última cotización.
Para mostrar la última dirección en el cambio del precio, vamos a preparar las mismas imágenes que figuran en la tabla de la ventana «Observación del Mercado» La primera actualización de las celdas de la tabla va a ejecutarse inmediatamente en el método de la creación del control y realizarse con la llamada al método auxiliar de la clase personalizada CProgram::InitializingTable().
//| Clase para crear la aplicación |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
{
private:
//--- Inicialización de la tabla
void InitializingTable(void);
};
//+------------------------------------------------------------------+
//| Inicialización de la tabla |
//+------------------------------------------------------------------+
void CProgram::InitializingTable(void)
{
//--- Array de los nombres de encabezados
string text_headers[COLUMNS1_TOTAL]={"Symbol","Bid","Ask","!","Time"};
//--- Array de los símbolos
string text_array[25]=
{
"AUDUSD","GBPUSD","EURUSD","USDCAD","USDCHF","USDJPY","NZDUSD","USDSEK","USDHKD","USDMXN",
"USDZAR","USDTRY","GBPAUD","AUDCAD","CADCHF","EURAUD","GBPCHF","GBPJPY","NZDJPY","AUDJPY",
"EURJPY","EURCHF","EURGBP","AUDCHF","CHFJPY"
//--- arrays string image_array[3]=
{
"::Images\\EasyAndFastGUI\\Icons\\bmp16\\circle_gray.bmp",
"::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp",
"::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp"
};
//---
for(int c=0; c<COLUMNS1_TOTAL; c++)
{
//--- Establecemos los nombres para encabezados
m_canvas_table.SetHeaderText(c,text_headers[c]);
//---
for(int r=0; r<ROWS1_TOTAL; r++)
{
//--- Establecemos imágenes
m_canvas_table.SetImages(c,r,image_array);
//--- Establecemos los nombres de símbolo
if(c<1)
m_canvas_table.SetValue(c,r,text_array[r]);
//--- Valor predefinido para todas las celdas
else
m_canvas_table.SetValue(c,r,"-");
}
}
}
En el proceso del trabajo, los valores de las celdas de nuestra tabla van a actualizarse cada 16 milisegundos usando el temporizador. Para eso, ha sido creado otro método auxiliar, CProgram::UpdateTable(). Aquí el programa sale del método si es el fin de semana (sábado o domingo). Luego, en el ciclo doble se hace el repaso de todas las columnas y filas de la tabla donde obtenemos dos últimos ticks para cada símbolo, y a base del análisis del cambio de precios establecemos los valores correspondientes.
{
private:
//--- Inicialización de la tabla
void InitializingTable(void);
};
//+------------------------------------------------------------------+
//| Actualización de los valores de la tabla |
//+------------------------------------------------------------------+
void CProgram::UpdateTable(void)
{
MqlDateTime check_time;
::TimeToStruct(::TimeTradeServer(),check_time);
//--- Salir si no es sábado o domingo
if(check_time.day_of_week==0 || check_time.day_of_week==6)
return;
//---
for(int c=0; c<m_canvas_table.ColumnsTotal(); c++)
{
for(int r=0; r<m_canvas_table.RowsTotal(); r++)
{
//--- Símbolo para el que obtenemos los daros
string symbol=m_canvas_table.GetValue(0,r);
//--- Obtenemos los datos de dos últimos ticks
MqlTick ticks[];
if(::CopyTicks(symbol,ticks,COPY_TICKS_ALL,0,2)<2)
continue;
//--- Establecer el array, como series temporales
::ArraySetAsSeries(ticks,true);
//--- Columna de los símbolos - Symbol. Determinamos la dirección del precio.
if(c==0)
{
int index=0;
//--- Si los precios no han cambiado
if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid)
index=0;
//--- Si el precio Bid ha cambiado para arriba
else if(ticks[0].bid>ticks[1].bid)
index=1;
//--- Si el precio Bid ha cambiado para abajo
else if(ticks[0].bid<ticks[1].bid)
index=2;
//--- Establecemos la imagen correspondiente
m_canvas_table.ChangeImage(c,r,index,true);
}
else
{
//--- Columna de la diferencia de precios - Spread (!)
if(c==3)
{
//--- Obtenemos y establecemos el tamaño del spread en puntos
int spread=(int)::SymbolInfoInteger(symbol,SYMBOL_SPREAD);
m_canvas_table.SetValue(c,r,string(spread),true);
continue;
}
//--- Obtenemos el número de dígitos después de la coma
int digit=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS);
//--- Columna de precios Bid
if(c==1)
{
m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].bid,digit));
//--- Si el precio ha cambiado, establecemos el color correspondiente para la dirección
if(ticks[0].bid!=ticks[1].bid)
m_canvas_table.TextColor(c,r,(ticks[0].bid<ticks[1].bid)? clrRed : clrBlue,true);
//---
continue;
}
//--- Columna de precios Ask
if(c==2)
{
m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].ask,digit));
//--- Si el precio ha cambiado, establecemos el color correspondiente para la dirección
if(ticks[0].ask!=ticks[1].ask)
m_canvas_table.TextColor(c,r,(ticks[0].ask<ticks[1].ask)? clrRed : clrBlue,true);
//---
continue;
}
//--- Columna de la hora de la última llegada de los precios del símbolo
if(c==4)
{
long time =::SymbolInfoInteger(symbol,SYMBOL_TIME);
string time_msc =::IntegerToString(ticks[0].time_msc);
int length =::StringLen(time_msc);
string msc =::StringSubstr(time_msc,length-3,3);
string str =::TimeToString(time,TIME_MINUTES|TIME_SECONDS)+"."+msc;
//---
color clr=clrBlack;
//--- Si los precios no han cambiado
if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid)
clr=clrBlack;
//--- Si el precio Bid ha cambiado para arriba
else if(ticks[0].bid>ticks[1].bid)
clr=clrBlue;
//--- Si el precio Bid ha cambiado para abajo
else if(ticks[0].bid<ticks[1].bid)
clr=clrRed;
//--- Establecemos el valor y el color del texto
m_canvas_table.SetValue(c,r,str);
m_canvas_table.TextColor(c,r,clr,true);
continue;
}
}
}
}
//--- Actualizar la tabla
m_canvas_table.UpdateTable();
}
Este es el resultado que hemos obtenido:
Fig. 5. Comparación entre los datos en la ventana «Observación del Mercado» y el análogo del usuario.
Puede descargar esta aplicación de prueba al final del artículo para estudiarla más detalladamente.
Conclusión
En esta fase del desarrollo de la librería para la creación de las interfaces gráficas, su esquema general tiene el siguiente aspecto.
Fig. 6. Estructura de la librería en la fase actual del desarrollo.
Abajo puede descargar la última versión de la librería y los archivos para las pruebas.
Si le surgen algunas preguntas sobre el uso del material de estos archivos, puede dirigirse a la descripción detallada del proceso de desarrollo de la librería en uno de los artículos de esta serie, o bien hacer su pregunta en los comentarios para el artículo.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/3042
- 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