Interfaces gráficas X: Ordenamiento, reconstrucción de la tabla y controles en las celdas (build 11)
Índice
- Introducción
- Ordenamiento de la tabla
- Inserción y eliminación de columnas y filas
- Evento del clic izquierdo del ratón
- Controles en las celdas de la tabla
- 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.
Seguimos con el desarrollo de lla tabla dibujada. Vamos a nombrar sus nuevas funcionalidades.
- Ordenamiento de datos de la tabla.
- Control del número de las columnas y filas: inserción y eliminación de las columnas y filas según el índice especificado; vaciado completo de la tabla (queda sólo una columna y una fila); reconstrucción de la tabla (vaciado completo y establecimiento de la nueva dimensionalidad).
- Vamos a ampliar las posibilidades del control de usuario de la interfaz gráfica: se añade el procesamiento del clic izquierdo del ratón.
- Empezaremos a añadir los controles a las celdas de la tabla: casillas de verificación (checkbox) y botones.
Si Usted ya utiliza esta librería para diseñar las interfaces gráficas, y aplica la tabla tipo CTable para visualizar los datos, ahora se le propone que pase a la tabla tipo CCanvasTable. A partir de este artículo, se ha igualado por completo con los demás tipos de las tablas de esta librería, e incluso las ha superado en algunos parámetros.
Ordenamiento de la tabla
La mayoría de los métodos para el ordenamiento de los datos de la tabla se han quedado los mismos que en CTable. El artículo Interfaces gráficas X: Control «Hora», control «Lista de las casillas de verificación» y ordenamiento (sort) de la tabla describe detalladamente cómo está organizado todos esto. Aquí hablaremos un poco sobre los cambios y adiciones referentes a la tabla dibujada tipo CCanvasTable.
Para dibujar el indicio de la tabla ordenada, vamos a necesitar un array estático tipo CTImage compuesto de dos elementos. Ahí van a guardarse las rutas hacia los imágenes que deben reflejar la dirección del ordenamiento. Si el usuario no establece su versión de las imágenes, se utilizarán los iconos predefinidos. La inicialización con valores predefinidos y el llenado de los arrays de las imágenes se realiza en el método de la creación de encabezados CCanvasTable::CreateHeaders().
//+------------------------------------------------------------------+ //| Clase par crear la tabla dibujada | //+------------------------------------------------------------------+ class CCanvasTable : public CElement { private: //--- Iconos para el indicio de datos ordenados CTImage m_sort_arrows[2]; //--- public: //--- Establecimiento de imágenes para el indicio de datos ordenados void SortArrowFileAscend(const string path) { m_sort_arrows[0].m_bmp_path=path; } void SortArrowFileDescend(const string path) { m_sort_arrows[1].m_bmp_path=path; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) { ... //--- Inicialización de la estructura del indicio de ordenamiento m_sort_arrows[0].m_bmp_path=""; m_sort_arrows[1].m_bmp_path=""; } //+------------------------------------------------------------------+ //| Crea encabezados de la tabla | //+------------------------------------------------------------------+ bool CCanvasTable::CreateHeaders(void) { //--- Salir si los encabezados están desactivados if(!m_show_headers) return(true); //--- Formación del nombre del objeto string name=CElementBase::ProgramName()+"_table_headers_"+(string)CElementBase::Id(); //--- Coordenadas int x =m_x+1; int y =m_y+1; //--- Determinamos las imágenes como el indicio de la posibilidad del ordenamiento de la tabla if(m_sort_arrows[0].m_bmp_path=="") m_sort_arrows[0].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp"; if(m_sort_arrows[1].m_bmp_path=="") m_sort_arrows[1].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp"; //--- for(int i=0; i<2; i++) { ::ResetLastError(); if(!::ResourceReadImage(m_sort_arrows[i].m_bmp_path,m_sort_arrows[i].m_image_data, m_sort_arrows[i].m_image_width,m_sort_arrows[i].m_image_height)) { ::Print(__FUNCTION__," > error: ",::GetLastError()); } } //--- Creación del objeto ::ResetLastError(); if(!m_headers.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,m_table_x_size,m_header_y_size,COLOR_FORMAT_ARGB_NORMALIZE)) { ::Print(__FUNCTION__," > Fallo al crear el lienzo para dibujar los encabezados de la tabla: ",::GetLastError()); return(false); } //--- Adjuntar al gráfico //--- Establecemos las propiedades //--- Coordenadas //--- Guardamos los tamaños //--- Márgenes desde el punto extremo del panel //--- Guardamos el puntero del objeto //--- Establecemos el área visible //--- Establecemos el desplazamiento del frame dentro de la imagen por los ejes X y Y ... return(true); }
El método CCanvasTable::DrawSignSortedData() va a utilizarse para dibujar el indicio del array ordenado.Este elemento se dibuja sólo si (1) el modo del ordenamiento está activado y (2) los datos de la tabla ya han sido ordenados. Los márgenes se puede controlar a través de los métodos CCanvasTable::SortArrowXGap() y CCanvasTable::SortArrowYGap(). La imagen significa que el ordenamiento ascendiente va a tener el índice 0, y el descendiente, el índice 1. A continuación, sigue el ciclo doble del dibujo de la imagen. Este tema ya ha sido considerado detalladamente.
class CCanvasTable : public CElement { private: //--- Márgenes para el icono de indicio de datos ordenados int m_sort_arrow_x_gap; int m_sort_arrow_y_gap; //--- public: //--- Iconos para el indicio de tabla ordenada void SortArrowXGap(const int x_gap) { m_sort_arrow_x_gap=x_gap; } void SortArrowYGap(const int y_gap) { m_sort_arrow_y_gap=y_gap; } //--- private: //--- Dibuja el indicio de la posibilidad de ordenar la tabla void DrawSignSortedData(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) : m_sort_arrow_x_gap(20), m_sort_arrow_y_gap(6) { ... } //+------------------------------------------------------------------+ //| Dibuja el indicio de la posibilidad de ordenar la tabla | //+------------------------------------------------------------------+ void CCanvasTable::DrawSignSortedData(void) { //--- Salir si (1) el ordenamiento está desactivado o (2) no ha sido realizado todavía if(!m_is_sort_mode || m_is_sorted_column_index==WRONG_VALUE) return; //--- Cálculo de coordenadas int x =m_columns[m_is_sorted_column_index].m_x2-m_sort_arrow_x_gap; int y =m_sort_arrow_y_gap; //--- Imagen seleccionada en dirección del ordenamiento int image_index=(m_last_sort_direction==SORT_ASCEND)? 0 : 1; //--- Dibujamos for(uint ly=0,i=0; ly<m_sort_arrows[image_index].m_image_height; ly++) { for(uint lx=0; lx<m_sort_arrows[image_index].m_image_width; lx++,i++) { //--- Si no hay color, ir al siguiente píxel if(m_sort_arrows[image_index].m_image_data[i]<1) continue; //--- Obtenemos el color de la capa inferior (fondo del encabezado) y el color del píxel indicado de la imagen uint background =m_headers.PixelGet(x+lx,y+ly); uint pixel_color =m_sort_arrows[image_index].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_headers.PixelSet(x+lx,y+ly,foreground); } } }
Durante el ordenamiento de los valores de la tabla, se utiliza el método CCanvasTable::Swap() con el fin de solaparlos. Su particularidad consiste en la necesidad de desplazar no sólo el texto de la celda y/o la imagen, sino también la mayoría de las propiedades de la celda. Al desplazar las imágenes, para evitar los fragmentos repetidos del código, necesitaremos el método adicional CCanvasTable::ImageCopy(). Se le envían dos arrays tipo CCanvasTable::ImageCopy(), receptor y fuente de datos. Como el tercer argumento, se envía el índice de la imagen copiada, porque en una celda no pueden haber varias imágenes.
class CCanvasTable : public CElement { private: //--- Copia los datos de la imagen de un array a otro void ImageCopy(CTImage &destination[],CTImage &source[],const int index); }; //+------------------------------------------------------------------+ //| Copia los datos de la imagen de un array a otro | //+------------------------------------------------------------------+ void CCanvasTable::ImageCopy(CTImage &destination[],CTImage &source[],const int index) { //--- Copiamos los píxeles de la imagen ::ArrayCopy(destination[index].m_image_data,source[index].m_image_data); //--- Copiamos las propiedades de la imagen destination[index].m_image_width =source[index].m_image_width; destination[index].m_image_height =source[index].m_image_height; destination[index].m_bmp_path =source[index].m_bmp_path; }
Al final, el código del método CCanvasTable::Swap() es como se muestra en el listado del código de abajo. Antes de mover las imágenes, primero es necesario comprobar si están presentes, y si no, pasar a la siguiente columna. Si en una de las celdas hay imágenes, se desplazan a través del método CCanvasTable::ImageCopy(). Es la misma operación como en el caso del desplazamiento de otras propiedades de la celda. Es decir, primero memorizamos la propiedad de la primera celda. Luego, copiamos la misma propiedad de la segunda celda en lugar de la primera. Y finalmente, en lugar de la segunda celda colocamos el valor guardado anteriormente de la primera.
class CCanvasTable : public CElement { private: //--- Alternar los valores de las celdas especificadas void Swap(uint r1,uint r2); }; //+------------------------------------------------------------------+ //| Alterna los elementos | //+------------------------------------------------------------------+ void CCanvasTable::Swap(uint r1,uint r2) { //--- Recorremos en el ciclo todas las columnas for(uint c=0; c<m_columns_total; c++) { //--- Alternamos el texto completo string temp_text =m_columns[c].m_rows[r1].m_full_text; m_columns[c].m_rows[r1].m_full_text =m_columns[c].m_rows[r2].m_full_text; m_columns[c].m_rows[r2].m_full_text =temp_text; //--- Alternamos el texto breve temp_text =m_columns[c].m_rows[r1].m_short_text; m_columns[c].m_rows[r1].m_short_text =m_columns[c].m_rows[r2].m_short_text; m_columns[c].m_rows[r2].m_short_text =temp_text; //--- Alternamos el número de dígitos después de la coma uint temp_digits =m_columns[c].m_rows[r1].m_digits; m_columns[c].m_rows[r1].m_digits =m_columns[c].m_rows[r2].m_digits; m_columns[c].m_rows[r2].m_digits =temp_digits; //--- Alternamos el color del texto color temp_text_color =m_columns[c].m_rows[r1].m_text_color; m_columns[c].m_rows[r1].m_text_color =m_columns[c].m_rows[r2].m_text_color; m_columns[c].m_rows[r2].m_text_color =temp_text_color; //--- Alternamos el índice de la imagen seleccionada int temp_selected_image =m_columns[c].m_rows[r1].m_selected_image; m_columns[c].m_rows[r1].m_selected_image =m_columns[c].m_rows[r2].m_selected_image; m_columns[c].m_rows[r2].m_selected_image =temp_selected_image; Comprobamos si hay imágenes en las celdas int r1_images_total=::ArraySize(m_columns[c].m_rows[r1].m_images); int r2_images_total=::ArraySize(m_columns[c].m_rows[r2].m_images); //--- Pasamos a la siguiente columna si ambas celdas no contienen imágenes if(r1_images_total<1 && r2_images_total<1) continue; //--- Alternamos las imágenes CTImage r1_temp_images[]; //--- ::ArrayResize(r1_temp_images,r1_images_total); for(int i=0; i<r1_images_total; i++) ImageCopy(r1_temp_images,m_columns[c].m_rows[r1].m_images,i); //--- ::ArrayResize(m_columns[c].m_rows[r1].m_images,r2_images_total); for(int i=0; i<r2_images_total; i++) ImageCopy(m_columns[c].m_rows[r1].m_images,m_columns[c].m_rows[r2].m_images,i); //--- ::ArrayResize(m_columns[c].m_rows[r2].m_images,r1_images_total); for(int i=0; i<r1_images_total; i++) ImageCopy(m_columns[c].m_rows[r2].m_images,r1_temp_images,i); } }
El clic en uno de los encabezados de la tabla llama al método CCanvasTable::OnClickHeaders(), que inicia el ordenamiento de datos. En esta clase, este método es mucho más sencillo que en la tabla tipo CTable: compare y asegúrese por sí mismo.
class CCanvasTable : public CElement { private: //--- Procesamiento del clic en el encabezado bool OnClickHeaders(const string clicked_object); }; //+------------------------------------------------------------------+ //| Procesamiento del clic en el encabezado | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickHeaders(const string clicked_object) { //--- Salir si (1) el modo de ordenamiento está desactivado o (2) se ejecuta el proceso del cambio del ancho de la columna if(!m_is_sort_mode || m_column_resize_control!=WRONG_VALUE) 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_headers.Name()!=clicked_object) return(false); //--- Para la determinación del índice de la columna uint column_index=0; //--- Obtenemos la coordenada relativa X debajo del cursor del ratón int x=m_mouse.RelativeX(m_headers); //--- Determinamos el encabezado en el que hemos pulsado for(uint c=0; c<m_columns_total; c++) { //--- Si el encabezado ha sido encontrado, recordamos su índice if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2) { column_index=c; break; } } //--- Ordenamiento de datos para la columna especificada SortData(column_index); //--- Enviamos el mensaje sobre ello ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index))); return(true); }
Los demás métodos para el ordenamiento de datos han quedado sin cambiar y no se diferencian en nada de los considerados anteriormente Por eso vamos a considerar sólo la lista de las declaraciones de estos campos y métodos en la clase CCanvasTable:
class CCanvasTable : public CElement { private: //--- Modo de ordenamiento de datos por las columnas bool m_is_sort_mode; //--- Indice de la columna ordenada (WRONG_VALUE – tabla no ordenada) int m_is_sorted_column_index; //--- Ultima dirección del ordenamiento ENUM_SORT_MODE m_last_sort_direction; //--- public: //--- Modo de datos ordenados void IsSortMode(const bool flag) { m_is_sort_mode=flag; } //--- Obtención/establecimiento del tipo de datos ENUM_DATATYPE DataType(const uint column_index); void DataType(const uint column_index,const ENUM_DATATYPE type); //--- Ordenar los datos para la columna especificada void SortData(const uint column_index=0); //--- private: //--- Procesamiento del clic en el encabezado bool OnClickHeaders(const string clicked_object); //--- Método del ordenamiento rápido void QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND); //--- Comprobación de las condiciones del ordenamiento bool CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction); };
Abajo, se puede ver cómo funciona el ordenamiento en este tipo de tablas:
Fig. 1. Demostración del ordenamiento en la tabla tipo CCanvasTable.
Inserción y eliminación de columnas y filas
En uno de los artículos anteriores para las tablas tipo CTable, hemos considerado los métodos para insertar y eliminar las columnas y filas. En aquella versión, la inserción era posible sólo al final de la tabla. Ahora vamos a quitar este inconveniente enojoso: pues, hagamos que se pueda insertar una columna o una fila al especificar el índice del lugar de su inserción.
Ejemplo: vamos a insertar una columna al principio de la tabla de datos, es decir, la nueva columna debe convertirse en la primera (índice 0). El array de las columnas debe aumentarse a un elemento, mientras que las propiedades y los valores de todas las celdas de la tabla deben desplazarse a un elemento a la derecha, dejando las celdas de la columna nueva en blanco. El mismo principio actúa también durante la inserción de las filas de la tabla.
Cuando hay que eliminar una columna o una fila, funciona el principio reverso. Intentaremos quitar la primera columna de nuestro ejemplo anterior. Primero, los datos y las propiedades de las celdas serán desplazados a un elemento a la izquierda, y después de eso, el tamaño del array de las columnas se reducirá a un elemento.
Aquí, han sido implementados los métodos adicionales para que sea más cómodo crear los métodos de la inserción y eliminación de columnas y filas. Para inicilizar las celdas, columnas y filas insertadas con valores predefinidos, van a utilizarse los métodos CCanvasTable::ColumnInitialize() y CCanvasTable::CellInitialize().
class CCanvasTable : public CElement { private: //--- Inicialización de la columna especificada con valores predefinidos void ColumnInitialize(const uint column_index); //--- Inicialización de la fila especificada con valores predefinidos void CellInitialize(const uint column_index,const uint row_index); }; //+--------------------------------------------------------------------+ //| Inicialización de la columna especificada con valores predefinidos | //+--------------------------------------------------------------------+ void CCanvasTable::ColumnInitialize(const uint column_index) { //--- Inicialización de las propiedades de las columnas con valores predefinidos m_columns[column_index].m_x =0; m_columns[column_index].m_x2 =0; m_columns[column_index].m_width =100; m_columns[column_index].m_type =TYPE_STRING; m_columns[column_index].m_text_align =ALIGN_CENTER; m_columns[column_index].m_text_x_offset =m_text_x_offset; m_columns[column_index].m_image_x_offset =m_image_x_offset; m_columns[column_index].m_image_y_offset =m_image_y_offset; m_columns[column_index].m_header_text =""; } //+------------------------------------------------------------------+ //| Inicialización de la fila especificada con valores predefinidos | //+------------------------------------------------------------------+ void CCanvasTable::CellInitialize(const uint column_index,const uint row_index) { m_columns[column_index].m_rows[row_index].m_full_text =""; m_columns[column_index].m_rows[row_index].m_short_text =""; m_columns[column_index].m_rows[row_index].m_selected_image =0; m_columns[column_index].m_rows[row_index].m_text_color =m_cell_text_color; m_columns[column_index].m_rows[row_index].m_digits =0; m_columns[column_index].m_rows[row_index].m_type =CELL_SIMPLE; //--- Por defecto, la celda no tiene imágenes ::ArrayFree(m_columns[column_index].m_rows[row_index].m_images);s }
Para copiar las propiedades de una columna a la otra, hay que usar el método CCanvasTable::ColumnCopy(), cuyo código viene a continuación:
class CCanvasTable : public CElement { private: //--- Hace la copia de la columna especificada (source) insertándola en nuevo lugar (dest.) void ColumnCopy(const uint destination,const uint source); }; //+-----------------------------------------------------------------------------------------+ //| Hace la copia de la columna especificada (source) insertándola en nuevo lugar (dest.) | //+-----------------------------------------------------------------------------------------+ void CCanvasTable::ColumnCopy(const uint destination,const uint source) { m_columns[destination].m_header_text =m_columns[source].m_header_text; m_columns[destination].m_width =m_columns[source].m_width; m_columns[destination].m_type =m_columns[source].m_type; m_columns[destination].m_text_align =m_columns[source].m_text_align; m_columns[destination].m_text_x_offset =m_columns[source].m_text_x_offset; m_columns[destination].m_image_x_offset =m_columns[source].m_image_x_offset; m_columns[destination].m_image_y_offset =m_columns[source].m_image_y_offset; }
El método CCanvasTable::CellCopy() copia una celda especificada a la otra. Para eso, hay que pasar los índices de la columna y fila de la celda de destino y los índices de la columna y fila de la celda fuente.
class CCanvasTable : public CElement { private: //--- Hace la copia de la celda especificada (source) insertándola en nuevo lugar (dest.) void CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source); }; //+-----------------------------------------------------------------------------------------+ //| Hace la copia de la celda especificada (source) insertándola en nuevo lugar (dest.) | //+-----------------------------------------------------------------------------------------+ void CCanvasTable::CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source) { m_columns[column_dest].m_rows[row_dest].m_type =m_columns[column_source].m_rows[row_source].m_type; m_columns[column_dest].m_rows[row_dest].m_digits =m_columns[column_source].m_rows[row_source].m_digits; m_columns[column_dest].m_rows[row_dest].m_full_text =m_columns[column_source].m_rows[row_source].m_full_text; m_columns[column_dest].m_rows[row_dest].m_short_text =m_columns[column_source].m_rows[row_source].m_short_text; m_columns[column_dest].m_rows[row_dest].m_text_color =m_columns[column_source].m_rows[row_source].m_text_color; m_columns[column_dest].m_rows[row_dest].m_selected_image =m_columns[column_source].m_rows[row_source].m_selected_image; //--- Copiamos el tamaño del array desde la fuente al receptor int images_total=::ArraySize(m_columns[column_source].m_rows[row_source].m_images); ::ArrayResize(m_columns[column_dest].m_rows[row_dest].m_images,images_total); //--- for(int i=0; i<images_total; i++) { //--- Copiar si hay imágenes if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<1) continue; //--- Hacemos la copia de la imagen ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i); } }
Con el fin de evitar la repetición continua del código, ha sido implementado otro método que será llamado después de las modificaciones introducidas en la reconstrucción de la tabla. Cada vez tras la inserción y eliminación de las columnas y filas, es necesario recontar y luego establecer nuevos tamaños de la tabla, y después de eso redibujar la tabla para que los cambios hagan efecto. Para eso, se usa el método CCanvasTable::RecalculateAndResizeTable(). Aquí, como único parámetro, se indica si es necesario redibujar la tabla entera (true), o simplemente actualizarla (false).
class CCanvasTable : public CElement { private: //--- Cálculo tomando en cuenta las últimas modificaciones, y cambio de los tamaños de la tabla void RecalculateAndResizeTable(const bool redraw=false); }; //+-------------------------------------------------------------------------------------------+ //| Cálculo tomando en cuenta las últimas modificaciones, y cambio de los tamaños de la tabla | //+-------------------------------------------------------------------------------------------+ void CCanvasTable::RecalculateAndResizeTable(const bool redraw=false) { //--- Calcular los tamaños de la tabla CalculateTableSize(); //--- Establecer nuevo tamaño de la tabla ChangeTableSize(); //--- Actualizar la tabla UpdateTable(redraw); }
Los métodos para insertar y eliminar las columnas y filas son mucho más simples y claros que las versiones de estos métodos en la tabla tipo CTable. Usted mismo puede comparar el código de estos métodos, y aquí como ejemplo mostraremos sólo el código para insertar y eliminar las columnas. El código para trabajar con las filas tiene la misma secuencia de acciones que ha sido descrita al principio de esta sección. Sólo voy a remarcar que el indicio de la tabla ordenada va a desplazarse junto con la columna ordenada, tanto en el caso de la inserción, como en el caso de la eliminación. Cuando la columna ordenada se elimina, el campo que guarda el índice de la columna ordenada, se pone a cero.
class CCanvasTable : public CElement { public: //--- Inserta la columna en la tabla según el índice especificado void AddColumn(const int column_index,const bool redraw=false); //--- Elimina la columna de la tabla según el índice especificado void DeleteColumn(const int column_index,const bool redraw=false); }; //+------------------------------------------------------------------+ //| Inserta la columna en la tabla según el índice especificado | //+------------------------------------------------------------------+ void CCanvasTable::AddColumn(const int column_index,const bool redraw=false) { //--- Aumentamos el tamaño del array a un elemento int array_size=(int)ColumnsTotal(); m_columns_total=array_size+1; ::ArrayResize(m_columns,m_columns_total); //--- Establecer el tamaño para los arrays de filas ::ArrayResize(m_columns[array_size].m_rows,m_rows_total); //--- Corrección del índice en caso de exceder el rango int checked_column_index=(column_index>=(int)m_columns_total)? (int)m_columns_total-1 : column_index; //--- Desplazar todas las columnas (movemos desde el fin del array hacia el índice de la columna añadida) for(int c=array_size; c>=checked_column_index; c--) { //--- Desplazar el indicio del array ordenado if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE) m_is_sorted_column_index++; //--- Indice de la columna anterior int prev_c=c-1; //--- En columna nueva, inicialización con valores predefinidos if(c==checked_column_index) ColumnInitialize(c); //--- Movemos los datos de la columna anterior a la actual else ColumnCopy(c,prev_c); //--- for(uint r=0; r<m_rows_total; r++) { //--- Inicialización de las celdas de nueva columna con valores por defecto if(c==checked_column_index) { CellInitialize(c,r); continue; } //--- Movemos los datos desde la celda de la columna anterior a la celda de la actual CellCopy(c,r,prev_c,r); } } //--- Calcular y establecer nuevos tamaños de la tabla RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Elimina la columna de la tabla según el índice especificado | //+------------------------------------------------------------------+ void CCanvasTable::DeleteColumn(const int column_index,const bool redraw=false) { //--- Obtenemos el tamaño del array de columnas int array_size=(int)ColumnsTotal(); //--- Corrección del índice en caso de exceder el rango int checked_column_index=(column_index>=array_size)? array_size-1 : column_index; //--- Desplazar otras columnas (nos movemos del índice especificado hasta la última columna) for(int c=checked_column_index; c<array_size-1; c++) { //--- Desplazar el indicio del array ordenado if(c!=checked_column_index) { if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE) m_is_sorted_column_index--; } //--- Poner a cero si la columna ordenada ha sido eliminada else m_is_sorted_column_index=WRONG_VALUE; //--- Indice de la siguiente columna int next_c=c+1; //--- Movemos los datos de la siguiente columna a la actual ColumnCopy(c,next_c); //--- Movemos los datos desde las celdas de la siguiente columna a las celdas de la actual for(uint r=0; r<m_rows_total; r++) CellCopy(c,r,next_c,r); } //--- Reducimos el array de columnas a un elemento m_columns_total=array_size-1; ::ArrayResize(m_columns,m_columns_total); //--- Calcular y establecer nuevos tamaños de la tabla RecalculateAndResizeTable(redraw); }
Los métodos para la reconstrucción completa y el vaciado de la tabla también son muy simples, a diferencia de los métodos análogos en la tabla tipo CTable. Aquí basta con establecer el nuevo tamaño a través de la llamada al método CCanvasTable::TableSize(). Al vaciar la tabla, se establece el tamaño mínimo, cuando queda sólo una columna y una fila. Durante el vaciado también se resetean los campos donde se guardan los valores de la fila seleccionada, dirección del ordenamiento y el índice de la columna ordenada. Al final de ambos métodos se calculan y se establecen nuevos tamaños de la tabla.
class CCanvasTable : public CElement { public: //--- Reconstrucción de la tabla void Rebuilding(const int columns_total,const int rows_total,const bool redraw=false); //--- Vacía la tabla. Queda sólo una columna y una fila. void Clear(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Reconstrucción de la tabla | //+------------------------------------------------------------------+ void CCanvasTable::Rebuilding(const int columns_total,const int rows_total,const bool redraw=false) { //--- Establecer nuevo tamaño TableSize(columns_total,rows_total); //--- Calcular y establecer nuevos tamaños de la tabla RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Vacía la tabla. Queda sólo una columna y una fila. | //+------------------------------------------------------------------+ void CCanvasTable::Clear(const bool redraw=false) { //--- Establecer nuevo tamaño 1x1 TableSize(1,1); //--- Establecer los valores por defecto m_selected_item_text =""; m_selected_item =WRONG_VALUE; m_last_sort_direction =SORT_ASCEND; m_is_sorted_column_index =WRONG_VALUE; //--- Calcular y establecer nuevos tamaños de la tabla RecalculateAndResizeTable(redraw); }
Es funciona así:
Fig. 2. Demostración del control de la dimensionalidad de la tabla.
Al final del artículo, se puede descargar la aplicación de prueba que se muestra en la animación anterior.
Evento del clic izquierdo del ratón
A veces, puede ser necesario activar alguna acción haciendo doble clic con ratón. Vamos a implementar la generación de tal evento en nuestra librería. Para eso, añadimos el identificador ON_DOUBLE_CLICK al archivo Defines.mqh para el evento del doble clic izquierdo del ratón:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ ... #define ON_DOUBLE_CLICK (34) // Doble clic izquierdo del ratón ...
El evento con el identificador ON_DOUBLE_CLICK va a generarse en la clase CMouse. En el Sistema operativo, este evento suele generarse al hacer las acciones «pulsar-soltar-pulsar» con el botón izquierdo del ratón. Pero el testeo en el entorno del terminal ha mostrado que no siempre se puede detectar momentáneamente el evento del clic izquierdo del ratón cuando llega el evento CHARTEVENT_MOUSE_MOVE (parámetro sparam). Por eso, se ha decidido implementar la generación del evento a través de las acciones «pulsar-soltar-pulsar-soltar». Es posible detectar con precisión estas acciones cuando ocurre el evento CHARTEVENT_CLICK.
Por defecto, debe pasar no menos de 300 milisegundos entre dos pulsaciones. Con el fin de seguir el evento CHARTEVENT_CLICK, en el manejador de eventos del ratón va a invocarse el método CMouse::CheckDoubleClick(). En el inicio de este método, se declaran dos variables estáticas, en los cuales, cada vez que se llame al método, van a actualizarse los valores del tick anterior y actual (número de milisegundos transcurridos desde el momento del inicio del sistema). Si entre estos valores hay menos milisegundos que figuran en el campo m_pause_between_clicks, entonces se generará el evento del doble clic del ratón.
//+------------------------------------------------------------------+ //| Clase para obtener los parámetros del ratón | //+------------------------------------------------------------------+ class CMouse { private: Pausa entre las pulsaciones con el botón izquierdo del ratón (para determinar el doble clic) uint m_pause_between_clicks; //--- private: //--- Comprobación del doble clic izquierdo del ratón bool CheckDoubleClick(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CMouse::CMouse(void) : m_pause_between_clicks(300) { ... } //+------------------------------------------------------------------+ //| Procesamiento de eventos del ratón | //+------------------------------------------------------------------+ void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Procesamiento del evento del clic en el gráfico if(id==CHARTEVENT_CLICK) { //--- Comprobamos el doble clic izquierdo del ratón CheckDoubleClick(); return; } } //+------------------------------------------------------------------+ //| Comprobación del doble clic izquierdo del ratón | //+------------------------------------------------------------------+ void CMouse::CheckDoubleClick(void) { static uint prev_depressed =0; static uint curr_depressed =::GetTickCount(); //--- Actualizamos los valores prev_depressed =curr_depressed; curr_depressed =::GetTickCount(); //--- Determinamos el tiempo entre las pulsaciones uint counter=curr_depressed-prev_depressed; //--- Si entre las pulsaciones ha pasado menos tiempo del que está especificado, enviamos el mensaje sobre el doble clic if(counter<m_pause_between_clicks) ::EventChartCustom(m_chart.ChartId(),ON_DOUBLE_CLICK,counter,0.0,""); }
De esta manera, en los manejadores de eventos de todas las clases de la librería se puede monitorear el doble clic izquierdo del ratón en cualquier lugar del área del gráfico, independientemente de que si haya o no un objeto gráfico debajo del cursor.
Controles en las celdas de la tabla
En este artículo, vamos a abrir el tema de los controles en las celdas de la tabla. Por ejemplo, esta posibilidad puede ser oportuna cuando es necesario crear un sistema comercial de parámetros múltiples. Es conveniente implementar la interfaz gráfica de este sistema en forma de una tabla. Vamos a empezar a insertar estos controles en las celdas de la tabla, comenzando con las casillas de verificación (checkbox) y los botones.
En primer lugar, nos hará falta la nueva enumeración ENUM_TYPE_CELL, donde van a determinarse los tipos de las celdas:
//+------------------------------------------------------------------+ //| Enumeración de los tipos de las celdas | //+------------------------------------------------------------------+ enum ENUM_TYPE_CELL { CELL_SIMPLE =0, CELL_BUTTON =1, CELL_CHECKBOX =2 };
Por defecto, durante la inicialización, a las celdas se les asigna el tipo simple, CELL_SIMPLE, lo que significa «celda sin control». Para establecer u obtener el tipo de la celda, utilice los métodos CCanvasTable::CellType(), cuyo código se muestra a continuación. Una vez establecido el tipo CELL_CHECKBOX (chechbox) para la celda, también hay que establecer los iconos para el estado del checkbox. El número mínimo de iconos para este tipo de celdas son dos. Se puede establecer más de dos imágenes, entonces habrá posibilidad de trabajar con los checkbox de parámetros múltiples.
class CCanvasTable : public CElement { //--- Establecimiento/obtención del tipo de la celda void CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type); ENUM_TYPE_CELL CellType(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Establece el tipo de la celda | //+------------------------------------------------------------------+ void CCanvasTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type) { //--- Comprobar la superación del rango if(!CheckOutOfRange(column_index,row_index)) return; //--- Establecer el tipo de la celda m_columns[column_index].m_rows[row_index].m_type=type; } //+------------------------------------------------------------------+ //| Obtención del tipo de la celda | //+------------------------------------------------------------------+ ENUM_TYPE_CELL CCanvasTable::CellType(const uint column_index,const uint row_index) { //--- Comprobar la superación del rango if(!CheckOutOfRange(column_index,row_index)) return(WRONG_VALUE); //--- Devolver el tipo de datos para la columna especificada return(m_columns[column_index].m_rows[row_index].m_type); }
Las series de los iconos para los checkbox y botones pueden ser de tamaños diferentes en cada columna, por eso se necesita la posibilidad de establecer los márgenes X y Y de los iconos para cada columna individualmente. Se puede hacer eso usando los métodos CCanvasTable::ImageXOffset() y CCanvasTable::ImageYOffset():
class CCanvasTable : public CElement { public: //--- Desplazamiento de imágenes por los ejes X y Y void ImageXOffset(const int &array[]); void ImageYOffset(const int &array[]); };
Otro complemento es un modo cuya activación permitirá excluir la cancelación de la selección de la fila en caso de la pulsación reiterativa. Este modo tendrá efecto sólo si el modo de la selección de la fila está activado.
class CCanvasTable : public CElement { private: //--- Sin cancelación de la selección de la fila cuando se vuelve a pulsar bool m_is_without_deselect; //--- public: //--- Modo «Sin cancelación de la selección de la fila cuando se vuelve a pulsar» void IsWithoutDeselect(const bool flag) { m_is_without_deselect=flag; } };
Para determinar el índice de la fila o columna que ha sido pulsada, ahora se utilizan los métodos separados CCanvasTable::PressedRowIndex() y CCanvasTable::PressedCellColumnIndex(). Ya han sido considerados antes como bloques del método CCanvasTable::OnClickTable(). Puede estudiar su código completo con nuevas adiciones en los archivos adjuntos al artículo. Sólo quería subrayar que en total, a través de estos métodos se puede detectar la celda en la que ha sido hecho el clic izquierdo. A continuación, vamos a ver a dónde serán enviados los índices de la columna y fila obtenidos.
class CCanvasTable : public CElement { private: //--- Devuelve el índice de la fila pulsada int PressedRowIndex(void); //--- Devuelve el índice de la columna con la celda pulsada int PressedCellColumnIndex(void); };
Al hacer clic en la celda de la tabla, hay que obtener su tipo, y si contiene un control, también es necesario determinar si ha sido activado. Para este propósito, ha sido implementada una serie de métodos privados, el principal de los cuales es CCanvasTable::CheckCellElement(). Precisamente, a este método se traspasan los índices de la columna y fila obtenidos a través de los métodos CCanvasTable::PressedRowIndex() y CCanvasTable::PressedCellColumnIndex(). Este método es de dos modos. Dependiendo del evento cuyo procesamiento acciona la llamada a este método, se utiliza el tercer parámetro que permite especificar si este evento ha sido un doble clic del ratón.
Luego, se verifica el tipo de la celda, y en función de su tipo, se llama al método correspondiente. Si la celda contiene el control «Botón», se invocará el método CCanvasTable::CheckPressedButton(). Para las celdas con checkbox se usa el método CCanvasTable::CheckPressedCheckBox().
class CCanvasTable : public CElement { private: //--- Comprueba si ha sido empleado el control de la celda al hacer clic bool CheckCellElement(const int column_index,const int row_index,const bool double_click=false); }; //+-------------------------------------------------------------------------+ //| Comprueba si ha sido empleado el control de la celda al hacer clic | //+-------------------------------------------------------------------------+ bool CCanvasTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false) { //--- Salir si en la celda no hay control if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE) return(false); //--- switch(m_columns[column_index].m_rows[row_index].m_type) { //--- Si es una celda con botón case CELL_BUTTON : { if(!CheckPressedButton(column_index,row_index,double_click)) return(false); //--- break; } //--- Si es una celda con checkbox case CELL_CHECKBOX : { if(!CheckPressedCheckBox(column_index,row_index,double_click)) return(false); //--- break; } } //--- return(true); }
Vamos a ver cómo están organizados los métodos CCanvasTable::CheckPressedButton() y CCanvasTable::CheckPressedCheckBox(). Al principio de ambos métodos se encuentra la comprobación del número de imágenes en la celda. En las celdas de botones debe haber como mínimo un icono, en las celdas de checkbox, como mínimo dos iconos. Luego, hay que determinar si ha tenido lugar el clic precisamente en este icono. En caso con checkbox, implementamos dos modos de su conmutación. Una pulsación solitaria va a tener efecto sólo si hemos clic en el icono con la imagen del checkbox. Un doble clic conmuta el checkbox en cualquier parte de la celda. En ambos métodos, cuando todas las condiciones están cumplidas, se genera el evento con el identificador correspondiente al control. Como parámetro double se envía el índice de la imagen, mientras que en el parámetro string se forma una cadena con los índices de la columna y la fila.
class CCanvasTable : public CElement { private: //--- Comprueba si el clic ha sido hecho en el botón de la celda bool CheckPressedButton(const int column_index,const int row_index,const bool double_click=false); //--- Comprueba si el clic ha sido hecho en el checkbox de la celda bool CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| Comprueba si el clic ha sido hecho en el botón de la celda | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedButton(const int column_index,const int row_index,const bool double_click=false) { //--- Salir si en la celda no hay imágenes if(ImagesTotal(column_index,row_index)<1) { ::Print(__FUNCTION__," > ¡Establezca como mínimo un icono para la celda de botón!"); return(false); } //--- Obtenemos las coordenadas relativas debajo del cursor del ratón int x=m_mouse.RelativeX(m_table); //--- Obtenemos el borde derecho de la imagen int image_x =int(m_columns[column_index].m_x+m_columns[column_index].m_image_x_offset); int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width); //--- Salir si el clic no ha sido hecho en la imagen if(x>image_x2) return(false); else { //--- Si no es el doble clic, enviamos el mensaje if(!double_click) { int image_index=m_columns[column_index].m_rows[row_index].m_selected_image; ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElementBase::Id(),image_index,string(column_index)+"_"+string(row_index)); } } //--- return(true); } //+------------------------------------------------------------------+ //| Comprueba si el clic ha sido hecho en el checkbox de la celda | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false) { //--- Salir si en la celda no hay imágenes if(ImagesTotal(column_index,row_index)<2) { ::Print(__FUNCTION__," > ¡Establezca como mínimo dos iconos para la celda de checkbox!"); return(false); } //--- Obtenemos las coordenadas relativas debajo del cursor del ratón int x=m_mouse.RelativeX(m_table); //--- Obtenemos el borde derecho de la imagen int image_x =int(m_columns[column_index].m_x+m_image_x_offset); int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width); //--- Salir si (1) el clic no ha sido hecho en la imagen y (2) no es el doble clic if(x>image_x2 && !double_click) return(false); else { //--- Indice actual de la imagen seleccionada int image_i=m_columns[column_index].m_rows[row_index].m_selected_image; //--- Determinamos el siguiente índice para la imagen int next_i=(image_i<ImagesTotal(column_index,row_index)-1)? ++image_i : 0; //--- Seleccionar la siguiente imagen y actualizar la tabla ChangeImage(column_index,row_index,next_i,true); m_table.Update(false); //--- Enviamos el mensaje sobre ello ::EventChartCustom(m_chart_id,ON_CLICK_CHECKBOX,CElementBase::Id(),next_i,string(column_index)+"_"+string(row_index)); } //--- return(true); }
Como resultado, el código de los métodos CCanvasTable::OnClickTable() y CCanvasTable::OnDoubleClickTable() que procesan el clic en la tabla ahora es mucho más claro y legible (véase el código de abajo). Dependiendo de los modos activados y el tipo de la celda pulsada, se genera el evento correspondiente.
class CCanvasTable : public CElement { private: //--- Procesamiento del clic en la tabla bool OnClickTable(const string clicked_object); //--- Procesamiento del doble clic en la tabla bool OnDoubleClickTable(const string clicked_object); }; //+------------------------------------------------------------------+ //| Procesamiento del clic en la tabla | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickTable(const string clicked_object) { //--- Salir si (1) el modo de selección de la fila está desactivado o (2) se ejecuta el proceso del cambio del ancho de la columna if(m_column_resize_control!=WRONG_VALUE) 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); //--- Determinamos la fila en la que hemos pulsado int r=PressedRowIndex(); //--- Determinamos la celda en la que hemos pulsado int c=PressedCellColumnIndex(); //--- Comprobamos si ha sido empleado el control en la celda bool is_cell_element=CheckCellElement(c,r); //--- Si (1) el modo de selección de la fila está activado y (2) el control en la celda no ha sido empleado if(m_selectable_row && !is_cell_element) { //--- Cambiar el color RedrawRow(true); m_table.Update(); //--- Enviamos el mensaje sobre ello ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,string(c)+"_"+string(r)); } //--- return(true); } //+------------------------------------------------------------------+ //| Procesamiento del doble clic en la tabla | //+------------------------------------------------------------------+ bool CCanvasTable::OnDoubleClickTable(const string clicked_object) { if(!m_table.MouseFocus()) return(false); //--- Determinamos la fila en la que hemos pulsado int r=PressedRowIndex(); //--- Determinamos la celda en la que hemos pulsado int c=PressedCellColumnIndex(); //--- Comprobamos si ha sido empleado el control en la celda y devolvemos el resulltado return(CheckCellElement(c,r,true)); }
El procesamiento del doble clic en la celda, se realiza en el manejador del control cuando ocurre el evento ON_DOUBLE_CLICK:
//+------------------------------------------------------------------+ //| Manejador de eventos | //+------------------------------------------------------------------+ void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Procesamiento del doble clic izquierdo del ratón if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK) { //--- Clic en la tabla if(OnDoubleClickTable(sparam)) return; //--- return; } }
Al fin y al cabo, todo funciona así:
Fig. 3. Demostración de la interacción con los controles en las celdas de la tabla.
Use los archivos adjuntos para descargar las aplicaciones demostradas en este artículo a su ordenador.
Conclusión
En esta fase del desarrollo de la librería para la creación de las interfaces gráficas, su esquema tiene el siguiente aspecto:
Fig. 4. 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/3104
- 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