Descargar MetaTrader 5

Interfaces gráficas X: Algoritmo del traslado de palabras en el campo de edición multilínea (build 12)

11 abril 2017, 10:10
Anatoli Kazharski
0
628

Índice

Introducción

El primer artículo de la serie nos cuenta con más detalles para qué sirve esta librería: Interfaces gráficas I: Preparación de la estructura de la librería (Capítulo 1). Al final de cada artículo de la serie se muestra la lista completa de los capítulos con los enlaces. Además, se puede descargar la versión completa de la librería en la fase actual del desarrollo del proyecto. Es necesario colocar los ficheros en los mismos directorios, tal como están ubicados en el archivo.

En este artículo sigamos desarrollando el control «Campo de edición multilínea». Para conocer lo que ha sido hecho antes, consulte el artículo Interfaces gráficas X: Control «Campo de edición del texto multilínea» (build 8). Esta vez, nuestra tarea consiste en configurar el traslado automático de palabras a la siguiente línea si no encajan en el campo de edición, o el traslado inverso a la línea anterior si aparece esta posibilidad.


Modo «Ajuste de línea» en el campo de edición multilínea

En todos los editores de texto o aplicaciones con la información textual funciona el traslado de palabras si el ancho del área que contiene el texto se sobrellena. Eso libra de una fatigosa necesidad de usar constantemente la barra de desplazamiento horizontal. 

Por defecto, el modo «Ajuste de línea» será deshabilitado. Para activar este modo, es necesario utilizar el método CTextBox::WordWrapMode(). Es el único método público en la implementación del ajuste de línea con traslado por palabras. Todos los demás métodos serán privados, a continuación consideraremos más detalladamente su organización.

//+------------------------------------------------------------------+
//| Clase para crear el campo de edición multilínea                  |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Modo «Ajuste de línea»
   bool m_word_wrap_mode;
   //---
public:
   //--- Modo «Ajuste de línea»
   void WordWrapMode(const bool mode) { m_word_wrap_mode=mode; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_word_wrap_mode(false)

Para una configuración correcta del traslado de palabras e inserción del texto en una u otra línea, es necesario que cada línea tenga el indicio del salto de línea.

Vamos a ver un simple ejemplo con una sola línea. Abrimos un editor de texto cualquiera, en el que se puede habilitar/deshabilitar el modo del traslado de palabras, por ejemplo, Bloc de notas. Insertamos una línea en el documento:

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

Si el modo del traslado está deshabilitado, dependiendo del ancho del campo de edición, la línea puede no encajar. Entonces, para leerla, tendremos que usar la barra de desplazamiento horizontal:

 Fig. 1. Modo «Ajuste de línea» está deshabilitado.

Fig. 1. Modo «Ajuste de línea» está deshabilitado.


Ahora activaremos el modo del traslado de palabras. La línea deberá encajarse en el campo de edición del editor del texto:

 Fig. 2. Modo «Ajuste de línea» está habilitado.


Fig. 2. Modo «Ajuste de línea» está habilitado.


Observamos que la línea original se ha dividido en tres sublíneas que se han colocado consecutivamente una tras otra. Aquí solamente la tercera sublínea tiene el indicio del salto de línea, y si leemos la primera línea en este archivo a través del programa, obtendremos el texto entero hasta el salto de línea. 

Podemos comprobar eso a través de un simple script:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- Obtenemos el handle del archivo
   int file=::FileOpen("Тема 'Перенос по словам'.txt",FILE_READ|FILE_TXT|FILE_ANSI);
//--- Leeremos el archivo si el handle ha sido obtenido
   if(file!=INVALID_HANDLE)
      ::Print(__FUNCTION__," > ",::FileReadString(file));
   else
      ::Print(__FUNCTION__," > error: ",::GetLastError());
  }
//+------------------------------------------------------------------+

Aquí tenemos el resultado de la lectura de la primera (y la única en esta caso) línea y su visualización en el registro:

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

Con el fin de organizar la semejante lectura de la información desde el campo de edición multilínea, en la clase CTextBox en la estructura StringOptions (nombre viejo KeySymbolOptions) se añade otra propiedad bool que va a guardar el indicio del salto de línea.

   //--- Caracteres y sus propiedades
   struct StringOptions
     {
      string            m_symbol[];    // Caracteres
      int               m_width[];     // Ancho de caracteres
      bool              m_end_of_line; // Indicio del salto de línea
     };
   StringOptions  m_lines[];

Para implementar el traspaso de palabras, necesitaremos unos métodos principales y auxiliares. Sus tareas son las siguientes.

Métodos principales:

  • Ajuste de línea
  • Devolución de los índices del primer carácter y espacio visibles a la derecha
  • Devolución del número de caracteres traspasados
  • Traspaso del texto a la siguiente línea
  • Traspaso del texto desde la siguiente línea a la actual

Métodos auxiliares:

  • Devolución del número de palabras en la línea especificada
  • Devolución del índice del carácter «espacio» por su número
  • Desplazamiento de líneas
  • Desplazamiento de caracteres en la línea especificada
  • Copiado de caracteres al array pasado para el traslado a otra línea
  • Inserción de caracteres desde el array pasado en la línea especificada

Vamos a ver con detalles cómo están organizados los métodos auxiliares.


Descripción del algoritmo y métodos adicionales

En el algoritmo del traspaso de palabras hay un momento cuando es necesario iniciar el ciclo donde se va a buscar el índice del espacio por su número. Para organizar este ciclo, necesitamos un método para determinar el número de palabras en la línea. Abajo se muestra el código del método CTextBox::WordsTotal() que ejecuta esta tarea.

Es bastante fácil calcular las palabras. Hay que recorrer en el ciclo el array de caracteres de la línea especificada, siguiendo la aparición del patrón, cuando el carácter actual no es un espacio (' '), y el anterior lo es. Eso va a significar el comienzo de una palabra nueva. El contador también se aumenta si hemos llegado al final de la línea para no perder la última palabra.

class CTextBox : public CElement
  {
private:
   //--- Devuelve el número de palabras en la línea especificada
   uint              WordsTotal(const uint line_index);
  };
//+------------------------------------------------------------------+
//| Devuelve el número de palabras en la línea especificada          |
//+------------------------------------------------------------------+
uint CTextBox::WordsTotal(const uint line_index)
  {
//--- Obtenemos el tamaño del array de líneas
   uint lines_total=::ArraySize(m_lines);
//--- Prevención de superar el rango
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Obtenemos el tamaño del array de caracteres de la línea especificada
   uint symbols_total=::ArraySize(m_lines[l].m_symbol);
//--- Contador de palabras
   uint words_counter=0;
//--- Buscamos el espacio con el índice especificado
   for(uint s=1; s<symbols_total; s++)
     {
      //--- Contamos si (1) hemos llegado al final de la línea o (2) hemos encontrado el espacio (fin de la palabra)
      if(s+1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE))
         words_counter++;
     }
//--- Devolver el número de palabras
   return(words_counter);
  }

Para determinar el índice del carácter «espacio» por su número, va a usarse el método CTextBox::SymbolIndexBySpaceNumber(). Después de obtener este valor, se puede calcular el ancho de una o varias palabras desde el comienzo de la sublínea, usando el método CTextBox::LineWidth(). 

Para demostrarlo, veamos el ejemplo con el texto en una línea. Los caracteres (azul), sublíneas (verde) y espacios (rojo) están indexados. Se puede observar que, por ejemplo, el primer espacio (0) en la primera línea (0) tiene el índice 6.

 Fig. 3. Indices de los caracteres (azul), sublíneas (verde) y espacios (rojo).

Fig. 3. Indices de los caracteres (azul), sublíneas (verde) y espacios (rojo).


Abajo se muestra el código del método CTextBox::SymbolIndexBySpaceNumber(). Aquí, todo es muy fácil: hay que recorrer en el ciclo todos los caracteres de la línea especificada, aumentando el contador cada vez que se encuentre el carácter del espacio. Si durante alguna iteración, resulta que el contador tiene el valor igual al índice del espacio especificado en el valor pasado del segundo argumento, el valor del índice del carácter se guarda y el ciclo se detiene. Precisamente, este valor es devuelto por el métoodo.

class CTextBox : public CElement
  {
private:
   //--- Devuelve el índice del carácter «espacio» por su número 
   uint              SymbolIndexBySpaceNumber(const uint line_index,const uint space_index);
  };
//+------------------------------------------------------------------+
//| Devuelve el índice del carácter «espacio» por su número          |
//+------------------------------------------------------------------+
uint CTextBox::SymbolIndexBySpaceNumber(const uint line_index,const uint space_index)
  {
//--- Obtenemos el tamaño del array de líneas
   uint lines_total=::ArraySize(m_lines);
//--- Prevención de superación del rango
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Obtenemos el tamaño del array de caracteres de la línea especificada
   uint symbols_total=::ArraySize(m_lines[l].m_symbol);
//--- (1) Para determinar el índice del carácter del espacio y (2) el contador de espacios
   uint symbol_index  =0;
   uint space_counter =0;
//--- Buscamos el espacio con el índice especificado
   for(uint s=1; s<symbols_total; s++)
     {
      //--- Si hemos encontrado el espacio
      if(m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE)
        {
         //--- Si el contador es igual al índice especificado del espacio, lo guardamos y detenemos el ciclo
         if(space_counter==space_index)
           {
            symbol_index=s;
            break;
           }
         //--- Aumentamos el contador de espacios
         space_counter++;
        }
     }
//--- Devolver el tamaño de la línea si el índice del espacio no ha sido encontrado
   return((symbol_index<1)? symbols_total : symbol_index);
  }

Veamos los detalles del algoritmo del traspaso de palabras en la parte que se refiere al desplazamiento de elementos de los arrays de las líneas y caracteres. Ilustraremos paso a paso cómo se ve eso en diferentes situeciones. Supongamos que tenemos la línea siguiente:

The quick brown fox jumped over the lazy dog.

Esta línea no cabe por su ancho en el campo de texto cuya área está marcada con el rectángulo rojo en la imagen 4. Vemos que es necesario traspasar la parte «sobrante» — 'over the lazy dog.'— a la siguiente línea.

 Fig. 4. Situación con el sobrellenado de la línea del campo de texto.

Fig. 4. Situación con el sobrellenado de la línea del campo de texto.

Puesto que el array dinámico de las líneas en este momento se compone de un solo elemento, primero hay que aumentarlo a un elemento más. En la línea nueva, es necesario establecer el array de caracteres según el número de caracteres del texto a traspasar, y después de eso traspasar la parte de la línea que no cabe. Resultado final:

 Fig. 5. Una parte de la línea ha sido traspasada a la siguiente línea nueva.

Fig. 5. Una parte de la línea ha sido traspasada a la siguiente línea nueva.

Ahora veremos cómo va a trabajar el algoritmo si el ancho del campo de edición se reduce aproximadamente a un 30%. Aquí primero también se determina qué parte de la primera línea (índice 0) excede los límites del campo de texto. En este caso, no se ha encajado la sublínea 'fox jumped'. Luego, el array dinámico de las líneas se aumenta a un elemento. Luego, todas las sublíneas que se encuentran por debajo se desplazan hacia abajo a una línea, dejando así el espacio libre para el texto a traspasar. Después de eso, la sublínea 'fox jumped' se desplaza al sitio libre, tal como ha sido descrito en el párrafo anterior. Eeste paso se muestra en la imagen de abajo.

 Fig. 6. Traspaso del texto a la segunda línea (índice 1).

Fig. 6. Traspaso del texto a la segunda línea (índice 1).


En la siguiente iteración del ciclo, el algoritmo pasa a la siguiente línea (índice 1). Aquí, hay que volver a comprobar si una parte de esta línea excede los límites del campo de texto. Si la comprobación mostrará que no excede, es necesario comprobar si hay espacio suficiente en esta línea a la derecha para colocar ahí una parte de la siguiente línea con el índice 2. Así comprobamos el cumplimiento de las condiciones del traspaso inverso del texto desde el comienzo de la siguiente línea (índice 2) al final de la actual (índice 1).

Aparte de esta condición, es necesario comprobar si dispone la línea actual del indicio del salto de línea. Si lo tiene, entonces el traspaso inverso no se realiza. En nuestro caso, no hay indicio del salto de línea, y hay sitio suficiente para traspasar una palabra 'over'. En caso del traspaso inverso, el tamaño de los arrays de caracteres se modifica según el número de los caracteres añadidos y extraídos en la línea actual y la siguiente, respectivamente. Durante el traspaso inverso, antes del cambio del tamaño del array de caracteres, los caracteres restantes se desplazan al comienzo de la línea. Este paso se muestra en la imagen de abajo. 

 Fig. 7. Traspaso inverso del texto a la segunda línea (índice 1) desde la tercera línea (índice 2).

Fig. 7. Traspaso inverso del texto a la segunda línea (índice 1) desde la tercera línea (índice 2).


Como podemos ver, durante la reducción del campo de edición, va a realizarse el traspaso directo e inverso del texto. Si el campo va a extenderse, el traspaso inverso a los sitios liberados será suficiente. En caso del traspaso del texto a la siguiente línea, el array dinámico de líneas va a aumentarse cada vez a un elemento, y en caso del traspaso inverso del texto restante entero de la siguiente línea, el array de líneas va a reducirse cada vez a un elemento. Pero antes de eso, si hay mas líneas, ellas debes ser desplazadas a una línea hacia arriba, para excluir la formación de una línea vacía al traspasar inversamente el resto del texto. 

En el proceso del recorrido del ciclo, nosotros no veremos todos estos pasos relacionados con el traslado de las líneas, traspaso directo e inverso de las palabras: el lienzo para dibujar el campo de edición se actualizará sólo una vez, después de que todas las operaciones con los arrays se hayan ejecutado. Abajo se muestra una imagen aproximada que verá el usuario durante el trabajo con la interfaz gráfica:

 Fig. 8. Demostración de trabajo del algoritmo de traspaso de palabras en el ejemplo del editor de texto.

Fig. 8. Demostración de trabajo del algoritmo de traspaso de palabras en el ejemplo del editor de texto.


Y eso no es todo. Si en una línea ha quedado sólo una palabra (secuencia de caracteres continua), en este caso el traspaso se realiza carácter por carácter. Esta situación se muestra a continuación:

 Fig. 9. Demostración del traspaso carácter por carácter en caso cuando la palabra no cabe.


Fig. 9. Demostración del traspaso carácter por carácter en caso cuando la palabra no cabe.

Ahora hablaremos de los métodos para trasladar las líneas y los caracteres. Para el desplazamiento de las líneas se va a usar el método CTextBox::MoveLines(). Este método recibe el índice de las líneas, desde el que y hasta el que es necesario desplazar las líneas a una posición. El tercer parámetro es la dirección del desplazamiento. Por defecto, está establecido en el desplazamiento de las líneas hacia abajo. 

Hasta ahora, estábamos utilizando el algoritmo del desplazamiento de las líneas sólo una vez, gestionando el campo de edición a través de las teclas 'Enter' y 'Backspace'. Ahora, el mismo código se utiliza en varios métodos de la clase CTextBox, por eso sería conveniente implementar un método separado para su uso repetido.

Código del método CTextBox::MoveLines():

class CTextBox : public CElement
  {
private:
   //--- Desplaza las líneas
   void              MoveLines(const uint from_index,const uint to_index,const bool to_down=true);
  };
//+------------------------------------------------------------------+
//| Desplazamiento de líneas                                         |
//+------------------------------------------------------------------+
void CTextBox::MoveLines(const uint from_index,const uint to_index,const bool to_down=true)
  {
//--- Desplazamiento de líneas en dirección de abajo
   if(to_down)
     {
      for(uint i=from_index; i>to_index; i--)
        {
         //--- Indice del elemento anterior del array de las líneas
         uint prev_index=i-1;
         //--- Obtenemos el tamaño del array de caracteres
         uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol);
         //--- Establecemos nuevo tamaño para los arrays
         ArraysResize(i,symbols_total);
         //--- Hacer una copia de la línea
         LineCopy(i,prev_index);
         //--- Si es la última iteración
         if(prev_index==to_index)
           {
            //--- Salir si es la primera línea
            if(to_index<1)
               break;
           }
        }
     }
//--- Desplazamiento de líneas en dirección de arriba
   else
     {
      for(uint i=from_index; i<to_index; i++)
        {
         //--- Indice del siguiente elemento del array de las líneas
         uint next_index=i+1;
         //--- Obtenemos el tamaño del array de caracteres
         uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
         //--- Establecemos nuevo tamaño para los arrays
         ArraysResize(i,symbols_total);
         //--- Hacer una copia de la línea
         LineCopy(i,next_index);
        }
     }
  }

Para desplazar los caracteres, en la línea ha sido implementado el método CTextBox::MoveSymbols(). Se invoca no sólo en los métodos nuevos referentes al modo del traspaso de palabras, sino también durante la inserción/eliminación de caracteres a través del teclado en los métodos CTextBox::AddSymbol() y CTextBox::DeleteSymbol(), considerados anteriormente. Aquí se especifican los siguientes parámetros de entrada: (1) índice de la línea donde van a desplazarse los caracteres; (2) índices del carácter desde el que y hasta el que va a realizarse el desplazamiento; (3) dirección del desplazamiento (por defecto, se establece el desplazamiento a la izquierda).

class CTextBox : public CElement
  {
private:
   //--- Desplazamiento de caracteres en la línea especificada
   void              MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true);
  };
//+------------------------------------------------------------------+
//| Desplazamiento de caracteres en la línea especificada            |
//+------------------------------------------------------------------+
void CTextBox::MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true)
  {
//--- Obtenemos el tamaño del array de caracteres
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Diferencia
   uint offset=from_pos-to_pos;
//--- Si es necesario desplazar los caracteres a la izquierda
   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];
        }
     }
//--- Si es necesario desplazar los caracteres a la derecha
   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];
        }
     }
  }

Aquí también va a utilizarse más de una vez el código de los métodos auxiliares para copiar e insertar los caracteres: CTextBox::CopyWrapSymbols() y CTextBox::PasteWrapSymbols(). Durante el copiado, al método CTextBox::CopyWrapSymbols() se le pasa un array dinámico vacío y se indica en qué línea y a partir de qué carácter es necesario copiar el número especificado de caracteres. Para insertar los caracteres, al método CTextBox::PasteWrapSymbols() es necesario pasarle el array con los caracteres copiados antes, indicando el índice de la línea y del carácter a donde va a realizarse la inserción.

class CTextBox : public CElement
  {
private:
   //--- Copia los caracteres al array pasado para el traslado a otra línea
   void              CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[]);
   //--- Inserta los caracteres desde el array pasado en la línea especificada
   void              PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[]);
  };
//+------------------------------------------------------------------+
//| Copia los caracteres a trasladar al array pasado                 |
//+------------------------------------------------------------------+
void CTextBox::CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[])
  {
//--- Establecemos el tamaño para el array
   ::ArrayResize(array,symbols_total);
//--- Copiamos al array los caracteres que es necesario pasar
   for(uint i=0; i<symbols_total; i++)
      array[i]=m_lines[line_index].m_symbol[start_pos+i];
  }
//+------------------------------------------------------------------+
//| Inserta los caracteres en la línea especificada                  |
//+------------------------------------------------------------------+
void CTextBox::PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[])
  {
   uint array_size=::ArraySize(array);
//--- Añadir los datos a los arrays de la estructura de la línea nueva
   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]);
     }
  }

A continuación, analizaremos los métodos principales del algoritmo del traslado de palabras.


Descripción de métodos principales

Cuando el algoritmo empieza su trabajo, en el ciclo se comprueba si hay sobrellenado de cada línea. Para esta comprobación ha sido implementado el método CTextBox::CheckForOverflow(). Este método devuelve tres valores, dos de los cuales se guardan en las variables pasadas a este método como parámetros por referencias. 

Al principio del método, hay que obtener el ancho de la línea actual, el índice de la cual se pasa al método como el primer parámetro. El ancho de la línea se comprueba tomando en cuenta el margen desde el lado izquierdo del campo de edición y el ancho de la barra de desplazamiento vertical. Si el ancho de la línea cabe en el campo de edición, el método devuelve false, lo que significa «no hay sobrellenados». Si la línea no se encaja, luego es necesario determinar los índices del primer carácter y espacio visibles en la parte derecha del campo de edición. Para eso nos movemos en el ciclo desde el final de la línea por todos los caracteres y comprobamos si cabe la linea desde el principio hasta este carácter según el ancho del campo de texto. Si la línea se encaja, el índice de este carácter se guarda. Aparte de eso, en cada iteración se verifica si el carácter actual es un espacio, y si es el caso, se guarda su índice y la búsqueda se termina.

Después de todas esas comprobaciones y la búsqueda, el método devuelve la verdad si por lo menos uno de los índices buscados ha sido encontrado. Eso quiere decir que la línea no cabe. Luego, los índices del carácter y del espacio van a usarse así: si el índice del carácter ha sido detectado y el índice del espacio no, entonces en la línea no hay espacios y es necesario traspasar una parte de caracteres de esta línea. Si el espacio ha sido detectado, hay que traspasar la parte de la línea a partir del índice de este espacio.

class CTextBox : public CElement
  {
private:
   //--- Devuelve los índices del primer carácter y espacio visibles
   bool              CheckForOverflow(const uint line_index,int &symbol_index,int &space_index);
  };
//+------------------------------------------------------------------+
//| Comprobación del sobrellenado de la línea                        |
//+------------------------------------------------------------------+
bool CTextBox::CheckForOverflow(const uint line_index,int &symbol_index,int &space_index)
  {
//--- Obtenemos el tamaño del array de caracteres
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Márgenes
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Obtenemos el ancho completo de la línea
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- Si el ancho de esta línea cabe en el campo
   if(full_line_width<(uint)m_area_visible_x_size)
      return(false);
//--- Determinamos los índices de los caracteres del sobrellenado
   for(uint s=symbols_total-1; s>0; s--)
     {
      //--- Obtenemos (1) el ancho de la sublínea desde el principio hasta el carácter actual y (2) el carácter
      uint   line_width =LineWidth(s,line_index)+x_offset_plus;
      string symbol     =m_lines[line_index].m_symbol[s];
      //--- Si no hemos encontrado el carácter visible
      if(symbol_index==WRONG_VALUE)
        {
         //--- Si el ancho de la sublínea cabe en el área del campo de edición, recordamos el índice del carácter
         if(line_width<(uint)m_area_visible_x_size)
            symbol_index=(int)s;
         //--- Ir al carácter siguiente
         continue;
        }
       //--- Si es un espacio, recordamos su índice y detenemos el ciclo
      if(symbol==SPACE)
        {
         space_index=(int)s;
         break;
        }
     }
//--- El cumplimiento de la condición significa que la línea no cabe
   bool is_overflow=(symbol_index!=WRONG_VALUE || space_index!=WRONG_VALUE);
//--- Devolver el resultado
   return(is_overflow);
  }

Si la línea se encaja y el método CTextBox::CheckForOverflow() devuelve false, hay que comprobar si es posible realizar el traspaso inverso. El método CTextBox::WrapSymbolsTotal() se utiliza para determinar el número de caracteres para el traspaso. 

Este método devuelve en la variable por referencia el numero de caracteres a traspasar, así como el indicio de que si se trata del texto completo que falta o sólo una parte de él. Al principio del método se calculan los valores para las variables locales, por ejemplo los parámetros siguientes:

  • Número de caracteres de la línea actual
  • Ancho completo de la línea
  • Ancho del espacio libre
  • Número de palabras en la línea siguiente
  • Número de caracteres en la línea siguiente

Después de eso, en el ciclo se determina cuántas palabras se puede traspasar desde la siguiente línea a la línea actual. En cada iteración, obtenemos el ancho de la sublínea hasta el espacio especificado, comprobamos si ella cabe en el espacio libre en la línea actual.

Si se encaja, recordamos el índice del carácter y comprobamos si se puede meter aquí una palabra más. Si la comprobación ha demostrado que el texto se ha terminado, lo anotamos en una variable local destinada especialmente para eso y detenemos el ciclo

Si la sublínea no cabe, aquí también hay que comprobar si es el último carácter en la línea, colocar la marca de que es una línea continua sin espacios y detener el ciclo.

Luego, si en la siguiente línea hay espacios o no hay sitio libre, el método devuelve el resultado enseguida. Si la comprobación ha sido superada, determinamos si se puede traspasar una parte de la palabra desde la siguiente línea a la línea actual. El traspaso inverso de una parte de la palabra se realiza sólo si esta línea no cabe en el espacio libre de la línea actual, y además los últimos caracteres de la línea actual y la siguiente no son espacios. Si estas comprobaciones han sido superadas, en el siguiente ciclo se determinará cuántos caracteres hay que traspasar.

class CTextBox : public CElement
  {
private:
   //--- Devuelve el número de caracteres a traspasar
   bool              WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total);
  };
//+----------------------------------------------------------------------------+
//| Devuelve el número de caracteres a traspasar con el indicio del volumen    |
//+----------------------------------------------------------------------------+
bool CTextBox::WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total)
  {
//--- Indicio del (1) número de caracteres a traspasar y de la (2) línea sin espacios
   bool is_all_text=false,is_solid_row=false;
//--- Obtenemos el tamaño del array de caracteres
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Márgenes
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Obtenemos el ancho completo de la línea
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- Obtenemos el ancho del espacio libre
   uint free_space=m_area_visible_x_size-full_line_width;
//--- Obtenemos el número de palabras en la siguiente línea
   uint next_line_index =line_index+1;
   uint words_total     =WordsTotal(next_line_index);
//--- Obtenemos el tamaño del array de caracteres
   uint next_line_symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Obtener el número de palabras que pueden ser traspasadas desde la línea siguiente (búsqueda por el espacio)
   for(uint w=0; w<words_total; w++)
     {
      //--- Obtenemos el (1) índice del espacio y el (2) ancho de la sublínea desde el principio hasta el espacio
      uint ss_index        =SymbolIndexBySpaceNumber(next_line_index,w);
      uint substring_width =LineWidth(ss_index,next_line_index);
      //--- Si la sublínea cabe en el espacio libre de la línea actual
      if(substring_width<free_space)
        {
         //--- ...comprobaremos si se puede añadir una palabra más
         wrap_symbols_total=ss_index;
         //--- Detenerse si es la línea entera
         if(next_line_symbols_total==wrap_symbols_total)
           {
            is_all_text=true;
            break;
           }
        }
      else
        {
         //--- Si es la línea continua si espacio
         if(ss_index==next_line_symbols_total)
            is_solid_row=true;
         //---
         break;
        }
     }
//--- Devolver el resultado inmediatamente si (1) es la línea con un espacio o (2) no hay sitio libre
   if(!is_solid_row || free_space<1)
      return(is_all_text);
//--- Obtenemos el ancho completo de la siguiente línea
   full_line_width=LineWidth(next_line_symbols_total,next_line_index)+x_offset_plus;
//--- Si (1) la línea no cabe y no hay espacios al final de (2) la línea actual y (3) la siguiente
   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)
     {
      //--- Obtener el número de caracteres que pueden ser traspasados desde la siguiente línea
      for(uint s=next_line_symbols_total-1; s>=0; s--)
        {
        //| Obtenemos el ancho de la sublínea desde el principio hasta el carácter especificado
         uint substring_width=LineWidth(s,next_line_index);
         //--- Si la sublínea no cabe en el espacio libre del contenedor indicado, pasar al siguiente carácter
         if(substring_width>=free_space)
            continue;
         //--- Si la sublínea cabe, recordamos el valor y nos detenemos
         wrap_symbols_total=s;
         break;
        }
     }
//--- Devolver la verdad si hace falta traspasar el texto entero
   return(is_all_text);
  }

Si la línea no encaja, el texto desde la línea actual a la nueva va a traspasarse a través del método CTextBox::WrapTextToNewLine(). Este método va a utilizarse en dos modos: (1) traspaso automático y (2) traspaso forzoso, cuando por ejemplo se pulsa la tecla 'Enter'. Por defecto, como el tercer parámetro se establece el modo del traspaso automático. Los dos primeros parámetros del método son el (1) índice de la línea desde la que va a realizarse el traspaso del texto y el (2) índice del carácter a partir del que es necesario realizar el traspaso del texto a la siguiente (nueva) línea. 

Al principio del método, se determina el número de caracteres a traspasar. Luego, (1) al array local dinámico se copia la cantidad necesaria de caracteres de la línea actual, (2) se establecen los tamaños para los arrays de la línea actual y la siguiente, y (3) los caracteres copiados se añaden al array de caracteres de la siguiente línea. Después de eso hay que determinar la nueva posición del cursor del texto si durante la entrada del texto a través del teclado él se encontraba entre los caracteres traspasados.

La última operación en este método es la comprobación y determinación correcta de los indicios del salto de línea actual y la siguiente, puesto que en diferentes situaciones debe obtenerse el resultado único.

1. Si el método CTextBox::WrapTextToNewLine() ha sido invocado tras la pulsación de la tecla 'Enter', entonces si la línea actual tiene el indicio del salto de línea, en la siguiente línea también se hace el indicio del salto de línea. Si la línea actual no tiene el indicio del salto de línea, entonces hay que colocarlo, y de la línea siguiente quitarlo. 

2. Cuando el método se invoca en modo automático, entonces si la línea contiene el indicio del salto de línea, es necesario quitarlo, y colocarlo en la siguiente línea. Si la línea actual no contiene el indicio del salto de línea, entonces la ausencia del indicio debe establecerse para ambas líneas. 

Código del método:

class CTextBox : public CElement
  {
private:
   //--- Traspaso del texto a la siguiente línea
   void              WrapTextToNewLine(const uint curr_line_index,const uint symbol_index,const bool by_pressed_enter=false);
  };
//+------------------------------------------------------------------+
//| Traspaso del texto a la siguiente línea                          |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToNewLine(const uint line_index,const uint symbol_index,const bool by_pressed_enter=false)
  {
//--- Obtenemos el tamaño del array de caracteres de la línea
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Ultimo índice del carácter
   uint last_symbol_index=symbols_total-1;
//--- Corrección en caso de la línea vacía
   uint check_symbol_index=(symbol_index>last_symbol_index && symbol_index!=symbols_total)? last_symbol_index : symbol_index;
//--- Indice de la siguiente línea
   uint next_line_index=line_index+1;
//--- Número de caracteres a pasar a la línea nueva
   uint new_line_size=symbols_total-check_symbol_index;
//--- Copiamos al array los caracteres que es necesario pasar
   string array[];
   CopyWrapSymbols(line_index,check_symbol_index,new_line_size,array);
//--- Establecemos nuevo tamaño para los arrays de la estructura en la línea
   ArraysResize(line_index,symbols_total-new_line_size);
//--- Establecemos nuevo tamaño para los arrays de la estructura en la línea nueva
   ArraysResize(next_line_index,new_line_size);
//--- Añadir los datos a los arrays de la estructura de la línea nueva
   PasteWrapSymbols(next_line_index,0,array);
//--- Determinamos la nueva posición del cursor del 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;
//--- Si se indica que la llamada se debe a la pulsación en la tecla Enter
   if(by_pressed_enter)
     {
      //--- Si la línea tenía el indicio del salto de línea, colocamos el indicio del salto para la línea actual y la siguiente
      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;
        }
      //--- Si no, entonces sólo para la actual
      else
        {
         m_lines[line_index].m_end_of_line      =true;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
   else
     {
      //--- Si la línea tenía el indicio del salto de línea, seguimos y colocamos el indicio del salto en la siguiente línea
      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;
        }
      //--- Si la línea no tenía el indicio del salto de línea, seguimos en ambas líneas
      else
        {
         m_lines[line_index].m_end_of_line      =false;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
  }

El método CTextBox::WrapTextToPrevLine() está destinado para el traspaso inverso del texto. Se le pasa el índice de la siguiente línea y el número de caracteres que serán traspasados a la siguiente línea. Como el tercer parámetro se indica si se traspasa el texto restante por completo o sólo una parte. Por defecto, se establece que se traspasa una parte del texto (false). 

Al principio del método, al array dinámico local se copia la cantidad especificada de caracteres desde la siguiente línea. Luego, hay que aumentar el array de caracteres de la línea actual al número de caracteres que se añaden. Después de eso, (1) los caracteres copiados antes se añaden a los nuevos elementos del array de caracteres de la línea actual; (2) los caracteres restantes de la siguiente línea se traspasan al principio del array; (3) el array de caracteres de la siguiente línea se reduce por el número de caracteres extraídos. 

Luego, hay que corregir la posición del cursor del texto. Si se encontraba en la misma parte de la palabra que ha sido traspasada a la siguiente línea, entonces hay que traspasarlo junto con ella.

Al final del método, en el caso cuando se traspasa el texto restante por completo, hay que (1) añadir el indicio del salto de línea a la línea actual, (2) desplazar todas las líneas inferiores a una posición hacia arriba y (3) reducir el array de líneas a un elemento.

class CTextBox : public CElement
  {
private:
   //--- Traspaso del texto desde la línea especificada a la anterior
   void              WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false);
  };
//+------------------------------------------------------------------+
//| Traspaso del texto desde la siguiente línea a la actual          |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false)
  {
//--- Obtenemos el tamaño del array de caracteres de la línea
   uint symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Indice de la línea anterior
   uint prev_line_index=next_line_index-1;
//--- Copiamos al array los caracteres que es necesario pasar
   string array[];
   CopyWrapSymbols(next_line_index,0,wrap_symbols_total,array);
//--- Obtenemos el tamaño del array de caracteres de la línea anterior
   uint prev_line_symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
//--- Aumentamos el tamaño del array de la línea anterior al número de caracteres a añadir
   uint new_prev_line_size=prev_line_symbols_total+wrap_symbols_total;
   ArraysResize(prev_line_index,new_prev_line_size);
//--- Añadir los datos a los arrays de la estructura de la línea nueva
   PasteWrapSymbols(prev_line_index,new_prev_line_size-wrap_symbols_total,array);
//--- Desplazamos los caracteres al sitio liberado en la línea actual
   MoveSymbols(next_line_index,wrap_symbols_total,0);
//--- Reducimos el tamaño del array de la línea actual al número de caracteres extraídos de ella
   ArraysResize(next_line_index,symbols_total-wrap_symbols_total);
//--- Corregir el cursor del 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--;
     }
//--- Salir si no es el texto restante completo de la línea
   if(!is_all_text)
      return;
//--- Añadir el indicio del salto de línea para la línea anterior si la línea actual también lo tiene
   if(m_lines[next_line_index].m_end_of_line)
      m_lines[next_line_index-1].m_end_of_line=true;
//--- Obtenemos el tamaño del array de líneas
   uint lines_total=::ArraySize(m_lines);
//--- Desplazamos las líneas a una posición hacia arriba
   MoveLines(next_line_index,lines_total-1,false);
//--- Establecemos nuevo tamaño para el array de la líneas
   ::ArrayResize(m_lines,lines_total-1);
  }

Nos queda analizar el último método que es el más importante, CTextBox::WordWrap(). Para que luego el traspaso de palabras funcione, la llamada a este método debe colocarse en el método CTextBox::ChangeTextBoxSize(). 

Al principio del método CTextBox::WordWrap() se comprueba si los modos del campo de edición multilínea y el traspaso por palabras están activados. Si uno de estos métodos está desactivado, el programa sale del método. Si los modos están activados, es necesario recorrer todas las líneas en el ciclo para activar el algoritmo del traspaso del texto. Aquí, en cada iteración, se usa el método CTextBox::CheckForOverflow() para comprobar si la línea sobrellena el ancho del campo de edición. 

  1. Si la línea no cabe, vemos si ha sido encontrado el espacio más cercano de la parte derecha del campo a partir del que la parte de la línea actual será traspasada a la línea siguiente. El carácter del espacio no se traspasa a la siguiente línea, por eso el índice del espacio se incrementa. Luego, el array de caracteres se aumenta a un elemento, y las líneas inferiores se desplazan a una posición hacia abajo. Se vuelve a concretar el índice según el cual se traspasará la parte de la línea. Después de eso, el texto se traspasa. 
  2. Si la línea encaja, comprobamos si hace falta realizar el traspaso inverso. Al principios de este bloque, se comprueba el indicio del salto de la línea actual. Si existe, el programa pasa a la siguiente iteración. Si la comprobación ha sido superada, luego se determina el número de caracteres a traspasar, después de eso el texto se traspasa a la línea anterior.
//+------------------------------------------------------------------+
//| Clase para crear el campo de edición multilínea                  |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Ajuste de línea
   void              WordWrap(void);
  };
//+------------------------------------------------------------------+
//| Ajuste de línea                                                  |
//+------------------------------------------------------------------+
void CTextBox::WordWrap(void)
  {
//--- Salir si los modos del (1) campo multilínea o (2) traspaso por palabras están desactivados
   if(!m_multi_line_mode || !m_word_wrap_mode)
      return;
//--- Obtenemos el tamaño del array de líneas
   uint lines_total=::ArraySize(m_lines);
//--- Comprobamos si hace falta ajustar el texto por el ancho del campo de edición
   for(uint i=0; i<lines_total; i++)
     {
      //--- Para determinar los primeros índices visibles del (1) carácter y (2) del espacio
      int symbol_index =WRONG_VALUE;
      int space_index  =WRONG_VALUE;
      //--- Indice de la siguiente línea
      uint next_line_index=i+1;
      //--- Si la línea no encaja, traspasamos una parte de la línea actual a la línea nueva
      if(CheckForOverflow(i,symbol_index,space_index))
        {
         //--- Si el espacio ha sido encontrado, no va a traspasarse
         if(space_index!=WRONG_VALUE)
            space_index++;
        //--- Aumentamos el array de líneas a un elemento
         ::ArrayResize(m_lines,++lines_total);
         //--- Desplazamos las líneas desde la posición actual a un punto hacia abajo
         MoveLines(lines_total-1,next_line_index);
         //--- Comprobamos el índice del carácter a partir del que va a traspasarse el texto
         int check_index=(space_index==WRONG_VALUE && symbol_index!=WRONG_VALUE)? symbol_index : space_index;
         //--- Traspasamos el texto a la línea nueva
         WrapTextToNewLine(i,check_index);
        }
      //--- Si la línea encaja, comprobamos si hace falta realizar el traspaso inverso
      else
        {
         //--- Omitimos si (1) es salto de línea o (2) es la última línea
         if(m_lines[i].m_end_of_line || next_line_index>=lines_total)
            continue;
         //--- Determinamos el número de caracteres para el traspaso
         uint wrap_symbols_total=0;
         //--- Si es necesario traspasar el texto restante de la siguiente línea a la actual
         if(WrapSymbolsTotal(i,wrap_symbols_total))
           {
            WrapTextToPrevLine(next_line_index,wrap_symbols_total,true);
            //--- Actualizar el tamaño del array para su uso posterior en el ciclo
            lines_total=::ArraySize(m_lines);
            //--- Paso atrás para evitar la omisión de la línea para la siguiente comprobación
            i--;
           }
         //--- Traspasar sólo lo que cabe
         else
            WrapTextToPrevLine(next_line_index,wrap_symbols_total);
        }
     }
  }

Hemos analizado todos los métodos para el traspaso automático del texto. Ahora vamos a ver cómo funciona todo eso.


Aplicación para la prueba del control

Creamos la aplicación MQL para las pruebas. Vamos a usar la versión ya hecha en el artículo anterior sobre el campo de edición multilínea, pero antes eliminaremos el campo de edición de una línea de la interfaz gráfica. Todo lo demás es lo mismo. Pues, así es como funciona eso en el gráficos en el terminal MetaTrader 5:

Fig. 10. Demostración del traspaso de palabras en el control «Campos de edición multilínea». 

Fig. 10 Demostración del traspaso de palabras en el control «Campos de edición multilínea».


Puede descargar esta aplicación de prueba al final del artículo para estudiarla más detalladamente.


Conclusión

En esta fase del desarrollo de la librería para la creación de las interfaces gráficas, su esquema tiene el siguiente aspecto:

 Fig. 11. Estructura de la librería en la fase actual del desarrollo.


Fig. 11. Estructura de la librería en la fase actual del desarrollo.


Abajo puede descargar la última versión de la librería y los archivos para las pruebas.

Si le surgen algunas preguntas sobre el uso del material de estos archivos, puede dirigirse a la descripción detallada del proceso de desarrollo de la librería en uno de los artículos de esta serie, o bien hacer su pregunta en los comentarios para el artículo. 


Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/3173

Archivos adjuntos |
Interfaces gráficas X: Ordenamiento, reconstrucción de la tabla y controles en las celdas (build 11) Interfaces gráficas X: Ordenamiento, reconstrucción de la tabla y controles en las celdas (build 11)

Seguimos añadiendo nuevas posibilidades a la tabla dibujada que nos permitirán hacer lo siguiente: ordenar los datos, controlar el número de columnas y filas, establecer el tipo de las celdas para adjuntar los controles.

¡Visualice esto! La biblioteca gráfica en MQL5 como un análogo de plot en el lenguaje R ¡Visualice esto! La biblioteca gráfica en MQL5 como un análogo de plot en el lenguaje R

A la hora de investigar y estudiar patrones, la representación visual con la ayuda de gráficos juega un papel fundamental. En los lenguajes populares de programación en la comunidad científica, tales como R y Python, para la visualización se usa la función especial plot. Con su ayuda, es posible dibujar líneas, gráficos de dispersión e histogramas para visualizar patrones. En MQL5 usted puede hacer lo mismo con la ayuda de la clase CGraphics.

Cálculo del coeficiente de Hurst Cálculo del coeficiente de Hurst

En este artículo se explica detalladamente el sentido del exponente de Hurst, la interpretación de sus valores y el algoritmo del cálculo. Se muestran los resultados del análisis de algunos segmentos de los mercados financieros y se presenta el método de trabajo con los productos informáticos de MetaTrader 5 que implementan la idea del análisis fractal.

Tendencia universal con interfaz gráfica Tendencia universal con interfaz gráfica

En este artículo, a base de una serie de indicadores estándar, se crea el indicador universal de tendencia. Se desarrolla la interfaz gráfica para seleccionar el tipo del indicador y configurar sus parámetros. El indicador se visualiza en una ventana separada con las series de iconos de diferentes colores.