
Interfaces gráficas X: Nuevas posibilidades para la tabla dibujada (build 9)
Índice
Introducción
El primer artículo de la serie nos cuenta con más detalles para qué sirve esta librería: Interfaces gráficas I: Preparación de la estructura de la 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.
Hasta este momento, el tipo más desarrollado de las tablas en nuestra librería fue el tipo CTable. Esta tabla se reúne de los campos de edición tipo OBJ_EDIT y su desarrollo posterior ya resulta problemático. Por ejemplo, resulta complicado implementar el cambio manual del ancho de las columnas con la captura de los encabezados, es que no podemos controlar el área de la visibilidad de los objetos gráficos separados en la tabla. Aquí ya hemos alcanzado el límite de nuestras posibilidades.
Por eso, en esta fase del desarrollo de nuestra librería, el diseño de una tabla dibujada tipo CCanvasTable resulta más prometedor. Usted puede leer sobre las versiones anteriores y las actualizaciones de la tabla dibujada en los siguientes artículos:
- Interfaces gráficas VII: Controles “Tablas” (Capítulo 1).
- Interfaces gráficas X: Actualizaciones para la librería Easy And Fast (build 3)
En la captura de pantalla de abajo, se muestra la apariencia de la última versión de la tabla dibujada en la que hemos parado. Como podemos observar, es absolutamente inerte en este momento. Simplemente es una cuadrícula compuesta de las celdas de la tabla con los datos dentro de ellas. Podemos indicar el modo de alineación para las celdas. Además de las barras de desplazamiento y el modo del ajuste automático de los tamaños al formulario, en esta tabla no hay ninguna otra interactividad.
Fig. 1. Versión anterior de la tabla dibujada.
Vamos a solucionar esta situación y completar nuestra tabla dibujada con nuevas posibilidades. En esta actualización, nos ocuparemos de las siguientes funciones:
- Formateo en el estilo «Cebra»
- Seleccionar una fila de la tabla y quitar la selección al volver a pulsarla
- Han sido agregados los encabezados para las columnas con posibilidad de cambiar su color al situar el cursor encima y hacer clic en ellos
- Ajuste automático del texto al ancho de las columnas en caso si en la celda falta espacio
- Posibilidad de cambiar el ancho de los encabezados de cada columna agarrando y arrastrando su borde
Formateo en el estilo «Cebra»
En uno de los últimos artículos, hemos añadido el modo «Cebra» a la tabla CTable. Este modo permite orientarse mejor en una tabla que contiene muchas celdas. Vamos a implementar este modo también en una tabla dibujada.
Para activar el modo, usamos el método CCanvasTable::IsZebraFormatRows(). Le pasaremos el segundo color del estilo, y el color común de las celdas nos servirá del primer color.
//| Clase par crear la tabla dibujada |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
{
private:
//--- Modo del formateo en el estilo «Cebra»
color m_is_zebra_format_rows;
//---
public:
//--- Formateo en el estilo «Cebra»
void IsZebraFormatRows(const color clr) { m_is_zebra_format_rows=clr; }
};
Los métodos para la visualización de este estilo son diferentes en las tablas de diferentes tipos. En caso de CCanvasTable, si usamos el modo habitual, el fondo de la tabla (lienzo para el dibujo) se colorea completamente con el color común de las celdas. Al ser activado el modo «Cebra», se inicia el trabajo del ciclo. En cada de sus iteraciones se calculan las coordenadas para cada fila, y las áreas se colorean consecutivamente con dos colores. De eso se encarga el método FillRectangle() que se utiliza para dibujar los rectángulos coloreados .
{
public:
//--- Dibuja el fondo de las filas de la tabla
void DrawRows(void);
};
//+------------------------------------------------------------------+
//| Dibuja el fondo de las filas de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::DrawRows(void)
{
//--- Si el modo del formateo en el estilo «Cebra» está desactivado
if(m_is_zebra_format_rows==clrNONE)
{
//--- Colorear el lienzo con un color
m_table.Erase(::ColorToARGB(m_cell_color));
return;
}
//--- Coordenadas de los encabezados
int x1=0,x2=m_table_x_size;
int y1=0,y2=0;
//--- Formateo en el estilo «Cebra»
for(int r=0; r<m_rows_total; r++)
{
//--- Cálculo de coordenadas
y1=(r*m_cell_y_size)-r;
y2=y1+m_cell_y_size;
//--- Color de la fila
uint clr=::ColorToARGB((r%2!=0)? m_is_zebra_format_rows : m_cell_color);
//--- Dibujar el fondo de la fila
m_table.FillRectangle(x1,y1,x2,y2,clr);
}
}
Usted puede establecer el color de las filas que quiera. Como resultado, la tabla dibujada en el modo «Cebra» tendrá el siguiente aspecto:
Fig. 2. Tabla dibujada en el modo del formateo en el estilo «Cebra».
Seleccionar y quitar la selección de las filas de la tabla
Para seleccionar una fila, nos harán falta los campos adicionales y los métodos para almacenar y establecer los siguientes parámetros:
- Colores del fondo y del texto de la fila seleccionada
- Indice y texto
{
private:
//--- Color del (1) fondo y del (2) texto de la fila seleccionada
color m_selected_row_color;
color m_selected_row_text_color;
//--- (1) Índice y (2) texto de la fila seleccionada
int m_selected_item;
string m_selected_item_text;
//---
public:
//--- Devuelve el (1) índice y el (2) texto de la fila seleccionada en la tabla
int SelectedItem(void) const { return(m_selected_item); }
string SelectedItemText(void) const { return(m_selected_item_text); }
//---
private:
//--- Dibuja el fondo de las filas de la tabla
void DrawRows(void);
};
Usando el método CCanvasTable::SelectableRow(), se puede activar/desactivar el modo de selección de la fila:
{
private:
//--- Modo de la línea seleccionada
bool m_selectable_row;
//---
public:
//--- Selección de la fila
void SelectableRow(const bool flag) { m_selectable_row=flag; }
};
Para seleccionar una fila, hace falta un método separado para el dibujo del área especificada por el usuario. Abajo se muestra el código del método CCanvasTable::DrawSelectedRow(). Ahí se calculan las coordenadas para el área seleccionada en el lienzo, según las cuales se dibuja el rectángulo coloreado.
{
private:
//--- Dibuja la fila seleccionada
void DrawSelectedRow(void);
};
//+------------------------------------------------------------------+
//| Dibuja la fila seleccionada |
//+------------------------------------------------------------------+
void CCanvasTable::DrawSelectedRow(void)
{
//--- Establecemos las coordenadas iniciales para comprobar la condición
int y_offset=(m_selected_item*m_cell_y_size)-m_selected_item;
//--- Coordenadas
int x1=0,x2=0,y1=0,y2=0;
//---
x1=0;
y1=y_offset;
x2=m_table_x_size;
y2=y_offset+m_cell_y_size-1;
//--- Dibujar el rectángulo con el relleno
m_table.FillRectangle(x1,y1,x2,y2,::ColorToARGB(m_selected_row_color));
}
Al redibujar el texto, se usa el método auxiliar CCanvasTable::TextColor() que determina el color del texto en las celdas:
{
private:
//--- Devuelve el color del texto de la celda
uint TextColor(const int row_index);
};
//+------------------------------------------------------------------+
//| Devuelve el color del texto de la celda |
//+------------------------------------------------------------------+
uint CCanvasTable::TextColor(const int row_index)
{
uint clr=::ColorToARGB((row_index==m_selected_item)? m_selected_row_text_color : m_cell_text_color);
//--- Devolver el color del encabezado
return(clr);
}
Para seleccionar una fila de la tabla, hay que hacer clic izquierdo en ella. Para eso vamos a necesitar el método CCanvasTable::OnClickTable() que va a invocarse en el manejador de eventos del control por el identificador CHARTEVENT_OBJECT_CLICK.
Al principio del método, hay que superar algunas comprobaciones. El programa saldrá del método si:
- el modo de selección de la fila está desactivado;
- la barra de desplazamiento se encuentra en estado activo;
- el clic no ha sido hecho en la tabla.
Si las comprobaciones han sido superadas, es necesario obtener el desplazamiento actual desde el punto extremo del lienzo y la coordenada Y del cursor para calcular la coordenada de la pulsación. Después de eso, determinamos en el ciclo la fila de la pulsación. Luego, una vez encontrada esta fila, hay que comprobar si está seleccionada en este momento, y si es así, quitar la selección. Si la fila se selecciona, hay que guardar su índice y el texto de la primera columna. Después de terminar la búsqueda de la fila en el ciclo, la tabla se redibuja. Se envía el mensaje que incluye lo siguiente:
- identificador del evento ON_CLICK_LIST_ITEM
- identificador del control
- índice de la fila seleccionada
- texto de la fila seleccionada.
{
private:
//--- Procesamiento del clic en el control
bool OnClickTable(const string clicked_object);
};
//+------------------------------------------------------------------+
//| Procesamiento del clic en el control |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickTable(const string clicked_object)
{
//--- Salir si el modo de selección de la fila está desactivado
if(!m_selectable_row)
return(false);
//--- Salimos si la barra de desplazamiento se encuentra en modo activo
if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
return(false);
//--- Salimos si el nombre del objeto no coincide
if(m_table.Name()!=clicked_object)
return(false);
//--- Obtenemos el desplazamiento por los ejes X y Y
int xoffset=(int)m_table.GetInteger(OBJPROP_XOFFSET);
int yoffset=(int)m_table.GetInteger(OBJPROP_YOFFSET);
//--- Determinamos las coordenadas para el campo de edición debajo del cursor del ratón
int y=m_mouse.Y()-m_table.Y()+yoffset;
//--- Determinamos la fila que ha sido pulsada
for(int r=0; r<m_rows_total; r++)
{
//--- Establecemos las coordenadas iniciales para comprobar la condición
int y_offset=(r*m_cell_y_size)-r;
//--- Comprobación de la condición en el eje Y
bool y_pos_check=(y>=y_offset && y<y_offset+m_cell_y_size);
//--- Si la pulsación ha sido hecha en esta línea, vamos a la siguiente
if(!y_pos_check)
continue;
//--- Si hemos pulsado en una fila ya seleccionada, quitar la selección
if(r==m_selected_item)
{
m_selected_item =WRONG_VALUE;
m_selected_item_text ="";
break;
}
//--- Guardamos el índice de la fila
m_selected_item =r;
m_selected_item_text =m_vcolumns[0].m_vrows[r];
break;
}
//--- Dibujar la tabla
DrawTable();
//--- Enviamos el mensaje sobre ello
::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,m_selected_item_text);
return(true);
}
La tabla dibujada con una fila seleccionada es así:
Fig. 3. Demostración de la selección y deselección de una fila en la tabla dibujada.
Encabezados para las columnas
Cualquier tabla sin encabezados de las columnas parece a una pieza bruta. En este tipo de las tablas también vamos a dibujar los encabezados, pero lo haremos en un lienzo separado. Para eso, en la clase CCanvasTable se incluye otra instancia de la clase CRectCanvas y se crea un método separado para la creación del lienzo. No vamos a mostrar aquí el código de este método: es casi el mismo que el método para la creación de la tabla. Su diferencia consiste en los tamaños que establecemos y en la ubicación del objeto.
{
private:
//--- Objetos para crear la tabla
CRectCanvas m_headers;
//---
private:
bool CreateHeaders(void);
};
Ahora, veremos las propiedades referentes a los encabezados de las columnas. Se puede configurarlas antes de crear la tabla.
- Modo de visualización de los encabezados de la tabla.
- Tamaño (alto) de los encabezados.
- Color del fondo de los encabezados en diferentes estados.
- Color del texto de encabezados.
Campos y métodos referentes a estas propiedades:
{
private:
//--- Modo de visualización de los encabezados de la tabla
bool m_show_headers;
//--- Tamaño (alto) de los encabezados
int m_header_y_size;
//--- Color de los encabezados (fondo) en diferentes estados
color m_headers_color;
color m_headers_color_hover;
color m_headers_color_pressed;
//--- Color del texto de encabezados
color m_headers_text_color;
//---
public:
//--- (1) Modo de visualización de los encabezados, alto de los (2) encabezados
void ShowHeaders(const bool flag) { m_show_headers=flag; }
void HeaderYSize(const int y_size) { m_header_y_size=y_size; }
//--- Colores del (1) fondo y del (2) texto de los encabezados
void HeadersColor(const color clr) { m_headers_color=clr; }
void HeadersColorHover(const color clr) { m_headers_color_hover=clr; }
void HeadersColorPressed(const color clr) { m_headers_color_pressed=clr; }
void HeadersTextColor(const color clr) { m_headers_text_color=clr; }
};
Necesitamos un método que va a establecer los nombres de los encabezados. Aparte de eso, necesitaremos un array en el que van a almacenarse estos valores. El tamaño de este array será igual al número de las columnas y va a establecerse en el mismo método CCanvasTable::TableSize(), durante el establecimiento de los tamaños de la tabla.
{
private:
//--- Texto de los encabezados
string m_header_text[];
//---
public:
//--- Establecimiento del texto en el encabezado indicado
void SetHeaderText(const int column_index,const string value);
};
//+------------------------------------------------------------------+
//| Rellena el array de encabezados según el índice especificado |
//+------------------------------------------------------------------+
void CCanvasTable::SetHeaderText(const uint column_index,const string value)
{
//--- Comprobar la superación del rango de las columnas
uint csize=::ArraySize(m_vcolumns);
if(csize<1 || column_index>=csize)
return;
//--- Colocar el valor en el array
m_header_text[column_index]=value;
}
La alineación del texto en las celdas y encabezados se hará a través del método común CCanvasTable::TextAlign(). El modo de alineación en las celdas de la tabla y en los encabezados coincide por el eje X, mientras que, en caso del eje Y, este modo se establece con un valor pasado. En esta versión, el texto en los encabezados va a centrarse por el eje Y— TA_VCENTER, y en las celdas habrá un margen ajustable desde el borde superior de la celda— TA_TOP.
{
private:
//--- Devuelve el modo de alineación del texto en la columna especificada
uint TextAlign(const int column_index,const uint anchor);
};
//+---------------------------------------------------------------------+
//| Devuelve el modo de alineación del texto en la columna especificada |
//+---------------------------------------------------------------------+
uint CCanvasTable::TextAlign(const int column_index,const uint anchor)
{
uint text_align=0;
//--- Alineación del texto para la columna actual
switch(m_vcolumns[column_index].m_text_align)
{
case ALIGN_CENTER :
text_align=TA_CENTER|anchor;
break;
case ALIGN_RIGHT :
text_align=TA_RIGHT|anchor;
break;
case ALIGN_LEFT :
text_align=TA_LEFT|anchor;
break;
}
//--- Devolver el modo de alineación
return(text_align);
}
En muchas tablas del entorno operativo, la imagen del puntero cambia si el cursor se sitúa sobre la línea entre dos encabezados. En la captura de pantalla de abajo, esta situación se muestra a través del ejemplo de la tabla en la ventana «Caja de herramientas» de la plataforma comercial MetaTrader 5. Si pinchamos en este puntero, se activará el modo de modificación del ancho de la columna. El color del fondo del encabezado de esta columna cambiará.
Fig. 4. Puntero del cursor al ser situado sobre la línea entre los encabezados.
Prepararemos la misma imagen para nuestra librería. Usted puede descargar el archivo adjunto al artículo que contiene la carpeta con todas las imágenes de los controles de la librería. Añadimos nuevos identificadores en la enumeración de los punteros ENUM_MOUSE_POINTER en el archivo Enums.mqh para el cambio de los tamaños en los ejes X y Y:
//| Enumeración de tipos de punteros |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_POINTER
{
MP_CUSTOM =0,
MP_X_RESIZE =1,
MP_Y_RESIZE =2,
MP_XY1_RESIZE =3,
MP_XY2_RESIZE =4,
MP_X_RESIZE_RELATIVE =5,
MP_Y_RESIZE_RELATIVE =6,
MP_X_SCROLL =7,
MP_Y_SCROLL =8,
MP_TEXT_SELECT =9
};
Hay que introducir adiciones correspondientes en la clase CPointer, para que este tipo del puntero esté disponible para el uso en las clases de los controles.
//| Pointer.mqh |
//| Copyright 2015, MetaQuotes Software Corp. |
//| http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Recursos
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"
//+------------------------------------------------------------------+
//| Clase para crear el puntero del cursor del ratón |
//+------------------------------------------------------------------+
class CPointer : public CElement
{
private:
//--- Establecer las imágenes para el puntero del cursor
void SetPointerBmp(void);
};
//+------------------------------------------------------------------+
//| Establecer las imágenes para el puntero según el tipo del puntero|
//+------------------------------------------------------------------+
void CPointer::SetPointerBmp(void)
{
switch(m_type)
{
...
case MP_X_RESIZE_RELATIVE :
m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
break;
case MP_Y_RESIZE_RELATIVE :
m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
break;
...
}
//--- Si ha sido especificado el tipo personalizado (MP_CUSTOM)
if(m_file_on=="" || m_file_off=="")
::Print(__FUNCTION__," > Para el puntero del cursor hay que establecer ambas imágenes"
); }
Además, aquí vamos a necesitar campos adicionales:
- Para determinar el momento de la captación del borde del encabezado.
- Para determinar el momento cuando el cursor pase del área de un encabezado al área del otro. Es necesario para ahorrar los recursos, para que los encabezados se redibujen exclusivamente en el momento de la intersección de los límites de áreas adyacentes.
{
private:
//--- Para determinar el momento cuando el cursor pasa de un encabezado a otro
int m_prev_header_index_focus;
//--- Estado de la captura del borde del encabezado para cambiar el ancho de la columna
int m_column_resize_control;
};
El método CCanvasTable::HeaderColorCurrent() permite obtener el color actual para el encabezado, dependiendo del modo actual, posición del cursor y el estado del botón izquierdo del ratón. El foco sobre el encabezado va a determinarse en el método CCanvasTable::DrawHeaders() destinado para dibujar el fondo de los encabezados, y va a pasarse acá como resultado de la comprobación.
{
private:
//--- Devuelve el color actual del fondo del encabezado
uint HeaderColorCurrent(const bool is_header_focus);
};
//+------------------------------------------------------------------+
//| Devuelve el color actual del fondo del encabezado |
//+------------------------------------------------------------------+
uint CCanvasTable::HeaderColorCurrent(const bool is_header_focus)
{
uint clr=clrNONE;
//--- Si no hay foco
if(!is_header_focus || !m_headers.MouseFocus())
clr=m_headers_color;
else
{
//--- Si el botón izquierdo del ratón se mantiene pulsado y no estamos en el proceso del cambio del ancho de la columna
bool condition=(m_mouse.LeftButtonState() && m_column_resize_control==WRONG_VALUE);
clr=(condition)? m_headers_color_pressed : m_headers_color_hover;
}
//--- Devolver el color del encabezado
return(::ColorToARGB(clr));
}
El código del método CCanvasTable::DrawHeaders() se muestra más abajo. Aquí, si el cursor del ratón no se encuentra en el área de encabezados, el lienzo entero se colorea con el color especificado. Si el foco está sobre los encabezados, es necesario determinar sobre cuál de ellos se encuentra exactamente. Para eso hay que determinar las coordenadas relativas del cursor, y luego, calculando en el ciclo las coordenadas de cada encabezado, determinamos si se encuentra en el foco o no. Aparte de eso, aquí se toma en consideración el modo del cambio del ancho de las columnas. Para este modo, en los cálculos se utiliza el margen adicional. Si el foco ha sido encontrado, hay que recordar el índice de la columna.
{
private:
//--- Margen desde los límites de las líneas separadoras para mostrar el puntero del ratón en el modo del cambio del ancho de columnas
int m_sep_x_offset;
//---
private:
//--- Dibuja los encabezados
void DrawHeaders(void);
};
//+------------------------------------------------------------------+
//| Dibuja el fondo de encabezados |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeaders(void)
{
//--- Si no se encuentra en el foco, hay que resetear el color de encabezados
if(!m_headers.MouseFocus())
{
m_headers.Erase(::ColorToARGB(m_headers_color));
return;
}
//--- Para comprobar el foco sobre los encabezados
bool is_header_focus=false;
//--- Coordenadas del cursor del ratón
int x=0;
//--- Coordenadas
int x1=0,x2=0,y1=0,y2=m_header_y_size;
//--- Obtenemos las coordenadas relativas del cursor del ratón
if(::CheckPointer(m_mouse)!=POINTER_INVALID)
{
//--- Obtenemos el desplazamiento por el eje X
int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Determinamos las coordenadas del cursor del ratón
x=m_mouse.X()-m_headers.X()+xoffset;
}
//--- Limpiar el fondo de encabezados
m_headers.Erase(::ColorToARGB(clrNONE,0));
//--- Margen considerando el modo del cambio del ancho de las columnas
int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- Dibujamos el fondo de encabezados
for(int i=0; i<m_columns_total; i++)
{
//--- Calcular las coordenadas
x2+=m_vcolumns[i].m_width;
//--- Comprobamos el foco
if(is_header_focus=x>x1+((i!=0)? sep_x_offset : 0) && x<=x2+sep_x_offset)
m_prev_header_index_focus=i;
//--- Dibujar el fondo del encabezado
m_headers.FillRectangle(x1,y1,x2,y2,HeaderColorCurrent(is_header_focus));
//--- Calcular el margen para el siguiente encabezado
x1+=m_vcolumns[i].m_width;
}
}
Después de dibujar el fondo de encabezados, hay que dibujar la cuadrícula (bordes de los encabezados). Para eso se utiliza el método CCanvasTable::DrawHeadersGrid(). Primero, se dibuja el borde común, luego en el ciclo, se trazan las líneas separadoras.
{
private:
//--- Dibuja la cuadrícula de los encabezados de la tabla
void DrawHeadersGrid(void);
};
//+------------------------------------------------------------------+
//| Dibuja la cuadrícula de los encabezados de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersGrid(void)
{
//--- Color de la cuadrícula
uint clr=::ColorToARGB(m_grid_color);
//--- Coordenadas
int x1=0,x2=0,y1=0,y2=0;
x2=m_table_x_size-1;
y2=m_header_y_size-1;
//--- Dibujar el borde
m_headers.Rectangle(x1,y1,x2,y2,clr);
//--- Líneas separadoras
x2=x1=m_vcolumns[0].m_width;
for(int i=1; i<m_columns_total; i++)
{
m_headers.Line(x1,y1,x2,y2,clr);
x2=x1+=m_vcolumns[i].m_width;
}
}
En último lugar, dibujamos el texto de los encabezados. De esta tarea se encarga el método CCanvasTable::DrawHeadersText(). Aquí, hay que recorrer en el ciclo todos los encabezados, determinando la coordenada para el texto y el modo de alineación en cada iteración. La última operación del ciclo introduce el nombre del encabezado. Aquí mismo se utiliza la corrección del texto respecto al ancho de la columna. Para eso se utiliza el método CCanvasTable::CorrectingText(). Lo consideraremos con más detalles en la siguiente sección de este artículo.
{
private:
//--- Dibuja el texto de encabezados de la tabla
void DrawHeadersText(void);
};
//+------------------------------------------------------------------+
//| Dibuja el texto de encabezados de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersText(void)
{
//--- Para calcular las coordenadas y los márgenes
int x=0,y=m_header_y_size/2;
int column_offset =0;
uint text_align =0;
//--- Color del texto
uint clr=::ColorToARGB(m_headers_text_color);
//--- Propiedades de la fuente
m_headers.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- Dibujar el texto
for(int c=0; c<m_columns_total; c++)
{
//--- Obtenemos la coordenada X para el texto
x=TextX(c,column_offset);
//--- Obtenemos el modo de alineación del texto
text_align=TextAlign(c,TA_VCENTER);
//--- Dibujar el nombre de la columna
m_headers.TextOut(x,y,CorrectingText(c,0,true),clr,text_align);
}
}
Todos los métodos mencionados para el dibujo de encabezados se invocan en el método común CCanvasTable::DrawTableHeaders(). Si el modo de visualización de encabezados está desactivado, el acceso a este método queda bloqueado.
{
private:
//--- Dibuja los encabezados de la tabla
void DrawTableHeaders(void);
};
//+------------------------------------------------------------------+
//| Dibuja los encabezados de la tabla |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTableHeaders(void)
{
//--- Salir si los encabezados están desactivados
if(!m_show_headers)
return;
//--- Dibuja los encabezados
DrawHeaders();
//--- Dibujar la cuadrícula
DrawHeadersGrid();
//--- Dibujar el texto de los encabezados
DrawHeadersText();
}
El foco sobre el encabezado se determina con el método CCanvasTable::CheckHeaderFocus(). El programa sale del método en dos ocasiones:
- si el modo de visualización de encabezados está desactivado
- o si el proceso del cambio del ancho de las columnas ha comenzado.
Luego, obtenemos las coordenadas relativas del cursor en el lienzo. Buscamos el foco sobre algún encabezado en el ciclo y comprobamos si ha cambiado desde la última llamada a este método. Si registramos un foco nuevo (momento de la intersección de los límites de encabezados), hay que resetear el índice del encabezado guardado anteriormente y detener el ciclo.
{
private:
//--- Comprobación del foco en el encabezado
void CheckHeaderFocus(void);
};
//+------------------------------------------------------------------+
//| Comprobación del foco en el encabezado |
//+------------------------------------------------------------------+
void CCanvasTable::CheckHeaderFocus(void)
{
//--- Salir si (1) los encabezados están desactivados o (2) el proceso del cambio del ancho de las columnas ha comenzado
if(!m_show_headers || m_column_resize_control!=WRONG_VALUE)
return;
//--- Coordenadas de los encabezados
int x1=0,x2=0;
//--- Obtenemos el desplazamiento por el eje X
int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Obtenemos las coordenadas relativas del cursor del ratón
int x=m_mouse.X()-m_headers.X()+xoffset;
//--- Margen considerando el modo del cambio del ancho de las columnas
int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- Buscamos el foco
for(int i=0; i<m_columns_total; i++)
{
//--- Calcular la coordenada a la derecha
x2+=m_vcolumns[i].m_width;
//--- Si el foco del encabezado ha cambiado
if((x>x1+sep_x_offset && x<=x2+sep_x_offset) && m_prev_header_index_focus!=i)
{
m_prev_header_index_focus=WRONG_VALUE;
break;
}
//--- Calcular la coordenada a la izquierda
x1+=m_vcolumns[i].m_width;
}
}
A su vez, los encabezados se redibujan sólo cuando sus límites se cruzan. Eso ahorra los recursos de la CPU. Para esa tarea se utiliza el método CCanvasTable::ChangeHeadersColor(). Aquí el programa sale del método si el modo de visualización de los encabezados está desactivado o el proceso del cambio de su ancho está en marcha. Si las comprobaciones han sido superadas al principio del método, se comprueba el foco sobre los encabezados, y ellos se redibujan.
{
private:
//--- Cambia el color de encabezados
void ChangeHeadersColor(void);
};
//+------------------------------------------------------------------+
//| Cambio del color de encabezados |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeHeadersColor(void)
{
//--- Salir si los encabezados están desactivados
if(!m_show_headers)
return;
//--- Si el puntero del cursor está activado
if(m_column_resize.IsVisible() && m_mouse.LeftButtonState())
{
//--- Guardamos el índice de la columna capturada
if(m_column_resize_control==WRONG_VALUE)
m_column_resize_control=m_prev_header_index_focus;
//---
return;
}
//--- Si no se encuentra en el foco
if(!m_headers.MouseFocus())
{
//--- Si todavía no está indicado que no se encuentra en el foco
if(m_prev_header_index_focus!=WRONG_VALUE)
{
//--- Resetear el foco
m_prev_header_index_focus=WRONG_VALUE;
//--- Cambiar el color
DrawTableHeaders();
m_headers.Update();
}
}
//--- Si se encuentra en el foco
else
{
} //--- Comprobar el foco sobre los encabezados
CheckHeaderFocus();
//--- Si no hay foco
if(m_prev_header_index_focus==WRONG_VALUE)
{
//--- Cambiar el color
DrawTableHeaders();
m_headers.Update();
}
}
}
Abajo se muestra el código del método CCanvasTable::CheckColumnResizeFocus(). Es necesario para determinar el foco en las líneas entre los encabezados y se encarga de la muestra/ocultación del cursor para el cambio del ancho de las columnas. Al principio del método hay dos comprobaciones. El programa sale del método si el modo del cambio del ancho de las columnas está desactivado. Si el modo está activado y el proceso del cambio del ancho de la columna está en marcha, hay que actualizar las coordenadas del puntero del cursor del ratón desde el método.
Si el proceso del cambio del ancho de la columna no ha comenzado todavía, entonces si el cursor se encuentra en el área de encabezados, tratamos de determinar en el ciclo el foco sobre el límite de uno de ellos. Si el foco ha sido encontrado, actualizamos las coordenadas del puntero del cursor, hacemos que esté visible y salimos del método. Si el foco no ha sido encontrado, es necesario ocultar el puntero.
{
private:
//--- Comprobación del foco en los límites de encabezados para el cambio de su ancho
void CheckColumnResizeFocus(void);
};
//+--------------------------------------------------------------------------------+
//| Comprobación del foco en los límites de encabezados para el cambio de su ancho |
//+--------------------------------------------------------------------------------+
void CCanvasTable::CheckColumnResizeFocus(void)
{
//--- Salir si el modo del cambio del ancho de las columnas está desactivado
if(!m_column_resize_mode)
return;
//--- Salir si el proceso del cambio del ancho de las columnas ha comenzado
if(m_column_resize_control!=WRONG_VALUE)
{
//--- Actualizar las coordenadas del puntero y hacerlo visible
m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
return;
}
//--- Para comprobar el foco sobre los límites de los encabezados
bool is_focus=false;
//--- Si el cursor está en el área de encabezados
if(m_headers.MouseFocus())
{
//--- Coordenadas de los encabezados
int x1=0,x2=0;
//--- Obtenemos el desplazamiento por el eje X
int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Obtenemos las coordenadas relativas del cursor del ratón
int x=m_mouse.X()-m_headers.X()+xoffset;
//--- Buscamos el foco
for(int i=0; i<m_columns_total; i++)
{
//--- Cálculo de coordenadas
x1=x2+=m_vcolumns[i].m_width;
//--- Comprobación del foco
if(is_focus=x>x1-m_sep_x_offset && x<=x2+m_sep_x_offset)
break;
}
//--- Si hay foco
if(is_focus)
{
//--- Actualizar las coordenadas del puntero y hacerlo visible
m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
//--- Mostrar el puntero
m_column_resize.Show();
return;
}
}
//--- Ocultar el puntero si no hay foco
if(!m_headers.MouseFocus() || !is_focus)
m_column_resize.Hide();
}
El resultado es el siguiente:
Fig. 5. Encabezados para las columnas.
Corrección del largo de la línea respecto al ancho de la columna
Anteriormente, para que el texto no se solapase con las celdas contiguas, teníamos que ajustar el ancho de las columnas manualmente y volver a compilar el archivo para ver el resultado. Desde luego, no es muy cómodo.
Hagamos que el largo de la línea se corrija automáticamente si ella no cabe en la celda de la tabla. Las líneas corregidas anteriormente no van a corregirse de nuevo cuando la tabla se redibuja. Para almacenar estas líneas, agregamos un array más en la estructura de las propiedades de la tabla.
{
private:
//--- Array de valores y propiedades de la tabla
struct CTOptions
{
string m_vrows[];
string m_text[];
int m_width;
ENUM_ALIGN_MODE m_text_align;
};
CTOptions m_vcolumns[];
};
Como resultado, en el array m_vrows[] va a almacenarse el texto completo, y el array m_text[] va a contener su versión corregida.
El método CCanvasTable::CorrectingText() se encargará de corregir el largo de la línea, tanto en los encabezados como en las celdas de la tabla. Después de determinar el tipo del texto con el que trabajamos, obtenemos su ancho. Luego, comprobamos si el texto completo de la línea cabe en la celda, tomando en cuenta los márgenes especificados desde los bordes. Si cabe, lo guardamos en el array m_text[] y salimos del método. En esta versión, el texto corregido se guarda sólo para las celdas, y no para los encabezados.
Si el texto no cabe, hay que recortar los caracteres sobrantes y añadir los puntos suspensivos '…' que indicarán que el texto a visualizar está recortado. No es muy complicado implementar eso:
1) Obtenemos el largo de la línea.
Luego, empezando del final de la línea, recorremos en el ciclo todos los caracteres, eliminando el último carácter y guardando el texto ya recortado en la variable temporal.
3) Si ya no quedan caracteres, devolvemos la línea vacía.
4) Hasta que haya caracteres, obtenemos el ancho de la línea resultante tomando en consideración los puntos suspensivos.
5) Comprobamos si la línea en esta forma cabe en la celda de la tabla, tomando en cuenta los márgenes especificados desde los bordes de la celda.
6) Si la línea cabe, la guardamos en la variable global del método y detenemos el ciclo.
7) Después de eso, guardamos la línea corregida en el array m_text[] y la devolvemos del método.
{
private:
//--- Devuelve el texto corregido según el ancho de la columna
string CorrectingText(const int column_index,const int row_index,const bool headers=false);
};
//+------------------------------------------------------------------+
//| Devuelve el texto corregido según el ancho de la columna |
//+------------------------------------------------------------------+
string CCanvasTable::CorrectingText(const int column_index,const int row_index,const bool headers=false)
{
//--- Obtenemos el texto actual
string corrected_text=(headers)? m_header_text[column_index]: m_vcolumns[column_index].m_vrows[row_index];
//--- Márgenes desde los bordes de la celda por el eje X
int x_offset=m_text_x_offset*2;
//--- Obtenemos el puntero al objeto del lienzo
CRectCanvas *obj=(headers)? ::GetPointer(m_headers) : ::GetPointer(m_table);
//--- Obtenemos el ancho del texto
int full_text_width=obj.TextWidth(corrected_text);
//--- Si cabemos en la celda, guardamos el texto corregido en un array separado y lo devolvemos
if(full_text_width<=m_vcolumns[column_index].m_width-x_offset)
{
//--- Si no son encabezados, guardamos el texto corregido
if(!headers)
m_vcolumns[column_index].m_text[row_index]=corrected_text;
//---
return(corrected_text);
}
//--- Si el texto no se ajusta a la celda, hay que corregirlo (recortar los caracteres sobrantes y añadir puntos suspensivos)
else
{
//--- Para trabajar con la línea
string temp_text="";
//--- Obtenemos la longitud de la línea
int total=::StringLen(corrected_text);
//--- Vamos a eliminar los caracteres de la línea uno por uno, hasta que consigamos el ancho necesario del texto
for(int i=total-1; i>=0; i--)
{
//--- Eliminamos un carácter
temp_text=::StringSubstr(corrected_text,0,i);
//--- Si no queda nada, dejamos la línea en blanco
if(temp_text=="")
{
corrected_text="";
break;
}
//--- Agregamos los puntos suspensivos antes de la comprobación
int text_width=obj.TextWidth(temp_text+"...");
//--- Si cabemos en la celda
if(text_width<m_vcolumns[column_index].m_width-x_offset)
{
//--- Guardamos el texto y detenemos el ciclo
corrected_text=temp_text+"...";
break;
}
}
}
//--- Si no son encabezados, guardamos el texto corregido
if(!headers)
m_vcolumns[column_index].m_text[row_index]=corrected_text;
//--- Devolvemos el texto corregido
return(corrected_text);
}
El uso de las líneas corregidas durante el redibujo de la tabla es muy conveniente cuando el proceso del cambio del ancho de la columna está en marcha. En vez de corregir el texto dentro de todas las celdas de la tabla una y otra vez, será suficiente hacerlo solamente dentro de las celdas de la columna que cambia su ancho. Eso ahorra los recursos de la CPU.
El método CCanvasTable::Text() va a determinar si hace falta corregir el texto para la columna especificada o bastará con enviar la versión corregida anteriormente. Su código es el siguiente:{
private:
//--- Devuelve el texto
string Text(const int column_index,const int row_index);
};
//+------------------------------------------------------------------+
//| Devuelve el texto |
//+------------------------------------------------------------------+
string CCanvasTable::Text(const int column_index,const int row_index)
{
string text="";
//--- Corregimos el texto si no es el modo del cambio del ancho de la columna
if(m_column_resize_control==WRONG_VALUE)
text=CorrectingText(column_index,row_index);
//--- Si es el modo del cambio del ancho de la columna, entonces...
else
{
//--- ...corregimos el texto sólo para la columna cuyo ancho cambiamos
if(column_index==m_column_resize_control)
text=CorrectingText(column_index,row_index);
//--- Para las demás, usamos el texto corregido anteriormente
else
text=m_vcolumns[column_index].m_text[row_index];
}
//--- Devolvemos el texto
return(text);
}
El código del método CCanvasTable::ChangeColumnWidth() destinado para el cambio del ancho de la columna se muestra más abajo.
Establecemos el ancho mínimo de la columna a 30 píxeles. El programa saldrá del método si la visualización de los encabezados está desactivada. Si la comprobación ha sido superada, a continuación se comprueba el foco en los bordes de los encabezados. Si después de esta comprobación resulta que el proceso no ha sido iniciado/terminado, las variables auxiliares se resetean y el programa sale del método. Si el proceso ha sido iniciado, obtenemos la coordenada relativa X del cursor. Si el proceso acaba de empezar, hay que guardar los valores actuales de la coordenada X del cursor (variable x_fixed) y del ancho de la columna capturada (variable prev_width). Las variables locales destinadas para eso son estáticas. Por eso, durante cada entrada en este método, sus valores van a guardarse hasta que no vayan a ser reseteados al final del proceso.
Ahora calcularemos el nuevo ancho para la columna. Si resulta que el ancho mínimo de la columna ha sido alcanzado, el programa sale del método. De lo contrario, el nuevo ancho se guarda en la estructura de las propiedades de la tabla según la columna especificada. Luego, los tamaños de la tabla se recalculan y se aplican de nuevo, y al final del método ella se redibuja.
{
private:
//--- Ancho mínimo para las columnas
int m_min_column_width;
//---
private:
//--- Cambia el ancho de la columna capturada
void ChangeColumnWidth(void);
};
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_min_column_width(30)
{
...
}
//+------------------------------------------------------------------+
//| Cambia el ancho de la columna capturada |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeColumnWidth(void)
{
//--- Salir si los encabezados están desactivados
if(!m_show_headers)
return;
//--- Comprobación del foco en los límites de encabezados
CheckColumnResizeFocus();
//--- Variables auxiliares
static int x_fixed =0;
static int prev_width =0;
//--- Si hemos terminado, reseteamos los valores
if(m_column_resize_control==WRONG_VALUE)
{
x_fixed =0;
prev_width =0;
return;
}
//--- Obtenemos el desplazamiento por el eje X
int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- Obtenemos las coordenadas relativas del cursor del ratón
int x=m_mouse.X()-m_headers.X()+xoffset;
//--- Si acabamos de empezar el cambio del ancho de la columna
if(x_fixed<1)
{
//--- Guardamos la coordenada X actual y el ancho de la columna
x_fixed =x;
prev_width =m_vcolumns[m_column_resize_control].m_width;
}
//--- Calcularemos nuevo ancho para la columna
int new_width=prev_width+(x-x_fixed);
//--- Dejar sin alterar, si es menos de la restricción establecida
if(new_width<m_min_column_width)
return;
//--- Guardamos nuevo ancho de la columna
m_vcolumns[m_column_resize_control].m_width=new_width;
//--- Calcular los tamaños de la tabla
CalculateTableSize();
//--- Establecer nuevo tamaño de la tabla
ChangeTableSize();
//--- Dibujar la tabla
DrawTable();
}
Es lo que tenemos al final:
Fig. 5. Corrección del largo de la línea respecto al ancho de la columna.
Procesamiento de eventos
La gestión del color de los objetos de la tabla y el cambio del ancho de sus columnas se realiza por el manejador del control cuando ocurre el evento del desplazamiento del cursor (CHARTEVENT_MOUSE_MOVE).
//| Manejador de eventos |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
//--- Procesamiento del evento del desplazamiento del cursor
if(id==CHARTEVENT_MOUSE_MOVE)
{
//--- Salir si el control está ocultado
if(!CElementBase::IsVisible())
return;
//--- Salir si los números de las subventanas no coinciden
if(!CElementBase::CheckSubwindowNumber())
return;
//--- Comprobación del foco sobre los controles
CElementBase::CheckMouseFocus();
m_headers.MouseFocus(m_mouse.X()>m_headers.X() && m_mouse.X()<m_headers.X2() &&
m_mouse.Y()>m_headers.Y() && m_mouse.Y()<m_headers.Y2());
//--- Si la barra de desplazamiento se encuentra en acción
if(m_scrollv.ScrollBarControl() || m_scrollh.ScrollBarControl())
{
ShiftTable();
return;
}
//--- Cambio del color de objetos
ChangeObjectsColor();
//--- Cambiar el ancho de la columna capturada
ChangeColumnWidth();
return;
}
...
}
Además, vamos a necesitar un nuevo identificador del evento para determinar el momento del cambio del estado del botón izquierdo del ratón. Es necesario para deshacerse de las comprobaciones y procesamientos repetidos en varios bloques del código del manejador. Vamos a añadir nuevo identificador ON_CHANGE_MOUSE_LEFT_BUTTON al archivo Defines.mqh:
//| Defines.mqh |
//| Copyright 2015, MetaQuotes Software Corp. |
//| http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_CHANGE_MOUSE_LEFT_BUTTON (33) // Cambio del estado del botón izquierdo del ratón
Aparte de eso, a la clase para la obtención de los parámetros actuales del ratón (CMouse) ha sido añadido el método CMouse::CheckChangeLeftButtonState() para determinar el momento del cambio del estado del botón izquierdo del ratón. Este método se invoca en el manejador de la clase. Si el estado del botón izquierdo ha cambiado, desde el método se envía un mensaje con el identificador ON_CHANGE_MOUSE_LEFT_BUTTON. Luego se puede recibir este mensaje y procesarlo en cualquier control.
//| Clase para obtener los parámetros del ratón |
//+------------------------------------------------------------------+
class CMouse
{
private:
//--- Comprobación del cambio del estado del botón izquierdo del ratón
bool CheckChangeLeftButtonState(const string mouse_state);
};
//+------------------------------------------------------------------+
//| Procesamiento de eventos del desplazamiento del cursor del ratón |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
//--- Procesamiento del evento del desplazamiento del cursor
if(id==CHARTEVENT_MOUSE_MOVE)
{
//--- Coordenadas y el estado del botón izquierdo del ratón
m_x =(int)lparam;
m_y =(int)dparam;
m_left_button_state =CheckChangeLeftButtonState(sparam);
...
}
}
//+------------------------------------------------------------------+
//| Comprobación del cambio del estado del botón izquierdo del ratón |
//+------------------------------------------------------------------+
bool CMouse::CheckChangeLeftButtonState(const string mouse_state)
{
bool left_button_state=(bool)int(mouse_state);
//--- Enviamos el mensaje sobre el cambio del estado del botón izquierdo del ratón
if(m_left_button_state!=left_button_state)
::EventChartCustom(m_chart.ChartId(),ON_CHANGE_MOUSE_LEFT_BUTTON,0,0.0,"");
//---
return(left_button_state);
}
En la clase CCanvasTable, el procesamiento del evento con el identificador ON_CHANGE_MOUSE_LEFT_BUTTON es necesario para lo siguiente:
- para anular algunos campos de la clase;
- para corregir las barras de desplazamiento
- para redibujar la tabla
//| Manejador de eventos |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
//--- Cambio del estado del botón izquierdo del ratón
if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON)
{
//--- Salir si los encabezados están desactivados
if(!m_show_headers)
return;
//--- Si el botón izquierdo del ratón está suelto
if(!m_mouse.LeftButtonState())
{
//--- Resetar el modo del cambio del ancho
m_column_resize_control=WRONG_VALUE;
//--- Ocultar el puntero
m_column_resize.Hide();
//--- Corregimos la barra de desplazamiento de acuerdo con los últimos cambios
HorizontalScrolling(m_scrollh.CurrentPos());
}
//--- Resetear el índice del último foco del encabezado
m_prev_header_index_focus=WRONG_VALUE;
//--- Cambio del color de objetos
ChangeObjectsColor();
}
}
El resultado del trabajo de la aplicación MQL ha sido demostrado en las capturas animadas de este artículo. Usted puede descargar esta aplicación a su ordenador para analizarla más detalladamente.
Conclusión
En esta actualización de la librería, hemos mejorado la tabla dibujada tipo CCanvasTable. No es la versión definitiva de la tabla, continuaremos el trabajo con ella y añadiremos otras posibilidades nuevas.
A continuación, se muestra el esquema de la librería para la creación de interfaces gráficas.
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/3030





- 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