Interfaces gráficas X: Ordenação, reconstrução da tabela e controles nas células (build 11)
Anatoli Kazharski | 2 maio, 2017
Conteúdo
- Introdução
- A Tabela Ordenada
- Adição e remoção de colunas e linhas
- Evento do clique duplo do botão esquerdo do mouse
- Controles nas células da tabela
- Conclusão
Introdução
O primeiro artigo Interfaces gráficas I: Preparação da Estrutura da Biblioteca (Capítulo 1) considera em detalhes a finalidade desta biblioteca. Você irã encontrar uma lista de artigos com os links no final de cada capítulo. Lá, você também pode encontrar e baixar a versão completa da biblioteca, no estágio de desenvolvimento atual. Os arquivos devem estar localizados nas mesmas pastas que o arquivo baixado.
Nós continuamos a desenvolver a tabela renderizada. Vamos enumerar os novos recursos a serem adicionados a ela.
- Ordenação dos dados da tabela.
- Gestão do número de colunas e linhas: adicionar e remover as colunas e linhas em um índice especificado; limpar totalmente a tabela (deixando apenas uma coluna e uma linha); reconstrução da tabela (limpa a tabela e define novas dimensões).
- Extensão do gerenciamento de usuários sobre a interface gráfica: adicionado a manipulação de um evento de clique duplo do mouse.
- Nós vamos começar a adicionar controles às células de tabela: caixas de seleção e botões.
Se você já criou interfaces gráficas com a ajuda desta biblioteca e utiliza as tabelas do tipo CTable para exibir os dados, agora você está aconselhado a passar para as tabelas do tipo CCanvasTable. A partir deste artigo, ele foi completamente igualado com as outros tipos de tabela nesta biblioteca, chegando a superá-las em alguns aspectos.
A Tabela Ordenada
A maioria dos métodos para ordenação dos dados da tabela são as mesmas que na CTable. O artigo Interfaces Gráficas X: Os Controles Horário, Lista de Caixas de Seleção e Tabela Ordenada descreve como tudo isso é organizado em detalhes. Aqui, as mudanças e adições relacionadas as tabelas do tipo CCanvasTable serão mencionadas resumidamente.
Para desenhar o índice da tabela ordenada, será necessário um array estático do tipo CTImage com dois elementos. Ele irá conter os caminhos das imagens para exibir a direção da ordenação. Se o usuário não tinha definido uma imagem personalizada, então os ícones padrão serão utilizados. A inicialização com os valores padrão e o preenchimento dos arrays de imagem são executados no método CCanvasTable::CreateHeaders() para criar os cabeçalhos.
//+------------------------------------------------------------------+ //| Classe para a criação de uma tabela renderizada | //+------------------------------------------------------------------+ class CCanvasTable : public CElement { private: //--- Ícones para o sinal de dados ordenados CTImage m_sort_arrows[2]; //--- public: //--- Ajuste dos ícones para o sinal de dados 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; } }; //+------------------------------------------------------------------+ //| Construtor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) { ... //--- Inicialização da estrutura do sinal de ordenação m_sort_arrows[0].m_bmp_path=""; m_sort_arrows[1].m_bmp_path=""; } //+------------------------------------------------------------------+ //| Cria os cabeçalhos da tabela | //+------------------------------------------------------------------+ bool CCanvasTable::CreateHeaders(void) { //--- Sai, se os cabeçalhos são desativados if(!m_show_headers) return(true); //--- Elaborando o nome do objeto string name=CElementBase::ProgramName()+"_table_headers_"+(string)CElementBase::Id(); //--- Coordenadas int x =m_x+1; int y =m_y+1; //--- Define os ícones como um sinal de uma possível ordenação da tabela 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__," > erro: ",::GetLastError()); } } //--- Cria um 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__," > Falha na criação da tela para desenhar os cabeçalhos da tabela: ",::GetLastError()); return(false); } //--- Anexa ao gráfico //--- Define as propriedades //--- Coordenadas //--- Armazena o tamanho //--- Margens da borda do painel //--- Armazena o ponteiro de objeto //--- Define o tamanho da área visível //--- Ajusta o deslocamento do quadro dentro da imagem ao longo do eixos X e Y ... return(true); }
O método CCanvasTable::DrawSignSortedData() será utilizado para desenhar o sinal de um array ordenado. Este elemento é desenhado somente se o (1) modo de ordenação está habilitado e a (2) ordenação dos dados tabela já foi realizada. Os deslocamentos podem ser controlados usando os métodos CCanvasTable::SortArrowXGap() e CCanvasTable::SortArrowYGap(). O ícone indica que a ordenação por ordem ascendente terá o índice 0 e em ordem decrescente - 1. Em seguida, há um loop duplo para desenhar a imagem. Este tópico já foi discutido em detalhes.
class CCanvasTable : public CElement { private: //--- Deslocamentos para o ícone do sinal de dados ordenados int m_sort_arrow_x_gap; int m_sort_arrow_y_gap; //--- public: //--- Deslocamentos para o sinal da tabela 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: //--- Desenha o sinal da possibilidade de ordenar a tabela void DrawSignSortedData(void); }; //+------------------------------------------------------------------+ //| Construtor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) : m_sort_arrow_x_gap(20), m_sort_arrow_y_gap(6) { ... } //+------------------------------------------------------------------+ //| Desenha o sinal da possibilidade de ordenar a tabela | //+------------------------------------------------------------------+ void CCanvasTable::DrawSignSortedData(void) { //--- Sai, se (1) a ordenação está desativada ou (2) ainda não foi realizada if(!m_is_sort_mode || m_is_sorted_column_index==WRONG_VALUE) return; //--- Calcula as coordenadas int x =m_columns[m_is_sorted_column_index].m_x2-m_sort_arrow_x_gap; int y =m_sort_arrow_y_gap; //--- O ícone selecionado para a direção da ordenação int image_index=(m_last_sort_direction==SORT_ASCEND)? 0 : 1; //--- Desenha 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++) { //--- Se não há cor, vai para o próximo pixel if(m_sort_arrows[image_index].m_image_data[i]<1) continue; //--- Obter a cor da camada inferior (fundo do cabeçalho) e cor do pixel especificado do ícone uint background =m_headers.PixelGet(x+lx,y+ly); uint pixel_color =m_sort_arrows[image_index].m_image_data[i]; //--- Mistura as cores uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); //--- Desenha o pixel da sobreposição do ícone m_headers.PixelSet(x+lx,y+ly,foreground); } } }
O método CCanvasTable::Swap() é usado para trocar os valores da tabela enquanto ocorre a ordenação. A diferença aqui é que é necessário mover não só o texto e/ou as imagens exibidas na célula, mas também um grande número de propriedades da célula. Por razões de conveniência, e para eliminar os fragmentos de código duplicados, um método auxiliar CCanvasTable::imagecopy() será necessário quando se deslocam as imagens. Faz-se passar dois arrays do tipo CTImage — receptor e origem dos dados. O índice da imagem copiada é transmitida como o terceiro argumento, uma vez que uma única célula pode conter várias imagens.
class CCanvasTable : public CElement { private: //--- Copia os dados da imagem de um array para o outro void ImageCopy(CTImage &destination[],CTImage &source[],const int index); }; //+------------------------------------------------------------------+ //| Copia os dados da imagem de um array para o outro | //+------------------------------------------------------------------+ void CCanvasTable::ImageCopy(CTImage &destination[],CTImage &source[],const int index) { //--- Copia os pixels da imagem ::ArrayCopy(destination[index].m_image_data,source[index].m_image_data); //--- Copia as propriedades da imagem 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; }
O código resultante do método CCanvasTable::Swap() é exibido abaixo. Antes de mover as imagens, primeiro é necessário verificar se elas estão lá, e caso contrário, ir para a próxima coluna. Se uma das células contém imagens, então, elas são movidas usando o método CCanvasTable::Imagecopy(). Esta operação é a mesma que no caso de mover outras propriedades da célula. Ou seja, primeiro armazena a propriedade da primeira célula. Então, esta propriedade é copiada da segunda célula para a primeira célula. Finalmente, o valor previamente armazenado da primeira célula é movida para o lugar da segunda célula.
class CCanvasTable : public CElement { private: //--- Troca os valores nas células especificadas void Swap(uint r1,uint r2); }; //+------------------------------------------------------------------+ //| Troca os elementos | //+------------------------------------------------------------------+ void CCanvasTable::Swap(uint r1,uint r2) { //--- Itera sobre todas as colunas em um loop for(uint c=0; c<m_columns_total; c++) { //--- Troca o 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; //--- Troca o texto curto 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; //--- Troca o número de casas decimais 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; //--- Troca a cor do 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; //--- Troca o índice do ícone selecionado 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; //--- Verifica se as células contêm imagens 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); //--- Vai para a próxima coluna, se ambas as células não têm imagens if(r1_images_total<1 && r2_images_total<1) continue; //--- Troca as imagens 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); } }
Clicando em um dos cabeçalhos da tabela chama-se o método CCanvasTable::OnClickHeaders(), que começa a ordenação dos dados. Ele é significativamente mais simples nesta classe do que em uma tabela do tipo CTable: Compare e veja você mesmo.
class CCanvasTable : public CElement { private: //--- Manipulação do clique em um cabeçalho bool OnClickHeaders(const string clicked_object); }; //+------------------------------------------------------------------+ //| Manipulação do clique em um c | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickHeaders(const string clicked_object) { //--- Sai, se (1) o modo de ordenação é desativado ou (2) no processo de alteração da largura da coluna if(!m_is_sort_mode || m_column_resize_control!=WRONG_VALUE) return(false); //--- Sai, se a barra de rolagem está ativa if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) return(false); //--- Sai, se ele tem um nome de objeto diferente if(m_headers.Name()!=clicked_object) return(false); //--- Para a determinação do índice da coluna uint column_index=0; //--- Obtém a coordenada X relativa abaixo do cursor do mouse int x=m_mouse.RelativeX(m_headers); //--- Determina o cabeçalho clicado for(uint c=0; c<m_columns_total; c++) { //--- Se o cabeçalho for encontrado, armazena o seu índice if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2) { column_index=c; break; } } //--- Ordena os dados de acordo com a coluna especificada SortData(column_index); //--- Envia uma mensagem sobre ele ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index))); return(true); }
Os métodos restantes para a ordenação dos dados são mantidos inalterados, não tendo nenhuma diferença dos dados anteriormente considerados. Portanto, somente uma lista com as declarações desses campos e métodos na classe CCanvasTable serão consideradas:
class CCanvasTable : public CElement { private: //--- Modo de ordenação dos dados de acordo com as colunas bool m_is_sort_mode; //--- Índice da coluna classificada (WRONG_VALUE - tabela não foi ordenada) int m_is_sorted_column_index; //--- Direção da última ordenação ENUM_SORT_MODE m_last_sort_direction; //--- public: //--- Modo de ordenação dos dados void IsSortMode(const bool flag) { m_is_sort_mode=flag; } //--- Obtém/define o tipo de dados ENUM_DATATYPE DataType(const uint column_index); void DataType(const uint column_index,const ENUM_DATATYPE type); //--- Ordena os dados de acordo com a coluna especificada void SortData(const uint column_index=0); //--- private: //--- Manipulação do clique em um cabeçalho bool OnClickHeaders(const string clicked_object); //--- Método Quicksort void QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND); //--- Verificação das condições de ordenação bool CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction); };
A ordenação nestes tipo de tabelas é demonstrado a seguir:
Fig. 1. Demonstração da classificação das tabelas do tipo CCanvasTable.
Adição e remoção de colunas e linhas
Um dos artigos anteriores considerou os métodos para adicionar e remover colunas e linhas para as tabelas do tipo CTable. Essa versão permitia a adição apenas no final da tabela. Este incômodo vai ser agora corrigido: vamos adicionar uma coluna ou linha com a possível indicação do seu índice a ser adicionado.
Exemplo: adicione uma coluna para o início de uma tabela com dados: ou seja, a nova coluna deve tornar-se a primeira (índice 0). O array de colunas deve ser incrementado por um elemento, enquanto que as propriedades e valores de todas as células da tabela devem ser deslocadas uma célula para a direita, deixando apenas as células da nova coluna em branco. O mesmo princípio se aplica ao adicionar as linhas à tabela.
O princípio reverso funciona quando é necessário remover uma coluna ou linha. Vamos tentar remover a primeira coluna do exemplo anterior: em primeiro lugar, os dados e as propriedades das células serão deslocadas para a esquerda por um elemento, e depois disso, o tamanho do array de colunas será reduzido por um elemento.
Aqui, os métodos auxiliares têm sido implementados, a fim de facilitar a criação dos métodos para adicionar e remover as colunas e linhas. Os métodos CCanvasTable::ColumnInitialize() e CCanvasTable::CellInitialize() serão usados para inicializar as células, adicionar colunas e linhas, com os valores padrão.
class CCanvasTable : public CElement { private: //--- Inicializa a coluna especificada com os valores padrão void ColumnInitialize(const uint column_index); //--- Inicializa a célula especificada com os valores padrão void CellInitialize(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Inicializa a coluna especificada com os valores padrão | //+------------------------------------------------------------------+ void CCanvasTable::ColumnInitialize(const uint column_index) { //--- Inicializa as propriedades da coluna com os valores padrão 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 =""; } //+------------------------------------------------------------------+ //| Inicializa a célula especificada com os valores padrão | //+------------------------------------------------------------------+ 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 padrão, as células não contêm imagens ::ArrayFree(m_columns[column_index].m_rows[row_index].m_images); }
Para copiar as propriedades de uma coluna para a outra coluna, o método CCanvasTable::ColumnCopy() deve ser utilizado. Seu código é fornecido abaixo:
class CCanvasTable : public CElement { private: //--- Faz uma cópia da coluna especificada (origem) para uma nova localização (dest.) void ColumnCopy(const uint destination,const uint source); }; //+------------------------------------------------------------------------------+ //| Faz uma cópia da coluna especificada (origem) para a nova localização (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; }
O método CCanvasTable::CellCopy() copia uma célula especificada para outra. Para fazer isso, é necessário passar os índices da coluna e linha da célula receptora e os índices da coluna e linha da célula de origem.
class CCanvasTable : public CElement { private: //--- Faz uma cópia da célula especificada (origem) para uma nova localização (dest.) void CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source); }; //+------------------------------------------------------------------------------+ //| Faz uma cópia da célula especificada (origem) para a nova localização (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; //--- Copia o tamanho do array da origem para o 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++) { //--- Copia, se há imagens if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<1) continue; //--- faz uma cópia da imagem ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i); } }
Como parte de eliminar o código repetido a partir do método, um outro método simples foi implementado dentro deste método, que será chamado após as alterações feitas na reconstrução da tabela. Cada vez que linhas e colunas são adicionadas e removidas, a tabela deve ser recalculada e redimensionada, e depois disso, a tabela tem de ser redesenhada para refletir estas alterações. O método CCanvasTable::RecalculateAndResizeTable() será utilizado para esta finalidade. Aqui, o único parâmetro indica se é necessário redesenhar completamente a tabela (true) ou simplesmente atualizá-la (false).
class CCanvasTable : public CElement { private: //--- Recalcula levando em consideração as recentes mudanças e redimensiona a tabela void RecalculateAndResizeTable(const bool redraw=false); }; //+-------------------------------------------------------------------------------+ //| Calcula levando em consideração as recentes mudanças e redimensiona a tabela | //+-------------------------------------------------------------------------------+ void CCanvasTable::RecalculateAndResizeTable(const bool redraw=false) { //--- Calcula o tamanho da tabela CalculateTableSize(); //--- Redimensiona a tabela ChangeTableSize(); //--- Atualiza a tabela UpdateTable(redraw); }
Os métodos para adicionar e remover linhas e colunas são muito mais simples e mais fáceis de entender do que as versões desses métodos nas tabelas do tipo CTable. Você pode comparar o código destes métodos sozinho. Aqui, apenas o código dos métodos para adicionar e remover colunas será dado como um exemplo. O código para trabalhar com as linhas tem a mesma sequência de ações, tal como descrito anteriormente no início desta seção. Atenção: o sinal da tabela ordenada também se move em conjunto com a coluna ordenada tanto para a adição quanto para a remoção. No caso em que uma coluna ordenada é removida, o campo que contém o índice da coluna ordenada é zerada.
class CCanvasTable : public CElement { public: //--- Adiciona uma coluna à tabela no índice especificado void AddColumn(const int column_index,const bool redraw=false); //--- Remove uma coluna da tabela no índice especificado void DeleteColumn(const int column_index,const bool redraw=false); }; //+------------------------------------------------------------------+ //| Adiciona uma coluna à tabela no índice especificado | //+------------------------------------------------------------------+ void CCanvasTable::AddColumn(const int column_index,const bool redraw=false) { //--- Aumenta o tamanho do array por um elemento int array_size=(int)ColumnsTotal(); m_columns_total=array_size+1; ::ArrayResize(m_columns,m_columns_total); //--- Ajusta o tamanho dos arrays de linhas ::ArrayResize(m_columns[array_size].m_rows,m_rows_total); //--- Ajuste do índice no caso de exceder o intervalo int checked_column_index=(column_index>=(int)m_columns_total)? (int)m_columns_total-1 : column_index; //--- Desloca as outras colunas (começando da extremidade do array até o índice da coluna adicionada) for(int c=array_size; c>=checked_column_index; c--) { //--- Muda o sinal do array ordenado if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE) m_is_sorted_column_index++; //--- Índice da coluna anterior int prev_c=c-1; //--- inicializa a nova coluna com os valores padrão if(c==checked_column_index) ColumnInitialize(c); //--- Move os dados da coluna anterior para a coluna atual else ColumnCopy(c,prev_c); //--- for(uint r=0; r<m_rows_total; r++) { //--- Inicializa as novas células da coluna com os valores padrão if(c==checked_column_index) { CellInitialize(c,r); continue; } //--- Move os dados de uma célula da coluna anterior para a célula de coluna atual CellCopy(c,r,prev_c,r); } } //--- Calcula e redimensiona a tabela RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Remove uma coluna da tabela no índice especificado | //+------------------------------------------------------------------+ void CCanvasTable::DeleteColumn(const int column_index,const bool redraw=false) { //--- Obtém o tamanho do array de colunas int array_size=(int)ColumnsTotal(); //--- Ajuste do índice no caso de exceder o intervalo int checked_column_index=(column_index>=array_size)? array_size-1 : column_index; //--- Desloca outras colunas (começando a partir do índice especificado da última coluna) for(int c=checked_column_index; c<array_size-1; c++) { //--- Muda o sinal do 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--; } //--- Zera, se uma coluna ordenada foi removida else m_is_sorted_column_index=WRONG_VALUE; //--- Índice da próxima coluna int next_c=c+1; //--- Move os dados da próxima coluna para a coluna atual ColumnCopy(c,next_c); //--- Move os dados das células da próxima coluna para as células da coluna atual for(uint r=0; r<m_rows_total; r++) CellCopy(c,r,next_c,r); } //--- Diminui o array de colunas por um elemento m_columns_total=array_size-1; ::ArrayResize(m_columns,m_columns_total); //--- Calcula e redimensiona a tabela RecalculateAndResizeTable(redraw); }
Os métodos para reconstruir e limpar totalmente a tabela também são muito simples, ao contrário dos métodos similares nas tabelas do tipo CTable. Aqui, é suficiente definir o novo tamanho chamando o método CCanvasTable::TableSize(). O tamanho mínimo é definido ao limpar a tabela, deixando apenas uma coluna e uma linha. Os campos que contêm os valores da linha selecionada, direção da ordenação e índice da coluna ordenada também são zeradas ao limpar. Os novos tamanhos da tabela são calculados e definidos no final de ambos os métodos.
class CCanvasTable : public CElement { public: //--- Reconstrução da tabela void Rebuilding(const int columns_total,const int rows_total,const bool redraw=false); //--- Limpa a tabela. Apenas uma coluna e uma linha estão à esquerda. void Clear(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Reconstrução da tabela | //+------------------------------------------------------------------+ void CCanvasTable::Rebuilding(const int columns_total,const int rows_total,const bool redraw=false) { //--- Define o novo tamanho TableSize(columns_total,rows_total); //--- Calcula e redimensiona a tabela RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Limpa a tabela. Será deixado apenas uma coluna e uma linha. | //+------------------------------------------------------------------+ void CCanvasTable::Clear(const bool redraw=false) { //--- Define o tamanho mínimo para 1x1 TableSize(1,1); //--- Define os valores padrão m_selected_item_text =""; m_selected_item =WRONG_VALUE; m_last_sort_direction =SORT_ASCEND; m_is_sorted_column_index =WRONG_VALUE; //--- Calcula e redimensiona a tabela RecalculateAndResizeTable(redraw); }
É assim que ele funciona:
Fig. 2. Demonstração do gerenciamento das dimensões da tabela.
O aplicativo de teste apresentado na animação acima pode ser baixado no final do artigo.
Evento do clique duplo do botão esquerdo do mouse
Às vezes pode ser necessário chamar um determinado evento por um clique duplo. Vamos implementar a geração de um evento como esse na biblioteca. Para fazer isso, adicione o identificador ON_DOUBLE_CLICK para o evento do clique duplo do botão esquerdo do mouse para o arquivo Defines.mqh:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ ... #define ON_DOUBLE_CLICK (34) // Left mouse button double click ...
Evento com o identificador ON_DOUBLE_CLICK será gerado na classe CMouse. Nos sistemas operacionais, este evento geralmente é gerado pelas ações “pressionar-soltar-pressionar” do botão esquerdo do mouse. Mas os testes no ambiente do terminal mostrou que nem sempre é possível traçar imediatamente um evento de pressionar o botão esquerdo do mouse com a chegada do evento CHARTEVENT_MOUSE_MOVE (parâmetro sparam). Assim, foi decidido implementar a geração do evento através das ações “pressionar-soltar-pressionar”. Estas ações podem ser controladas com precisão com a chegada do evento CHARTEVENT_CLICK.
Por padrão, pelo menos 300 milissegundos devem passar entre dois cliques. Para acompanhar o evento CHARTEVENT_CLICK, o método CMouse::CheckDoubleClick() será chamado no manipulador de eventos do mouse. Duas variáveis estáticas são declaradas no início deste método, onde os valores do tick atual e anterior (o número de milissegundos que se passaram desde o início do sistema) será armazenado em cada chamada para o método. Se o número de milissegundos entre estes valores é menor do que o especificado no campo m_pause_between_clicks, então, o evento do clique duplo do botão esquerdo do mouse será gerado.
//+------------------------------------------------------------------+ //| Classe para obter os parâmetros do mouse | //+------------------------------------------------------------------+ class CMouse { private: //--- Botão de pausa dos cliques no botão esquerdo do mouse (para determinar o duplo clique) uint m_pause_between_clicks; //--- private: //--- Verifica o clique duplo do botão esquerdo do mouse bool CheckDoubleClick(void); }; //+------------------------------------------------------------------+ //| Construtor | //+------------------------------------------------------------------+ CMouse::CMouse(void) : m_pause_between_clicks(300) { ... } //+------------------------------------------------------------------+ //| Manipulação de eventos do mouse | //+------------------------------------------------------------------+ void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Manipulação de eventos do clique no gráfico if(id==CHARTEVENT_CLICK) { //--- Verifica o clique duplo do botão esquerdo do mouse CheckDoubleClick(); return; } } //+------------------------------------------------------------------+ //| Verifica o clique duplo do botão esquerdo do mouse | //+------------------------------------------------------------------+ void CMouse::CheckDoubleClick(void) { static uint prev_depressed =0; static uint curr_depressed =::GetTickCount(); //--- Atualiza os valores prev_depressed =curr_depressed; curr_depressed =::GetTickCount(); //--- Determina o tempo entre os cliques uint counter=curr_depressed-prev_depressed; //--- Se o tempo decorrido entre os dois cliques é menor do que o especificado, envia uma mensagem sobre um clique duplo if(counter<m_pause_between_clicks) ::EventChartCustom(m_chart.ChartId(),ON_DOUBLE_CLICK,counter,0.0,""); }
Assim, os manipuladores de eventos de todas as classes da biblioteca permitem rastrear o clique duplo do botão esquerdo do mouse em qualquer lugar na área do gráfico, independentemente da existência de um objeto gráfico sob o cursor.
Controles nas células da tabela
Este artigo irá iniciar o tema dos controles nas células da tabela. Por exemplo, esse recurso pode ser necessário quando é necessário criar um sistema especialista multi-parâmetro. É conveniente implementar a interface gráfica deste sistema como uma tabela. Vamos começar a adicionar esses controles para as células da tabela com as caixas de seleção e botões.
Primeiramente, isso vai exigir uma nova enumeração ENUM_TYPE_CELL, onde os tipos de células serão definidos:
//+------------------------------------------------------------------+ //| Enumeração dos tipos de células da tabela | //+------------------------------------------------------------------+ enum ENUM_TYPE_CELL { CELL_SIMPLE =0, CELL_BUTTON =1, CELL_CHECKBOX =2 };
Por padrão, durante a inicialização as células da tabela são atribuídas ao tipo simples - CELL_SIMPLE, significando “célula sem controle”. Para definir ou obter o tipo de célula, use os métodos CCanvasTable::CellType(), o seu código é fornecido abaixo. Se as células são atribuídas ao tipo CELL_CHECKBOX (caixa de seleção), também é necessário definir as imagens para os estados da caixa de seleção. O número mínimo de imagens para este tipo de célula é dois. É possível configurar mais de dois ícones, isso permitirá trabalhar com caixas de seleção multi-parâmetros.
class CCanvasTable : public CElement { //--- Definindo/obtendo o tipo de célula 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); }; //+------------------------------------------------------------------+ //| Define o tipo de célula | //+------------------------------------------------------------------+ void CCanvasTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type) { //--- Verifica se o tamanho do array não excedeu if(!CheckOutOfRange(column_index,row_index)) return; //--- Define o tipo de célula m_columns[column_index].m_rows[row_index].m_type=type; } //+------------------------------------------------------------------+ //| Obtém o tipo de célula | //+------------------------------------------------------------------+ ENUM_TYPE_CELL CCanvasTable::CellType(const uint column_index,const uint row_index) { //--- Verifica se o tamanho do array não excedeu if(!CheckOutOfRange(column_index,row_index)) return(WRONG_VALUE); //--- Retorna o tipo de dados para a coluna especificada return(m_columns[column_index].m_rows[row_index].m_type); }
As séries de imagens para as caixas de seleção e botões podem ter tamanhos diferentes, portanto, é necessário ter a possibilidade de definir os deslocamentos X e Y das imagens para cada coluna separadamente. Isto pode ser feito usando os métodos CCanvasTable::ImageXOffset() e CCanvasTable::ImageYOffset():
class CCanvasTable : public CElement { public: //--- Deslocamento das imagens ao longo do eixos X e Y void ImageXOffset(const int &array[]); void ImageYOffset(const int &array[]); };
Outra adição é o modo para a desativação da remoção da seleção de uma linha quando clicada novamente. Este modo só funciona se o modo de seleção da linha é habilitada.
class CCanvasTable : public CElement { private: //--- Sem remoção da seleção da linha quando clicada novamente bool m_is_without_deselect; //--- public: //--- O modo "Sem remoção da seleção da linha quando clicada novamente" void IsWithoutDeselect(const bool flag) { m_is_without_deselect=flag; } };
Os métodos separados CCanvasTable::PressedRowIndex() e CCanvasTable::PressedCellColumnIndex() são usados agora para a determinação do índice da coluna e linha clicada. Eles foram considerados antes como blocos do método CCanvasTable::OnClickTable(). Seu código completo com novas adições podem ser encontradas nos arquivos anexados ao artigo. Deve-se notar, separadamente, que usando esses dois métodos em conjunto ajuda a determinar a célula clicada com o botão esquerdo do mouse. Em seguida, considere onde os índices da coluna e linha recebidos serão passados.
class CCanvasTable : public CElement { private: //--- Retorna o índice da linha clicada int PressedRowIndex(void); //--- Retorna o índice da coluna da célula clicada int PressedCellColumnIndex(void); };
Quando uma célula da tabela é clicada, é necessário obter o seu tipo, e se ela contém um controle, então, também é necessário determinar se ela está ativada. Para isso, foi implementado uma série de métodos privados, o principal deles é o CCanvasTable::CheckCellElement(). Ele passa os índices da coluna e linha recebidos dos métodos CCanvasTable::PressedRowIndex() e CCanvasTable::PressedCellColumnIndex(). O método tem dois modos. Dependendo do tipo de evento manipulado quando o método é chamado, o terceiro parâmetros é usado, especificando se o evento é de clique duplo.
Depois disso, o tipo de célula é verificado, e o método apropriado é chamado dependendo do seu tipo. Se uma célula contém um controle “Button”, então o método CCanvasTable::CheckPressedButton() será chamado. O método CCanvasTable::CheckPressedCheckBox() destina-se a células com caixas de seleção.
class CCanvasTable : public CElement { private: //--- Verifica se o controle da célula foi ativado quando clicado bool CheckCellElement(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| Verifica se o controle da célula foi ativado quando clicado | //+------------------------------------------------------------------+ bool CCanvasTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false) { //--- Sai, se a célula não tem controles 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) { //--- Se for uma célula de botão case CELL_BUTTON : { if(!CheckPressedButton(column_index,row_index,double_click)) return(false); //--- break; } //--- Se for uma célula da caixa de seleção case CELL_CHECKBOX : { if(!CheckPressedCheckBox(column_index,row_index,double_click)) return(false); //--- break; } } //--- return(true); }
Vamos ver a estrutura dos métodos CCanvasTable::CheckPressedButton() e CCanvasTable::CheckPressedCheckBox(). O número de imagens na célula é verificado no início de ambos os métodos. As células de botão devem conter pelo menos um ícone, e as células checkbox 0, pelo menos dois. Então é necessário determinar se esta é a imagem que foi clicada. No caso da caixa de seleção, foi implementado duas maneiras para alternar ele. Um único clique só funcionará se o ícone com a caixa de seleção for clicada. Um clique duplo em qualquer lugar na célula alterna a caixa de seleção. Ambos os métodos geram um evento com o identificador correspondente para o controle, desde que estejam reunidas todas as condições. O índice da imagem é transmitido como um parâmetro do tipo double, e o parâmetro do tipo string forma uma string com os índices da coluna e da fila.
class CCanvasTable : public CElement { private: //--- Verifica se o botão na célula foi clicada bool CheckPressedButton(const int column_index,const int row_index,const bool double_click=false); //--- Verifica se a caixa de seleção na célula foi clicada bool CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| Verifica se o botão na célula foi clicado | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedButton(const int column_index,const int row_index,const bool double_click=false) { //--- Sai, se não houver imagens na célula if(ImagesTotal(column_index,row_index)<1) { ::Print(__FUNCTION__," > Atribuí pelo menos uma imagem para o botão da célula!"); return(false); } //--- Obtém as coordenadas relativas sob o cursor do mouse int x=m_mouse.RelativeX(m_table); //--- Obtém a borda direita da imagem 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); //--- Sai, se o clique não foi na imagem if(x>image_x2) return(false); else { //--- Se este não é um clique duplo, envia uma mensagem 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); } //+------------------------------------------------------------------+ //| Verifica se a caixa de seleção na célula foi clicada | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false) { //--- Sai, se não houver imagens na célula if(ImagesTotal(column_index,row_index)<2) { ::Print(__FUNCTION__," > Atribuí pelo menos uma imagem para o botão da célula!"); return(false); } //--- Obtém as coordenadas relativas sob o cursor do mouse int x=m_mouse.RelativeX(m_table); //--- Obtém a borda direita da imagem 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); //--- Sai, se (1) o clique não foi na imagem e (2) não foi um clique duplo if(x>image_x2 && !double_click) return(false); else { //--- Índice atual da imagem selecionada int image_i=m_columns[column_index].m_rows[row_index].m_selected_image; //--- Determina o próximo índice para a imagem int next_i=(image_i<ImagesTotal(column_index,row_index)-1)? ++image_i : 0; //--- Seleciona a imagem seguinte e atualiza a tabela ChangeImage(column_index,row_index,next_i,true); m_table.Update(false); //--- Envia uma mensagem sobre ele ::EventChartCustom(m_chart_id,ON_CLICK_CHECKBOX,CElementBase::Id(),next_i,string(column_index)+"_"+string(row_index)); } //--- return(true); }
Como resultado, o código dos métodos CCanvasTable::OnClickTable() e CCanvasTable::OnDoubleClickTable() para lidar com o clique sobre a tabela se tornou muito mais compreensível e legível (veja o código abaixo). Um evento correspondente é gerado de acordo com os modos ativado e o tipo da célula que foi clicado.
class CCanvasTable : public CElement { private: //--- Manipulação do clique da tabela bool OnClickTable(const string clicked_object); //--- Manipulação do clique duplo sobre a tabela bool OnDoubleClickTable(const string clicked_object); }; //+------------------------------------------------------------------+ //| Manipulação do clique sobre a tabela | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickTable(const string clicked_object) { //--- Sai, se (1) o modo de seleção da linha é desativado ou (2) no processo de alterar a largura da coluna if(m_column_resize_control!=WRONG_VALUE) return(false); //--- Sai, se a barra de rolagem está ativa if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) return(false); //--- Sai, se ele tem um nome de objeto diferente if(m_table.Name()!=clicked_object) return(false); //--- Determina a linha clicada int r=PressedRowIndex(); //--- Determina a célula clicada int c=PressedCellColumnIndex(); //--- Verifica se o controle na célula foi ativado bool is_cell_element=CheckCellElement(c,r); //--- Se (1) o modo de seleção da linha é ativado e (2) o controlo de células não é ativado if(m_selectable_row && !is_cell_element) { //--- Altera a cor RedrawRow(true); m_table.Update(); //--- Envia uma mensagem sobre ele ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,string(c)+"_"+string(r)); } //--- return(true); } //+------------------------------------------------------------------+ //| Manipulação do clique duplo sobre a tabela | //+------------------------------------------------------------------+ bool CCanvasTable::OnDoubleClickTable(const string clicked_object) { if(!m_table.MouseFocus()) return(false); //--- Determina a linha clicada int r=PressedRowIndex(); //--- Determina a célula clicada int c=PressedCellColumnIndex(); //--- Verifica se o controle na célula foi ativado e retorna o resultado return(CheckCellElement(c,r,true)); }
Eventos do clique duplo do botão esquerdo do mouse em uma célula são tratados no manipulador de eventos do controle no evento ON_DOUBLE_CLICK:
//+------------------------------------------------------------------+ //| Manipulador de eventos | //+------------------------------------------------------------------+ void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Manipulação do clique duplo do botão esquerdo do mouse if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK) { //--- Clicando na tabela if(OnDoubleClickTable(sparam)) return; //--- return; } }
No final, tudo funciona assim:
Fig. 3. A demonstração da interação com os controles nas células da tabela.
Os aplicativos em destaque no artigo podem ser baixados através do link abaixo.
Conclusão
A esquemática da biblioteca para a criação das interfaces gráficas no atual estágio de desenvolvimento é parecido com a imagem abaixo:
Fig. 4. Estrutura da biblioteca no atual estágio de desenvolvimento.
Abaixo, você pode baixar a versão mais recente da biblioteca e seus arquivos de teste.
Se você tiver dúvidas sobre a utilização do material a partir desses arquivos, você poderá consultar a descrição detalhada do desenvolvimento da biblioteca em um dos artigos da lista abaixo ou fazer sua pergunta nos comentários deste artigo.