Interfaces gráficas X: Algoritmo de quebra de linha na caixa de texto multilinha (build 12)

Anatoli Kazharski | 17 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.

Este artigo continua o desenvolvimento do controle Caixa de Texto Multilinha. Os progressos anteriores podem ser vistos no artigo Interfaces Gráficas X: O controle Caixa de Texto Multilinha (build 8). Desta vez, nossa tarefa é implementar um quebra automático de linha no caso da largura da caixa de texto ser excedida ou uma quebra automática de linha inversa do texto para a linha anterior se a oportunidade surgir.


Modo de quebra automática de linha em uma caixa de texto de multilinha

Todos os editores de texto ou aplicativos apresentam a quebra automática de linha para trabalhar com informações de texto, caso o texto exceda a largura da área de aplicação. Isso livra o incômodo de ter que usar a barra de rolagem horizontal o tempo todo. 

O modo quebra automática de linha será desativado por padrão. Para ativar este modo, use o CTextBox::WordWrapMode(). Este é o único método público na implementação da quebra automática de linha. Todos os outros serão privados, sua organização será discutida em detalhes abaixo.

//+------------------------------------------------------------------+
//| Classe para a criação de uma caixa de texto multilinha           |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- O modo Quebra automática de linha
   bool m_word_wrap_mode;
   //---
public:
   //--- O modo Quebra automática de linha
   void WordWrapMode(const bool mode) { m_word_wrap_mode=mode; }
  };
//+------------------------------------------------------------------+
//| Construtor                                                       |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_word_wrap_mode(false)


Para configurar a quebra automática de linha e adição de texto a uma linha, é necessário que cada linha tenha um sinal de sua extremidade.

Aqui está um exemplo simples com uma única linha. Abra qualquer editor de texto, onde o modo quebra automática de linha pode ser ativado/desativado — por exemplo, Bloco de Notas. Adicione uma linha a um documento:

Google is an American multinational technology company specializing in Internet-related services and products.

Se o modo de quebra automática de linha estiver desativado, dependendo da largura da caixa de texto, a linha poderá não caber na caixa de texto. Então, para ler a linha, a barra de rolagem horizontal terá que ser usada.

 Fig. 1. O modo de quebra automática de linha está desativado.

Fig. 1. O modo de quebra automática de linha está desativado.


Agora, habilite o modo de quebra automática de linha. A linha deve ajustar a largura da caixa de texto do editor:

 Fig. 2. O modo de quebra automática de linha está ativado.


Fig. 2. O modo de quebra automática de linha está ativado.


Pode-se ver que a sequência inicial foi dividida em três substrings, que foram consecutivamente alinhadas uma após a outra. Aqui, o sinal final está presente apenas na terceira substring. Ler a primeira linha deste arquivo através de um programa, ele retorna todo o texto até o sinal de fim da linha. 

Isso pode ser verificado usando um script simples:

//+------------------------------------------------------------------+
//| Script da função de início do programa                           |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- Obtém o identificador do arquivo
   int file=::FileOpen("Topic 'Word wrapping'.txt",FILE_READ|FILE_TXT|FILE_ANSI);
//--- Lê o arquivo se o identificador foi obtido
   if(file!=INVALID_HANDLE)
      ::Print(__FUNCTION__," > ",::FileReadString(file));
   else
      ::Print(__FUNCTION__," > erro: ",::GetLastError());
  }
//+------------------------------------------------------------------+

Resultado da leitura da primeira linha (neste caso, a única) e impressão para o log:

OnStart > Google is an American multinational technology company specializing in Internet-related services and products.

Para implementar tal leitura de informações da caixa de texto multilinha desenvolvida, adicione outra propriedade Bool para armazenar o sinal do fim da linha à estrutura StringOptions (anteriormente KeySymbolOptions) na classe CTextBox.

   //--- Caracteres e suas propriedades
   struct StringOptions
     {
      string            m_symbol[];    // Caracteres
      int               m_width[];     // Largura dos caracteres
      bool              m_end_of_line; // Sinal de fim de linha
     };
   StringOptions  m_lines[];

Vários métodos principais e auxiliares serão necessários para implementar a quebra automática de linha. Vamos enumerar suas tarefas.

Métodos principais:

  • Quebra automática de linha
  • Retorna os índices do primeiro caractere visível e espaço à direita
  • Retorna o número de caracteres a serem movidos
  • Quebra automática de linha para a próxima linha
  • Quebra automática de linha da próxima linha para a linha atual

Métodos auxiliares:

  • Retorna o número de palavras na linha especificada
  • Retorna o índice do caractere de espaço pelo seu número
  • Deslocamento de linhas
  • Deslocamento de caracteres na linha especificada
  • Copiando caracteres para o array passado para mover para a próxima linha
  • Colando caracteres do array passado para a linha especificada

Vejamos de perto a estrutura dos métodos auxiliares.


Descrição do algoritmo e métodos auxiliares

Há um momento em que o algoritmo de quebra automática de linha necessita iniciar um ciclo para encontrar o índice de um caractere de espaço por seu número. Para organizar tal ciclo, é necessário um método para determinar o número de palavras numa linha. Abaixo está o código do CTextBox::WordsTotal(), que executa esta tarefa.

Contar palavras é bastante simples. É necessário iterar sobre o array de caracteres da linha especificada, rastreando a aparência do padrão, onde o caractere atual não é um caractere de espaço (' '), enquanto que o anterior é. Isso indicará o início de uma nova palavra. O contador também aumenta se o final da linha for atingido, para que a última palavra não seja ignorada.

class CTextBox : public CElement
  {
private:
   //--- Retorna o número de palavras na linha especificada
   uint              WordsTotal(const uint line_index);
  };
//+------------------------------------------------------------------+
//| Retorna o número de palavras na linha especificada               |
//+------------------------------------------------------------------+
uint CTextBox::WordsTotal(const uint line_index)
  {
//--- Obtém o tamanho do array de linhas
   uint lines_total=::ArraySize(m_lines);
//--- Prevenção para exceder o tamanho do array
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Obtém o tamanho do array de caracteres para a linha especificada
   uint symbols_total=::ArraySize(m_lines[l].m_symbol);
//--- Contador de palavras
   uint words_counter=0;
//--- Busca um espaço no índice especificado
   for(uint s=1; s<symbols_total; s++)
     {
      //--- Contagem, se (2) atingiu o fim da linha ou (2) encontrou um espaço (fim da palavra)
      if(s+1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE))
         words_counter++;
     }
//--- Retorna o número de palavras
   return(words_counter);
  }


O método CTextBox::SymbolIndexBySpaceNumber() será usado para determinar o índice do caractere de espaço. Uma vez que este valor é obtido, é possível calcular a largura de uma ou mais palavras a partir do início de uma substring usando o método CTextBox::LineWidth(). 

Para maior clareza, considere um exemplo com uma linha de texto. Seus caracteres (azul), substrings (verde) e espaços (vermelho) foram indexados. Por exemplo, pode-se ver que o primeiro (0) espaço na primeira (0) linha tem um índice de caracteres igual a 6.

 Fig. 3. Índices de caracteres (azul), substrings (verde) e espaços (vermelho).

Fig. 3. Índices de caracteres (azul), substrings (verde) e espaços (vermelho).


Abaixo está o código do método CTextBox::SymbolIndexBySpaceNumber(). Aqui, tudo é simples: itere sobre todos os caracteres da substring especificada em um loop, aumentando o contador sempre que um novo caractere de espaço for encontrado. Se qualquer iteração mostrar que o contador é igual ao índice de espaço especificado no valor passado do segundo argumento, o valor do valor do índice de caracteres é armazenado e o ciclo é interrompido. Este é o valor retornado pelo método.

class CTextBox : public CElement
  {
private:
   //--- Retorna o índice de caractere de espaço por seu número 
   uint              SymbolIndexBySpaceNumber(const uint line_index,const uint space_index);
  };
//+------------------------------------------------------------------+
//| Retorna o índice de caractere de espaço por seu número           |
//+------------------------------------------------------------------+
uint CTextBox::SymbolIndexBySpaceNumber(const uint line_index,const uint space_index)
  {
//--- Obtém o tamanho do array de linhas
   uint lines_total=::ArraySize(m_lines);
//--- Prevenção para exceder o tamanho do array
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Obtém o tamanho do array de caracteres para a linha especificada
   uint symbols_total=::ArraySize(m_lines[l].m_symbol);
//--- (1) Para determinar o índice de caracteres de espaço e (2) do contador de espaços
   uint symbol_index  =0;
   uint space_counter =0;
//--- Busca um espaço no índice especificado
   for(uint s=1; s<symbols_total; s++)
     {
      /--- Se achou um espaço
      if(m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE)
        {
         //--- Se o contador for igual ao índice do espaço especificado, guarde-o e interrompe o ciclo
         if(space_counter==space_index)
           {
            symbol_index=s;
            break;
           }
         //--- Aumenta o contador de espaços
         space_counter++;
        }
     }
//--- Retorna o tamanho da linha se o índice de espaço não foi encontrado
   return((symbol_index<1)? symbols_total : symbol_index);
  }

Consideremos a parte do algoritmo quebra automática de linha relacionada ao movimento dos elementos dos arrays de linhas e caracteres. Vamos ilustrar isso em diferentes situações. Por exemplo, há uma linha:

The quick brown fox jumped over the lazy dog.

Esta linha não se encaixa na largura da caixa de texto. A área desta caixa de texto é mostrada na figura 4 por um retângulo vermelho. É evidente que a parte "excessiva" da linha — "over the lazy dog." — precisa ser movida para a próxima linha.

 Fig. 4. Situação com o transbordo da linha da caixa de texto.

Fig. 4. Situação com o transbordo da linha da caixa de texto.

Como o array dinâmico de linhas atualmente consiste em um único elemento, o array precisa ser incrementado por um elemento. O array de caracteres na nova linha deve ser definido de acordo com o número de caracteres do texto movido. Depois disso, a parte da linha que não se encaixa deve ser movida. O resultado final:

 Fig. 5. Uma parte da linha foi movida para a próxima nova linha.

Fig. 5. Uma parte da linha foi movida para a próxima nova linha.

Agora vamos ver como o algoritmo irá funcionar se a largura da caixa de texto diminuir em aproximadamente 30%. Aqui, ele também determina primeiro qual parte da primeira linha (índice 0) excede os limites da caixa de texto. Nesse caso, a substring 'fox jump' não se encaixava. Em seguida, o array dinâmico de linhas é incrementado por um elemento. Em seguida, todas as substrings localizadas abaixo são deslocadas para baixo por uma linha, liberando assim um lugar para o texto movido. Depois disso, a substring 'fox jumped' é movida para o lugar liberado, como foi descrito na passagem anterior. Esta etapa é mostrada na figura abaixo.

 Fig. 6. Movendo o texto para a segunda linha (índice 1).

Fig. 6. Movendo o texto para a segunda linha (índice 1).


O algoritmo vai para a próxima linha (índice 1) na próxima iteração do ciclo. Aqui, é necessário verificar se uma parte desta linha excede os limites da caixa de texto novamente. Se a verificação mostra que não excede, é necessário ver se esta linha tem espaço suficiente à direita para acomodar uma parte da próxima linha com o índice 2. Isso verifica as condições para a quebra automática de linha inversa do texto do início da próxima linha (índice 2) para o fim da atual (índice 1).

Além desta condição, é necessário verificar se a linha atual contém um sinal de quebra de linha. Se ele tiver, então a quebra automática de linha inversa não é executada. No exemplo, não há sinal de quebra de linha e há espaço suficiente para a quebra automática de linha inversa de uma palavra — 'over'. Durante uma quebra automática de linha inversa, o tamanho do array de caracteres é alterado pelo número de caracteres adicionados e extraídos das linhas atual e seguinte, respectivamente. Durante uma quebra automática de linha inversa, antes de alterar o tamanho do array de caracteres, os caracteres restantes são movidos para o início da linha. A figura abaixo demonstra este passo. 

 Fig. 7. Quebra automática de linha para a segunda linha (índice 1) da terceira linha (índice 2).

Fig. 7. Quebra automática de linha para a segunda linha (índice 1) da terceira linha (índice 2).


Pode-se ver que quando a área da caixa de texto se torna mais estreita, o envolvimento direto e inverso da palavra será executado. Por outro lado, quando a caixa de texto se estende, é executado a quebra automática de linha inversa para o espaço liberado. Toda vez que ocorre a quebra para a próxima linha, o array dinâmico é incrementado por um elemento. E toda vez que todo o texto restante da próxima linha é invertido, o array de linhas é decrementado por um elemento. Mas antes disso, caso haja mais linhas à frente, elas devem ser deslocadas para cima por uma linha, a fim de eliminar a formação de uma linha vazia quando a quebra invertida do texto restante. 

Todas estas etapas com o realinhamento da linha, a quebra automática de linha direta e inversa não serão vistas no decorrer do ciclo: A figura abaixo mostra um exemplo grosseiro do que os usuários verão ao trabalhar com a interface gráfica:

 Fig. 8. Demonstração do algoritmo de quebra automática de linha pelo exemplo de um editor de texto.

Fig. 8. Demonstração do algoritmo de quebra automática de linha pelo exemplo de um editor de texto.


E isso não é tudo. No caso de apenas uma palavra (sequência contínua de caracteres) é deixada em uma linha, a hifenização é realizada caractere por caractere. Esta situação é mostrada na figura abaixo:

 Fig. 9. Demonstração da quebra automática de linha caractere por caractere quando uma palavra não pode ser ajustada.


Fig. 9. Demonstração da quebra automática de linha caractere por caractere quando uma palavra não pode ser ajustada.

Agora considere os métodos para mover as linhas e caracteres. O CTextBox::MoveLines() será usado para mover linhas. O método recebe o índice das linhas, desde que e até que seja necessário as linhas serem deslocadas por uma posição. O terceiro parâmetro é a direção de deslocamento. Ele é definido para o deslocamento para baixo, por padrão. 

Anteriormente, o algoritmo de deslocamento de linha foi utilizado de forma não recorrente quando se controlava a caixa de texto utilizando a teclas 'Enter' e 'Backspace'. Agora, o mesmo código é usado em vários métodos da classe CTextBox, portanto, seria razoável implementar um método separado para seu uso repetido.

O código do método CTextBox::MoveLines():

class CTextBox : public CElement
  {
private:
   //--- Desloca as linhas
   void              MoveLines(const uint from_index,const uint to_index,const bool to_down=true);
  };
//+------------------------------------------------------------------+
//| Desloca as linhas                                                |
//+------------------------------------------------------------------+
void CTextBox::MoveLines(const uint from_index,const uint to_index,const bool to_down=true)
  {
//--- Desloca as linhas para baixo
   if(to_down)
     {
      for(uint i=from_index; i>to_index; i--)
        {
         //--- Índice do elemento anterior do array de linhas
         uint prev_index=i-1;
         //--- Obtém o tamanho do conjunto de caracteres
         uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol);
         //--- Redimensiona os arrays
         ArraysResize(i,symbols_total);
         //--- Faz uma cópia da linha
         LineCopy(i,prev_index);
         //--- Se esta é a última iteração
         if(prev_index==to_index)
           {
            //--- Sai, se esta for a primeira linha
            if(to_index<1)
               break;
           }
        }
     }
//--- Desloca as linhas para cima
   else
     {
      for(uint i=from_index; i<to_index; i++)
        {
         //--- Índice do próximo elemento do array de linhas
         uint next_index=i+1;
         //--- Obtém o tamanho do conjunto de caracteres
         uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
         //--- Redimensiona os arrays
         ArraysResize(i,symbols_total);
         //--- Faz uma cópia da linha
         LineCopy(i,next_index);
        }
     }
  }

O método CTextBox::MoveSymbols() foi implementado para deslocar os caracteres em uma linha. Ele é chamado não apenas nos novos métodos relacionados com o modo de quebra automática de linha, mas também ao adicionar/remover caracteres usando o teclado nos métodos CTextBox::AddSymbol() e CTextBox::DeleteSymbol() considerados anteriormente. Os parâmetros de entrada aqui definidos são: (1) índice da linha onde os caracteres devem ser movidos; (2) índices de caractere inicial e final para movimentação; (3) direção de movimento (o movimento para a esquerda é definido por padrão).

class CTextBox : public CElement
  {
private:
   //--- Movendo os caracteres na linha especificada
   void              MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true);
  };
//+------------------------------------------------------------------+
//| Deslocamento dos caracteres na linha especificada                |
//+------------------------------------------------------------------+
void CTextBox::MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true)
  {
//--- Obtém o tamanho do conjunto de caracteres
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Diferença
   uint offset=from_pos-to_pos;
//--- Se os caracteres devem ser movidos para a esquerda
   if(to_left)
     {
      for(uint s=to_pos; s<symbols_total-offset; s++)
        {
         uint i=s+offset;
         m_lines[line_index].m_symbol[s] =m_lines[line_index].m_symbol[i];
         m_lines[line_index].m_width[s]  =m_lines[line_index].m_width[i];
        }
     }
//--- Se os caracteres devem ser movidos para a direita
   else
     {
      for(uint s=symbols_total-1; s>to_pos; s--)
        {
         uint i=s-1;
         m_lines[line_index].m_symbol[s] =m_lines[line_index].m_symbol[i];
         m_lines[line_index].m_width[s]  =m_lines[line_index].m_width[i];
        }
     }
  }

O código dos métodos auxiliares para copiar e colar caracteres (os métodos CTextBox::CopyWrapSymbols() e CTextBox::PasteWrapSymbols()) também serão frequentemente usados ​​aqui. Ao copiar, o método CTextBox::CopyWrapSymbols() passa um array dinâmico vazio. Ele também indica a linha e o caractere inicial para copiar o número especificado de caracteres. Para colar os caracteres, é necessário passar para o método CTextBox::PasteWrapSymbols() um array com os caracteres anteriormente copiados, e, ao mesmo tempo, indicar o índice da linha e do caractere, onde a inserção será feita.

class CTextBox : public CElement
  {
private:
   //--- Copia os caracteres para o array passado para deslocar para a próxima linha
   void              CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[]);
   //--- Cola os caracteres do array passado para a linha especificada
   void              PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[]);
  };
//+------------------------------------------------------------------+
//| Copia os caracteres para o array passado para o deslocamento     |
//+------------------------------------------------------------------+
void CTextBox::CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[])
  {
//--- Define o tamanho do array
   ::ArrayResize(array,symbols_total);
//--- Copia os caracteres a serem movidos para o array
   for(uint i=0; i<symbols_total; i++)
      array[i]=m_lines[line_index].m_symbol[start_pos+i];
  }
//+------------------------------------------------------------------+
//| Cola os caracteres na linha especificada                         |
//+------------------------------------------------------------------+
void CTextBox::PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[])
  {
   uint array_size=::ArraySize(array);
//--- Adiciona os dados para os arrays da estrutura para a nova linha
   for(uint i=0; i<array_size; i++)
     {
      uint s=start_pos+i;
      m_lines[line_index].m_symbol[s] =array[i];
      m_lines[line_index].m_width[s]  =m_canvas.TextWidth(array[i]);
     }
  }

Agora, vamos considerar o os métodos principais do algoritmo de quebra automática de linha.


Descrição dos principais métodos

Quando o algoritmo inicia sua operação, ele verifica o transbordamento em cada linha em um ciclo. O método CTextBox::CheckForOverflow() foi implementado para tais verificações. Este método retorna três valores, dois dos quais são armazenados em variáveis ​​passadas para o método como parâmetros de referência. 

No início do método, é necessário obter a largura da linha atual, cujo índice é passado para o método como primeiro parâmetro. A largura da linha é verificada levando em consideração a largura do recuo a partir da borda esquerda da caixa de texto e a largura da barra de rolagem vertical. Se a largura da linha corresponder à caixa de texto, o método retornará false, que significa "sem transbordo". Se a linha não se encaixa, então é necessário determinar os índices do primeiro caractere visível e espaço no lado direito da caixa de texto. Para fazer isso, percorra os caracteres da linha a partir do final e verifique se a linha se encaixa na largura da caixa de texto do início até esse caractere. Se a linha se encaixa, o índice do caractere é armazenado. Além disso, cada iteração verifica se o caractere atual é um espaço. Se assim for, o seu índice é armazenado e a busca está completa.

Após todas essas verificações e pesquisa, o método retorna true se encontrar pelo menos um dos índices procurados. Isso indicaria que a linha não se encaixa. Os índices do caractere e do espaço serão usados ​​mais tarde da seguinte maneira: se um índice de caractere é encontrado enquanto o índice de espaço não foi, isso significa que a linha não contém espaços e é necessário mover uma parte dos caracteres desta linha. Se for encontrado um espaço, então é necessário mover uma parte da linha a partir do índice deste caractere de espaço.

class CTextBox : public CElement
  {
private:
   //--- Retorna os índices do primeiro caractere visível e o espaço
   bool              CheckForOverflow(const uint line_index,int &symbol_index,int &space_index);
  };
//+------------------------------------------------------------------+
//| Verifica o transbordamento da linha                              |
//+------------------------------------------------------------------+
bool CTextBox::CheckForOverflow(const uint line_index,int &symbol_index,int &space_index)
  {
//--- Obtém o tamanho do conjunto de caracteres
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Recuos
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Obtém a largura total da linha
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- Se a largura da linha se encaixa na caixa de texto
   if(full_line_width<(uint)m_area_visible_x_size)
      return(false);
//--- Determina os índices dos caracteres transbordantes
   for(uint s=symbols_total-1; s>0; s--)
     {
      //--- Obtém a (1) largura da substring desde o início até o caractere atual e (2) o caractere
      uint   line_width =LineWidth(s,line_index)+x_offset_plus;
      string symbol     =m_lines[line_index].m_symbol[s];
      //--- Se um caractere visível ainda não foi encontrado
      if(symbol_index==WRONG_VALUE)
        {
         //--- Se a largura da substring se encaixa na área da caixa de texto, armazena o índice de caracteres
         if(line_width<(uint)m_area_visible_x_size)
            symbol_index=(int)s;
         //--- Vai para o próximo caractere
         continue;
        }
      //--- Se este for um espaço, armazena seu índice e interrompe o ciclo
      if(symbol==SPACE)
        {
         space_index=(int)s;
         break;
        }
     }
//--- Se esta condição for atendida, então indica que a linha não se encaixa
   bool is_overflow=(symbol_index!=WRONG_VALUE || space_index!=WRONG_VALUE);
//--- Retorna o resultado
   return(is_overflow);
  }

Se a linha se encaixa o método CTextBox::CheckForOverflow() retorna false, então, é necessário verificar se a quebra automática de linha inversa pode ser feita. O método para determinar o número de caracteres para a quebra é o CTextBox::WrapSymbolsTotal(). 

Esse método retorna o número de caracteres a serem quebrados na variável de referência, bem como o sinal se é todo o texto restante ou apenas uma parte dele. Os valores para as variáveis ​​locais são calculados no início do método, por exemplo, os seguintes parâmetros:

  • O número de caracteres na linha atual
  • A largura total da linha
  • Com espaço livre
  • O número de palavras na próxima linha
  • O número de caracteres na próxima linha

Depois disso, um ciclo determina quantas palavras podem ser movidas da linha seguinte para a atual. Em cada iteração, depois de obter a largura de uma substring até o espaço especificado, verifique se a substring se encaixa na área livre na linha atual.

Se ela se encaixa, armazene o índice do caractere e verifique se outra palavra pode ser inserida aqui. Se a verificação mostrar que o texto terminou, então isso é marcado em uma variável local dedicada e o ciclo é interrompido. 

Se a substring não se encaixa, então também é necessário verificar se ele é o último caractere na linha, colocando uma marca que é uma sequência contínua sem espaços, e interromper o ciclo.

Em seguida, se a linha seguinte contiver espaços ou não tiver espaço livre, o método imediatamente retorna o resultado. No caso desta verificação ser satisfeita, ela ainda determina se uma parte de uma palavra da linha seguinte pode ser movida para a linha atual. A quebra de linha inversa de parte de uma palavra é executada somente se esta linha não se encaixar no espaço livre da linha atual e, ao mesmo tempo, os últimos caracteres da linha atual e das linhas seguintes não forem espaços. No caso dessas verificações serem satisfeitas, o próximo ciclo determina o número de caracteres a serem movidos.

class CTextBox : public CElement
  {
private:
   //--- Retorna o número de caracteres que houve a quebra de linha
   bool              WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total);
  };
//+------------------------------------------------------------------+
//| Retorna o número de caracteres que houve a quebra de linha com os sinais de volume|
//+------------------------------------------------------------------+
bool CTextBox::WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total)
  {
//--- Sinais do (1) número de caracteres a ser feito a quebra e (2) a linha sem espaços
   bool is_all_text=false,is_solid_row=false;
//--- Obtém o tamanho do conjunto de caracteres
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Recuos
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Obtém a largura total da linha
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- Obtém a largura do espaço livre
   uint free_space=m_area_visible_x_size-full_line_width;
//--- Obtém o número de palavras na linha seguinte
   uint next_line_index =line_index+1;
   uint words_total     =WordsTotal(next_line_index);
//--- Obtém o tamanho do conjunto de caracteres
   uint next_line_symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Determina o número de palavras a serem movidas da próxima linha (busca por espaços)
   for(uint w=0; w<words_total; w++)
     {
      //--- Obtém o (1) índice de espaço e (2) a largura da substring do início até o espaço
      uint ss_index        =SymbolIndexBySpaceNumber(next_line_index,w);
      uint substring_width =LineWidth(ss_index,next_line_index);
      //--- Se a substring se encaixa no espaço livre da linha atual
      if(substring_width<free_space)
        {
         //--- ...verifica se outra palavra pode ser inserida
         wrap_symbols_total=ss_index;
         //--- Interrompe se for toda a linha
         if(next_line_symbols_total==wrap_symbols_total)
           {
            is_all_text=true;
            break;
           }
        }
      else
        {
         //--- Se esta é uma linha contínua sem espaços
         if(ss_index==next_line_symbols_total)
            is_solid_row=true;
         //---
         break;
        }
     }
//--- Retorna o resultado imediatamente, se (1) esta é uma linha com um caractere de espaço ou (2) não há espaço livre
   if(!is_solid_row || free_space<1)
      return(is_all_text);
//--- Obtém a largura total da linha seguinte
   full_line_width=LineWidth(next_line_symbols_total,next_line_index)+x_offset_plus;
//--- Se (1) a linha não se encaixa e não há espaços no final das linhas (2) atual e (3) anteriores
   if(full_line_width>free_space && 
      m_lines[line_index].m_symbol[symbols_total-1]!=SPACE && 
      m_lines[next_line_index].m_symbol[next_line_symbols_total-1]!=SPACE)
     {
      //--- Determina o número de caracteres a serem movidos da próxima linha
      for(uint s=next_line_symbols_total-1; s>=0; s--)
        {
         //--- Obtém a largura da substring desde o início até o caractere especificado
         uint substring_width=LineWidth(s,next_line_index);
         //--- Se a substring não se encaixa no espaço livre do container especificado, vá para o caractere seguinte
         if(substring_width>=free_space)
            continue;
         //--- Se a substring se encaixa, armazena o valor e interrompe
         wrap_symbols_total=s;
         break;
        }
     }
//--- Retorna true, se for necessário mover o texto inteiro
   return(is_all_text);
  }

Se a linha não se encaixa, o texto será movido da linha atual para a linha CTextBox::WrapTextToNewLine(). Ela será usada em dois modos: (1) quebra automática de linha e (2) forçada: por exemplo, ao pressionar a tecla 'Enter'. Por padrão, o modo de quebra automática de linha é definido como um terceiro parâmetro. Os dois primeiros parâmetros do método são o índice (1) da linha para mover o texto do (2) índice do caractere, a partir do texto que deve ser movido para a próxima (nova) linha. 

O número de caracteres a serem movidos através da quebra de linha é determinada no início do método. Em seguida, (1) o número necessário de caracteres da linha atual é copiado para o array dinâmico local, (2) tamanhos do array atual e as linhas seguintes são definidas, e (3) os caracteres copiados são adicionados ao array de caracteres da próxima linha. Depois disso, é necessário determinar a localização do cursor do texto, se ele estava entre os caracteres que houve a quebra de linha ao inserir o texto a partir do teclado.

A última operação neste método é verificar e definir corretamente os sinais de quebra de linhas atual e seguintes, pois os resultados obtidos em diferentes situações devem ser únicos.

1. Se o CTextBox::WrapTextToNewLine() foi chamado após pressionar a tecla 'Enter', então, caso a linha atual tenha um sinal de quebra de linha, este sinal também é adicionado à próxima linha. Se a linha atual não tiver o sinal de quebra de linha, ele deve ser definido na linha atual e removido da linha seguinte.  

2. Quando o método é chamado no modo automático, então no caso da linha atual ter o sinal de quebra de linha, ela deve ser removida da linha atual e definida na próxima linha. Se a linha atual não tiver nenhum sinal de quebra, então a ausência do sinal deve ser definida para ambas as linhas. 

Código do método:

class CTextBox : public CElement
  {
private:
   //--- Realizando a quebra do texto para a próxima linha
   void              WrapTextToNewLine(const uint curr_line_index,const uint symbol_index,const bool by_pressed_enter=false);
  };
//+------------------------------------------------------------------+
//| Realizando a quebra do texto para a próxima linha                |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToNewLine(const uint line_index,const uint symbol_index,const bool by_pressed_enter=false)
  {
//--- Obtém o tamanho do conjunto de caracteres na linha
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- O último índice de caractere
   uint last_symbol_index=symbols_total-1;
//--- Ajuste no caso de uma linha vazia
   uint check_symbol_index=(symbol_index>last_symbol_index && symbol_index!=symbols_total)? last_symbol_index : symbol_index;
//--- Índice da próxima linha
   uint next_line_index=line_index+1;
//--- O número de caracteres a serem movidos para a nova linha
   uint new_line_size=symbols_total-check_symbol_index;
//--- Copia os caracteres a serem movidos para o array
   string array[];
   CopyWrapSymbols(line_index,check_symbol_index,new_line_size,array);
//--- Redimensiona os arrays da estrutura para a linha
   ArraysResize(line_index,symbols_total-new_line_size);
//--- Redimensiona os arrays da estrutura para a nova linha
   ArraysResize(next_line_index,new_line_size);
//--- Adiciona os dados para os arrays da estrutura para a nova linha
   PasteWrapSymbols(next_line_index,0,array);
//--- Determina a nova localização do cursor de texto
   int x_pos=int(new_line_size-(symbols_total-m_text_cursor_x_pos));
   m_text_cursor_x_pos =(x_pos<0)? (int)m_text_cursor_x_pos : x_pos;
   m_text_cursor_y_pos =(x_pos<0)? (int)line_index : (int)next_line_index;
//--- Se indicado que a chamada foi iniciada pelo pressionamento do Enter
   if(by_pressed_enter)
     {
      //--- Se a linha tiver um sinal de quebra, define o sinal para a linha atual e para a próxima
      if(m_lines[line_index].m_end_of_line)
        {
         m_lines[line_index].m_end_of_line      =true;
         m_lines[next_line_index].m_end_of_line =true;
        }
      //--- Se não, então, somente para a atual
      else
        {
         m_lines[line_index].m_end_of_line      =true;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
   else
     {
      //--- Se a linha tivesse um sinal de quebra, continue e coloque o sinal na próxima linha
      if(m_lines[line_index].m_end_of_line)
        {
         m_lines[line_index].m_end_of_line      =false;
         m_lines[next_line_index].m_end_of_line =true;
        }
      //--- Se a linha não tiver o sinal final, continue em ambas as linhas
      else
        {
         m_lines[line_index].m_end_of_line      =false;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
  }


O CTextBox::WrapTextToPrevLine() é projetado para quebra automática de linha inversa. Passa-se o índice da próxima linha e o número de caracteres a serem movidos para a linha atual. O terceiro parâmetro indica se deve ser movida todo o texto restante ou apenas uma parte. Realiza a quebra de uma parte do texto (falso) é definido por padrão. 

No início do método, o número especificado de caracteres da próxima linha é copiado para o array dinâmico local. Então, o array dos caracteres da linha atual deve ser incrementado pelo número adicionado de caracteres. Depois disso, (1) os caracteres copiados anteriormente são adicionados aos novos elementos do array de caracteres da linha atual; (2) os caracteres restantes da linha seguinte são movidos para o início do array; (3) o array dos caracteres da linha seguinte é decrementado pelo número de caracteres extraídos. 

Mais tarde, a localização do cursor de texto deve ser ajustada. Se ele foi localizado na mesma parte da palavra que foi realizado a quebra para a linha anterior, então ele também deve ser movido junto com essa parte.

No final, no caso de haver a quebra de todo o texto restante, é necessário (1) adicionar o sinal final à linha atual, (2) deslocar todas as linhas inferiores por uma posição para cima e (3) diminuir o array de linhas por um elemento.

class CTextBox : public CElement
  {
private:
   //--- Realiza a quebra de linha da linha especificada para a anterior
   void              WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false);
  };
//+------------------------------------------------------------------+
//| Realiza a quebra de linha da linha seguinte para a atual         |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false)
  {
//--- Obtém o tamanho do conjunto de caracteres na linha
   uint symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Índice da linha anterior
   uint prev_line_index=next_line_index-1;
//--- Copia os caracteres a serem movidos para o array
   string array[];
   CopyWrapSymbols(next_line_index,0,wrap_symbols_total,array);
//--- Obtém o tamanho do array de caracteres na linha anterior
   uint prev_line_symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
//--- Aumenta o tamanho do array da linha anterior pelo número de caracteres adicionados
   uint new_prev_line_size=prev_line_symbols_total+wrap_symbols_total;
   ArraysResize(prev_line_index,new_prev_line_size);
//--- Adiciona os dados para os arrays da estrutura para a nova linha
   PasteWrapSymbols(prev_line_index,new_prev_line_size-wrap_symbols_total,array);
//--- Desloca os caracteres para a área liberada na linha atual
   MoveSymbols(next_line_index,wrap_symbols_total,0);
//--- Diminui o tamanho do array da linha atual pelo número de caracteres extraídos
   ArraysResize(next_line_index,symbols_total-wrap_symbols_total);
//--- Ajusta o cursor de texto
   if((is_all_text && next_line_index==m_text_cursor_y_pos) || 
      (!is_all_text && next_line_index==m_text_cursor_y_pos && wrap_symbols_total>0))
     {
      m_text_cursor_x_pos=new_prev_line_size-(wrap_symbols_total-m_text_cursor_x_pos);
      m_text_cursor_y_pos--;
     }
//--- Sai, se este não é todo o texto restante da linha
   if(!is_all_text)
      return;
//--- Acrescente o sinal de quebra à linha anterior, se a linha atual tiver ela
   if(m_lines[next_line_index].m_end_of_line)
      m_lines[next_line_index-1].m_end_of_line=true;
//--- Obtém o tamanho do array de linhas
   uint lines_total=::ArraySize(m_lines);
//--- Desloca as linhas por uma linha
   MoveLines(next_line_index,lines_total-1,false);
//--- Redimensiona o array de linhas
   ::ArrayResize(m_lines,lines_total-1);
  }

É finalmente o momento de considerar o último método e o mais importante — CTextBox::WordWrap(). Para que a quebra automática de linha esteja operacional, deve ser colocado uma chamada para este método no CTextBox::ChangeTextBoxSize(). 

No início do método CTextBox::WordWrap(), existe uma verificação se a caixa de texto multilinha e os modos de quebra automática de linha estão ativados. Se um dos métodos estiver desativado, o programa deixa o método. Se os modos estiverem ativados, então é necessário iterar sobre todas as linhas para ativar o algoritmo da quebra automática de linha. Aqui, cada iteração usa o método CTextBox::CheckForOverflow() para verificar se uma linha transborda a largura da caixa de texto. 

  1. Se a linha não se encaixar, então, veja se um caractere de espaço mais próximo à borda direita da caixa de texto foi encontrado. Uma parte da linha atual a partir desse caractere de espaço será movida para a próxima linha. O caractere de espaço não é movido para a linha seguinte; portanto, o índice de espaço é incrementado. Em seguida, o array de linhas é incrementado por um elemento, e as linhas inferiores são deslocadas para baixo por uma posição. O índice para mover a parte da linha é verificado novamente. Depois disso, é realizado a quebra do texto. 
  2. Se a linha se encaixa, então, verifica se dever haver a quebra de linha inversa. É verificado um sinal de quebra da linha atual no início deste bloco. Se estiver presente, o programa passa para a próxima iteração. Se a verificação for satisfeita, o número de caracteres a ser movido é determinado, após o qual é realizado a quebra do texto para a linha anterior.
//+------------------------------------------------------------------+
//| Classe para a criação de uma caixa de texto multilinha           |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Quebra automática de linha
   void              WordWrap(void);
  };
//+------------------------------------------------------------------+
//| Quebra automática de linha                                       |
//+------------------------------------------------------------------+
void CTextBox::WordWrap(void)
  {
//--- Sai, se a (1) caixa de texto multilinha e (2) os modos de quebra de linha estiverem desativados
   if(!m_multi_line_mode || !m_word_wrap_mode)
      return;
//--- Obtém o tamanho do array de linhas
   uint lines_total=::ArraySize(m_lines);
//--- Verifica se é necessário ajustar o texto para a largura da caixa de texto
   for(uint i=0; i<lines_total; i++)
     {
      //--- Determina o primeiro caractere visível (1) e (2) o espaço
      int symbol_index =WRONG_VALUE;
      int space_index  =WRONG_VALUE;
      //--- Índice da próxima linha
      uint next_line_index=i+1;
      //--- Se a linha não se encaixa, realiza a quebra de parte da linha atual para a nova linha
      if(CheckForOverflow(i,symbol_index,space_index))
        {
         //--- Se um caractere de espaço for encontrado, ele não será quebrado
         if(space_index!=WRONG_VALUE)
            space_index++;
         //--- Aumenta o array de linhas por um elemento
         ::ArrayResize(m_lines,++lines_total);
         //--- Desloca as linhas para baixo a partir da posição atual por um item
         MoveLines(lines_total-1,next_line_index);
         //--- Verifica o índice do caractere, a partir do qual o texto será movido
         int check_index=(space_index==WRONG_VALUE && symbol_index!=WRONG_VALUE)? symbol_index : space_index;
         //--- Realiza a quebra do texto para a nova linha
         WrapTextToNewLine(i,check_index);
        }
      //--- Se a linha se encaixa, então, verifique se quebra de linha inversa deve ser feita
      else
        {
         //--- Pula, se (1) esta linha tem o sinal de quebra de linha ou (2) esta é a última linha
         if(m_lines[i].m_end_of_line || next_line_index>=lines_total)
            continue;
         //--- Determina o número de caracteres a ser realizado a quebra de linha
         uint wrap_symbols_total=0;
         //--- Se for necessário haver a quebra de linha do texto restante da próxima linha para a linha atual
         if(WrapSymbolsTotal(i,wrap_symbols_total))
           {
            WrapTextToPrevLine(next_line_index,wrap_symbols_total,true);
            //--- Atualiza o tamanho do array para uso posterior no ciclo
            lines_total=::ArraySize(m_lines);
            //--- Volta um passo para evitar saltar uma linha para a próxima verificação
            i--;
           }
         //--- Realiza a quebra apenas no que se encaixa
         else
            WrapTextToPrevLine(next_line_index,wrap_symbols_total);
        }
     }
  }


Todos os métodos para a quebra automática de linha foram considerados. Agora, vamos ver como tudo isso funciona.


Aplicação para testar os controles

Vamos criar um aplicativo MQL para testes. Tomaremos a versão existente do artigo anterior da caixa de texto multilinha, com a caixa de texto de linha única removida da interface gráfica do aplicativo. Todo o resto permanece o mesmo. Isto é como tudo deve funciona no gráfico do terminal MetaTrader 5:

Fig. 10. Demonstração da quebra automática de linha no controle da caixa de texto multilinha 

Fig. 10. Demonstração da quebra automática de linha no controle da caixa de texto multilinha


O aplicativo de teste apresentado no artigo pode ser baixado usando o link abaixo para estudá-lo ainda mais.


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. 11. Estrutura da biblioteca no atual estágio de desenvolvimento.


Fig. 11. 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.