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

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.

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.

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.

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.

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.