Interfaces gráficas X: Selección del texto en el campo de edición multilínea (build 13)

Anatoli Kazharski | 12 junio, 2017


Contenido

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 los artículos de cada parte se puede encontrar la lista de los capítulos con los enlaces, así como 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.

Para poder utilizar a 100% el campo de edición multilínea que hemos analizado en los artículos de la lista de abajo, nos queda implementar la selección del texto, ya que la eliminación de los caracteres uno por uno no es muy conveniente.

La selección del texto con diferentes combinaciones de teclas y la eliminación del texto seleccionado serán las mismas que en cualquier otro editor de texto. Además de eso, seguiremos optimizando el código y prepararemos las clases para el traspaso al proceso final de la segunda fase del desarrollo de la librería, cuando todos los controles estarán dibujados en las imágenes separadas (lienzos para el dibujado).

Aquí, será presentada la versión definitiva de este control de la interfaz de la librería. A continuación, los cambios van a introducirse sólo en el caso de encontrar las soluciones más eficaces respecto a algún algoritmo.

Seguimiento de la pulsación de la tecla Shift

En primer lugar, vamos a completar la clase CKeys, destinada para el trabajo con el teclado, con el método CKeys::KeyShiftState() para determinar el estado actual de la tecla Shift. Esta tecla va a utilizarse en distintas combinaciones para la selección del texto. En el listado de abajo se muestra el código de este método simple. La tecla Shift se considera pulsada si la función ::TerminalInfoInteger() con el identificador TERMINAL_KEYSTATE_SHIFT devuelve el valor inferior a cero.

//+------------------------------------------------------------------+
//| Clase para trabajar con el teclado                               |
//+------------------------------------------------------------------+
class CKeys
  {
public:
   //--- Devuelve el estado de la tecla Shift
   bool              KeyShiftState(void);
  };
//+------------------------------------------------------------------+
//| Devuelve el estado de la tecla Shift                             |
//+------------------------------------------------------------------+
bool CKeys::KeyShiftState(void)
  {
   return(::TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0);
  }

Combinaciones de las teclas para seleccionar el texto

Vamos a considerar todas las combinaciones de las teclas para seleccionar el texto que serán implementadas en nuestro campo de edición. Empecemos con la combinación de dos teclas.

  • Las combinaciones 'Shift + Left' y 'Shift + Right' desplazan el cursor del texto a la izquierda y a la derecha a un carácter, respectivamente. El texto se marca con otro color del fondo y del carácter (pueden ser ajustados por el usuario):

 Fig. 1. Selección del texto con desplazamiento a un carácter a la izquierda y derecha.

Fig. 1. Selección del texto con desplazamiento a un carácter a la izquierda y derecha.

  • Las combinaciones 'Shift + Home' y 'Shift + End' desplazan el cursor al principio o al final de la línea marcando todos los caracteres desde la posición inicial del cursor.

 Fig. 2. Selección del texto con desplazamiento desde la posición inicial del cursor hasta el principio o el final de la línea.

Fig. 2. Selección del texto con desplazamiento desde la posición inicial del cursor hasta el principio o el final de la línea.

  • Las combinaciones 'Shift + Up' y 'Shift + Down' desplazan el cursor del texto a una línea hacia arriba y hacia abajo, respectivamente. En este caso, el texto se selecciona en la línea inicial a partir del cursor hasta el principio de esta línea y hasta el cursor desde el fin de la línea final. Si entre la línea inicial y la línea final hay más líneas, el texto en ellas se selecciona por completo.

 Fig. 3. Selección del texto con desplazamiento a una línea hacia arriba y hacia abajo.

Fig. 3. Selección del texto con desplazamiento a una línea hacia arriba y hacia abajo.

A veces para seleccionar el texto se utilizan las combinaciones de tres teclas. Por ejemplo, cuando es necesario seleccionar varias palabras en una línea, la selección por caracteres será muy agotador. O si necesitamos seleccionar el texto compuesto de varias líneas, incluso la selección por líneas será inconveniente. 

En combinaciones de tres teclas, a parte de la tecla Shift, se utiliza Ctrl. Vamos a considerar todas las combinaciones de este tipo que serán implementadas en este artículo:

  • Las combinaciones 'Ctrl + Shift + Left' y 'Ctrl + Shift + Right' sirven para seleccionar el texto por palabras a la izquierda y a la derecha de la posición actual del cursor, respectivamente:

 Fig. 4. Selección del texto con desplazamiento a una palabra a la izquierda y a la derecha.

Fig. 4. Selección del texto con desplazamiento a una palabra a la izquierda y a la derecha.

  • Las combinaciones de las teclas 'Ctrl + Shift + Home' y 'Ctrl + Shift + End' permiten seleccionar el texto entero hasta el principio de la primera línea y hasta el final de la última línea a partir de la posición actual del cursor:

 Fig. 5. Selección del texto con desplazamiento del cursor hasta el principio y el final del documento.

Fig. 5. Selección del texto con desplazamiento del cursor hasta el principio y el final del documento.

En el siguiente apartado vamos a analizar los métodos que se utilizan para la selección del texto.


Métodos para seleccionar el texto

Por defecto, el texto seleccionado se muestra con caracteres blancos en el fondo azul. Si hace falta, se puede cambiar los colores usando los métodos CTextBox:: SelectedBackColor() y CTextBox:: SelectedTextColor(). 

class CTextBox : public CElement
  {
private:
   //--- Color del fondo y de los caracteres del texto seleccionado
   color             m_selected_back_color;
   color             m_selected_text_color;
   //---
private:
   //--- Color del fondo y de los caracteres del texto seleccionado
   void              SelectedBackColor(const color clr)        { m_selected_back_color=clr;       }
   void              SelectedTextColor(const color clr)        { m_selected_text_color=clr;       }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_selected_text_color(clrWhite),
                           m_selected_back_color(C'51,153,255')
  {
//...
  }

Para seleccionar un texto, hacen falta los campos y los métodos para marcar los índices iniciales y finales de las líneas y caracteres del texto seleccionado. Además de eso, vamos a necesitar un método para resetear estos valores cuando la selección se cancela.

Cada vez, después de pulsar una combinación de teclas para seleccionar el texto, antes del desplazamiento del cursor, va a invocarse el método CTextBox::SetStartSelectedTextIndexes(). Él establece los valores iniciales para los índices de la línea y carácter donde se encuentra el cursor del texto. Los valores van a establecerse sólo en el caso si se trata de la primera llamada al método después del último reseteo de estos valores. Después de llamar a este método, el cursor se desplaza, luego se llama al método CTextBox::SetEndSelectedTextIndexes() que establece los valores finales para los índices de la línea y carácter (es decir, la posición actual del cursor). Si en el proceso del desplazamiento del cursor en el modo de selección del texto, resulta que en este momento él se encuentra en el mismo lugar de donde hemos empezado, entonces los valores se resetean con la llamada al método CTextBox::ResetSelectedText(). Los valores también se resetean en caso de cualquier desplazamiento del cursor, eliminación del texto seleccionado o desactivación del campo de edición.

class CTextBox : public CElement
  {
private:
   //--- Índices iniciales y finales de las líneas y caracteres del texto seleccionado
   int               m_selected_line_from;
   int               m_selected_line_to;
   int               m_selected_symbol_from;
   int               m_selected_symbol_to;
   //---
private:
   //--- Establece los índices (1) iniciales y (2) finales para el texto seleccionado
   void              SetStartSelectedTextIndexes(void);
   void              SetEndSelectedTextIndexes(void);
   //--- Quitar la selección del texto
   void              ResetSelectedText(void);
  };
//+------------------------------------------------------------------+
//| Establecer los índices iniciales para el texto seleccionado      |
//+------------------------------------------------------------------+
void CTextBox::SetStartSelectedTextIndexes(void)
  {
//--- Si los índices iniciales para la selección del texto todavía no están establecidos
   if(m_selected_line_from==WRONG_VALUE)
     {
      m_selected_line_from   =(int)m_text_cursor_y_pos;
      m_selected_symbol_from =(int)m_text_cursor_x_pos;
     }
  }
//+------------------------------------------------------------------+
//| Establecer los índices finales para la selección del texto       |
//+------------------------------------------------------------------+
void CTextBox::SetEndSelectedTextIndexes(void)
  {
//--- Establecer los índices finales para la selección del texto
   m_selected_line_to   =(int)m_text_cursor_y_pos;
   m_selected_symbol_to =(int)m_text_cursor_x_pos;
//--- Si todos los índices son iguales, quitar la selección
   if(m_selected_line_from==m_selected_line_to && m_selected_symbol_from==m_selected_symbol_to)
      ResetSelectedText();
  }
//+------------------------------------------------------------------+
//| Quitar la selección del texto                                    |
//+------------------------------------------------------------------+
void CTextBox::ResetSelectedText(void)
  {
   m_selected_line_from   =WRONG_VALUE;
   m_selected_line_to     =WRONG_VALUE;
   m_selected_symbol_from =WRONG_VALUE;
   m_selected_symbol_to   =WRONG_VALUE;
  }

Los bloques del código que antes han sido utilizados en los métodos para el desplazamiento del cursor, ahora están implementados en unos métodos separados, porque van a usarse de nuevo en los métodos para la selección del texto. Lo mismo se refiere al código para la corrección de las barras de desplazamiento en caso cuando el cursor sale fuera del área visible. 

class CTextBox : public CElement
  {
private:
   //--- Desplazamiento del cursor a un carácter a la izquierda
   void              MoveTextCursorToLeft(void);
   //--- Desplazamiento del cursor a un carácter a la izquierda
   void              MoveTextCursorToRight(void);
   //--- Desplazamiento del cursor a un carácter hacia arriba
   void              MoveTextCursorToUp(void);
   //--- Desplazamiento del cursor a un carácter hacia abajo
   void              MoveTextCursorToDown(void);

   //--- Ajuste de la barra de desplazamiento horizontal
   void              CorrectingHorizontalScrollThumb(void);
   //--- Ajuste de la barra de desplazamiento vertical
   void              CorrectingVerticalScrollThumb(void);
  };

El código entero de los métodos del procesamiento de la pulsación de las combinaciones de teclas, una de las cuales es Shift, es prácticamente idéntico, salvo la llamada a los métodos para el desplazamiento del cursor del texto. Por eso tiene sentido crear un método adicional al que se puede simplemente pasar la dirección del desplazamiento del cursor. Al archivo Enums.mqh ha sido añadida la enumeración ENUM_MOVE_TEXT_CURSOR con varios identificadores (véase el código de abajo) que permiten indicar en qué lugar es necesario mover el cursor del texto:

  • TO_NEXT_LEFT_SYMBOL — a un carácter a la izquierda.
  • TO_NEXT_RIGHT_SYMBOL — a un carácter a la derecha.
  • TO_NEXT_LEFT_WORD — a una palabra a la izquierda.
  • TO_NEXT_RIGHT_WORD — a una palabra a la derecha.
  • TO_NEXT_UP_LINE — a una línea hacia arriba.
  • TO_NEXT_DOWN_LINE — a una línea hacia abajo.
  • TO_BEGIN_LINE — al principio de la línea actual.
  • TO_END_LINE — al final de la línea actual.
  • TO_BEGIN_FIRST_LINE — al principio de la primera línea.
  • TO_END_LAST_LINE — al final de la última línea.
//+----------------------------------------------------------------------+
//| Enumeración de la dirección del desplazamiento del cursor del texto  |
//+----------------------------------------------------------------------+
enum ENUM_MOVE_TEXT_CURSOR
  {
   TO_NEXT_LEFT_SYMBOL  =0,
   TO_NEXT_RIGHT_SYMBOL =1,
   TO_NEXT_LEFT_WORD    =2,
   TO_NEXT_RIGHT_WORD   =3,
   TO_NEXT_UP_LINE      =4,
   TO_NEXT_DOWN_LINE    =5,
   TO_BEGIN_LINE        =6,
   TO_END_LINE          =7,
   TO_BEGIN_FIRST_LINE  =8,
   TO_END_LAST_LINE     =9
  };

Ahora se puede crear el método general para el desplazamiento del cursor del texto CTextBox::MoveTextCursor(), al que será suficiente pasar uno de los identificadores de la lista arriba mencionada. El mismo método va a usarse ahora prácticamente en todos los métodos manejadores de eventos de la pulsación de las teclas en el control CTextBox.

class CTextBox : public CElement
  {
private:
   //--- Desplazamiento del cursor del texto en dirección indicada
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction);
  };
//+------------------------------------------------------------------+
//| Desplazamiento del cursor del texto en dirección indicada        |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction)
  {
   switch(direction)
     {
       //--- Mover el cursor a un carácter a la izquierda
      case TO_NEXT_LEFT_SYMBOL  : MoveTextCursorToLeft();        break;
       //--- Mover el cursor a un carácter a la derecha
      case TO_NEXT_RIGHT_SYMBOL : MoveTextCursorToRight();       break;
      //--- Mover el cursor a una palabra a la izquierda
      case TO_NEXT_LEFT_WORD    : MoveTextCursorToLeft(true);    break;
      //--- Mover el cursor a una palabra a la derecha
      case TO_NEXT_RIGHT_WORD   : MoveTextCursorToRight(true);   break;
      //--- Mover el cursor a una línea hacia arriba
      case TO_NEXT_UP_LINE      : MoveTextCursorToUp();          break;
      //--- Mover el cursor a una línea hacia abajo
      case TO_NEXT_DOWN_LINE    : MoveTextCursorToDown();        break;
      //--- Mover el cursor al principio de la línea actual
      case TO_BEGIN_LINE : SetTextCursor(0,m_text_cursor_y_pos); break;
      //--- Movemos el cursor al final de la línea actual
      case TO_END_LINE :
        {
         //---  Obtenemos el número de caracteres en la línea actual
         uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
         //--- Mover el cursor
         SetTextCursor(symbols_total,m_text_cursor_y_pos);
         break;
        }
      //--- Mover el cursor al principio de la primera línea
      case TO_BEGIN_FIRST_LINE : SetTextCursor(0,0); break;
      //--- Mover el cursor al final de la última línea
      case TO_END_LAST_LINE :
        {
         //--- Obtenemos el número de las líneas y caracteres en la última línea
         uint lines_total   =::ArraySize(m_lines);
         uint symbols_total =::ArraySize(m_lines[lines_total-1].m_symbol);
         //--- Mover el cursor
         SetTextCursor(symbols_total,lines_total-1);
         break;
        }
     }
  }

Se puede reducir aún más el código en este archivo ya que los métodos manejadores de eventos del desplazamiento del cursor y de la selección del texto contienen muchos bloques del código que se repiten. 

Aquí tenemos el ejemplo del bloque repetido en los métodos para el desplazamiento del cursor del texto:

//+------------------------------------------------------------------+
//| Procesamiento de la pulsación de la tecla "Left"                 |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyLeft(const long key_code)
  {
//--- Salir si (1) no es la tecla "Left" o (2) ha sido pulsada la tecla "Ctrl" o (3) ha sido pulsada la tecla «Shift»
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || m_keys.KeyShiftState())
      return(false);
//--- Quitar la selección
   ResetSelectedText();
//--- Mover el cursor del texto a un carácter
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- Ajustamos las barras de desplazamiento
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Actualizar el texto dentro del campo de edición
   DrawTextAndCursor(true);
//--- Enviamos el mensaje sobre ello
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

Aquí tenemos el ejemplo del bloque repetido en los métodos para la selección del texto:

//+------------------------------------------------------------------+
//| Procesamiento de la pulsación de la tecla Shift +Left            |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyShiftAndLeft(const long key_code)
  {
//--- Salir si (1) no es la tecla "Left" o (2) ha sido pulsada la tecla "Ctrl" o (3) la tecla «Shift» no ha sido pulsada
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_keys.KeyShiftState())
      return(false);
//--- Establecer los índices iniciales para la selección del texto
   SetStartSelectedTextIndexes();
//--- Mover el cursor del texto a un carácter
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- Establecer los índices finales para la selección del texto
   SetEndSelectedTextIndexes();
//--- Ajustamos las barras de desplazamiento
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Actualizar el texto dentro del campo de edición
   DrawTextAndCursor(true);
//--- Enviamos el mensaje sobre ello
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

Vamos a implementar otro método adicional CTextBox::MoveTextCursor(), al que también habrá que pasar el identificador con la dirección del desplazamiento, así como la bandera que indique si (1) se trata del desplazamiento del cursor o (2) de la selección del texto.

class CTextBox : public CElement
  {
private:
   //--- Desplazamiento del cursor del texto en dirección indicada
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text);
  };
//+------------------------------------------------------------------+
//| Desplazamiento del cursor del texto en dirección indicada y      |
//| con la condición                                                 |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text)
  {
//--- Si es sólo el desplazamiento del cursor del texto
   if(!with_highlighted_text)
     {
      //--- Quitar la selección
      ResetSelectedText();
      //--- Mover el cursor al principio de la primera línea
      MoveTextCursor(direction);
     }
//--- Si es con la selección
   else
     {
      //--- Establecer los índices iniciales para la selección del texto
      SetStartSelectedTextIndexes();
      //--- Mover el cursor del texto a un carácter
      MoveTextCursor(direction);
      //--- Establecer los índices finales para la selección del texto
      SetEndSelectedTextIndexes();
     }
//--- Ajustamos las barras de desplazamiento
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Actualizar el texto dentro del campo de edición
   DrawTextAndCursor(true);
//--- Enviamos el mensaje sobre ello
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
  }

Los métodos del procesamiento de las combinaciones de teclas para seleccionar el texto se muestran más abajo. Su código es prácticamente idéntico (se diferencian sólo los parámetros), por eso puede verlo en los archivos adjuntos al artículo:

class CTextBox : public CElement
  {
private:
   //--- Procesamiento de la pulsación de la tecla Shift +Left
   bool              OnPressedKeyShiftAndLeft(const long key_code);
   //--- Procesamiento de la pulsación de la tecla Shift + Right
   bool              OnPressedKeyShiftAndRight(const long key_code);
   //--- Procesamiento de la pulsación de la tecla Shift + Up
   bool              OnPressedKeyShiftAndUp(const long key_code);
   //--- Procesamiento de la pulsación de la tecla Shift + Down
   bool              OnPressedKeyShiftAndDown(const long key_code);
   //--- Procesamiento de la pulsación de la tecla Shift + Home
   bool              OnPressedKeyShiftAndHome(const long key_code);
   //--- Procesamiento de la pulsación de la tecla Shift + End
   bool              OnPressedKeyShiftAndEnd(const long key_code);

  //--- Procesamiento de la pulsación de la combinación Ctrl + Shift + Left
   bool              OnPressedKeyCtrlShiftAndLeft(const long key_code);
  //--- Procesamiento de la pulsación de la combinación Ctrl + Shift + Right
   bool              OnPressedKeyCtrlShiftAndRight(const long key_code);
  //--- Procesamiento de la pulsación de la combinación Ctrl + Shift + Home
   bool              OnPressedKeyCtrlShiftAndHome(const long key_code);
  //--- Procesamiento de la pulsación de la combinación Ctrl + Shift + End
   bool              OnPressedKeyCtrlShiftAndEnd(const long key_code);
  };

Hasta ahora el texto se colocaba sobre el lienzo usando líneas enteras. Pero puesto que los caracteres seleccionados y el fondo debajo de él cambian su color, es necesario mostrar el texto carácter por carácter. Con este fin vamos a introducir pequeños cambios en el método CTextBox::TextOut(). 

Además necesitaremos el método adicional CTextBox::CheckSelectedText() para comprobar los caracteres seleccionados. Ya sabemos que durante la selección del texto se memorizan los índices de las líneas y caracteres iniciales/finales del cursor del texto. Por eso, recorriendo los caracteres en el ciclo, podemos identificar con facilidad si un carácter está seleccionado o no dentro de la línea. La lógica es muy simple:

  1. Si el índice inicial de la línea es inferior al índice final, el carácter está seleccionado:
    • Si es la última línea y el carácter a la derecha del último seleccionado
    • Si es la línea inicial y el carácter a la izquierda del inicial seleccionado
    • En las líneas intermedias, todos los caracteres están seleccionados
  2. Si el índice inicial de la línea es superior al índice final, el carácter está seleccionado:
    • Si es la última línea y el carácter a la izquierda del último seleccionado
    • Si es la línea inicial y el carácter a la derecha del inicial seleccionado
    • En las líneas intermedias, todos los caracteres están seleccionados
  3. Si el texto está seleccionado sólo dentro de una línea, el carácter está seleccionado si se encuentra en el diapasón especificado entre el índice inicial y final.
class CTextBox : public CElement
  {
private:
   //--- Comprobación de la presencia del texto seleccionado
   bool              CheckSelectedText(const uint line_index,const uint symbol_index);
  };
//+------------------------------------------------------------------+
//| Comprobación de la presencia del texto seleccionado              |
//+------------------------------------------------------------------+
bool CTextBox::CheckSelectedText(const uint line_index,const uint symbol_index)
  {
   bool is_selected_text=false;
//--- Salir si no hay texto seleccionado
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- Si el índice inicial en la línea es menor
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- La última línea y el carácter a la derecha del último seleccionado
      if((int)line_index==m_selected_line_to && (int)symbol_index>=m_selected_symbol_to)
        { is_selected_text=true; }
      //--- La línea inicial y el carácter a la izquierda del inicial seleccionado
      else if((int)line_index==m_selected_line_from && (int)symbol_index<m_selected_symbol_from)
        { is_selected_text=true; }
      //--- Línea intermedia (se seleccionan todos los caracteres)
      else if((int)line_index>m_selected_line_to && (int)line_index<m_selected_line_from)
        { is_selected_text=true; }
     }
//--- Si el índice inicial en la línea es mayor
   else if(m_selected_line_from<m_selected_line_to)
     {
      //--- La última línea y el carácter a la izquierda del último seleccionado
      if((int)line_index==m_selected_line_to && (int)symbol_index<m_selected_symbol_to)
        { is_selected_text=true; }
      //--- La línea inicial y el carácter a la derecha del inicial seleccionado
      else if((int)line_index==m_selected_line_from && (int)symbol_index>=m_selected_symbol_from)
        { is_selected_text=true; }
      //--- Línea intermedia (se seleccionan todos los caracteres)
      else if((int)line_index<m_selected_line_to && (int)line_index>m_selected_line_from)
        { is_selected_text=true; }
     }
//--- Si el índice inicial y final están en la misma línea
   else
     {
      //--- Encontrada la línea a verificar
      if((int)line_index>=m_selected_line_to && (int)line_index<=m_selected_line_from)
        {
         //--- Si el desplazamiento del cursor va a la derecha y el carácter se encuentra en el diapasón seleccionado
         if(m_selected_symbol_from>m_selected_symbol_to)
           {
            if((int)symbol_index>=m_selected_symbol_to && (int)symbol_index<m_selected_symbol_from)
               is_selected_text=true;
           }
         //--- Si el desplazamiento del cursor va a la izquierda y el carácter se encuentra en el diapasón seleccionado
         else
           {
            if((int)symbol_index>=m_selected_symbol_from && (int)symbol_index<m_selected_symbol_to)
               is_selected_text=true;
           }
        }
     }
//--- Devolver el resultado
   return(is_selected_text);
  }

Al método CTextBox::TextOut(), que sirve para la visualización del texto, hay que añadirle el ciclo interno con el repaso de los caracteres de la línea, en vez de la visualización de la línea entera. Ahí se determina si el carácter a comprobar está seleccionado. Si el carácter está seleccionado, se determina su color, y debajo se dibuja un rectángulo coloreado. Sólo después de eso, se visualiza el carácter

class CTextBox : public CElement
  {
private:
   //--- Visualización del texto en el lienzo
   void              TextOut(void);
  };
//+------------------------------------------------------------------+
//| Visualización del texto en el lienzo                             |
//+------------------------------------------------------------------+
void CTextBox::TextOut(void)
  {
//--- Vaciar el lienzo
   m_canvas.Erase(AreaColorCurrent());
//--- Obtenemos el tamaño del array de líneas
   uint lines_total=::ArraySize(m_lines);
//--- Ajuste en caso de salir fuera del diapasón
   m_text_cursor_y_pos=(m_text_cursor_y_pos>=lines_total)? lines_total-1 : m_text_cursor_y_pos;
//--- Obtenemos el tamaño del array de caracteres
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Si el modo multilínea está activado o el número de caracteres es superior a cero
   if(m_multi_line_mode || symbols_total>0)
     {
      //--- Obtenemos el ancho de la línea
      int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos);
      //--- Obtenemos el alto de la línea y recorremos todas las líneas en el ciclo
      int line_height=(int)LineHeight();
      for(uint i=0; i<lines_total; i++)
        {
         //--- Obtenemos las coordenadas para el texto
         int x=m_text_x_offset;
         int y=m_text_y_offset+((int)i*line_height);
        //--- Obtenemos el tamaño de la línea
         uint string_length=::ArraySize(m_lines[i].m_symbol);
         //--- Dibujamos el texto
         for(uint s=0; s<string_length; s++)
           {
            uint text_color=TextColorCurrent();
            //--- Si hay texto seleccionado, determinamos su color, así como el color del fondo del carácter actual
            if(CheckSelectedText(i,s))
              {
               //--- Color del texto seleccionado
               text_color=::ColorToARGB(m_selected_text_color);
               //--- Calculamos las coordenadas para dibujar el fondo
               int x2=x+m_lines[i].m_width[s];
               int y2=y+line_height-1;
               //--- Dibujamos el color del fondo del carácter
               m_canvas.FillRectangle(x,y,x2,y2,::ColorToARGB(m_selected_back_color));
              }
            //--- Dibujar el carácter
            m_canvas.TextOut(x,y,m_lines[i].m_symbol[s],text_color,TA_LEFT);
            //--- Coordenada X para el carácter siguiente
            x+=m_lines[i].m_width[s];
           }
        }
     }
//--- Si el modo multilínea está desactivado y además en la línea no hay caracteres, se mostrará el texto por defecto
   else
     {
       //--- Dibujar el texto si está especificado
      if(m_default_text!="")
         m_canvas.TextOut(m_area_x_size/2,m_area_y_size/2,m_default_text,::ColorToARGB(m_default_text_color),TA_CENTER|TA_VCENTER);
     }
  }

Los métodos para la selección del texto están implementados, pues así se mostrará todo eso en una aplicación final:

 Fig. 6. Demostración de la selección del texto en el campo de edición de una aplicación MQL.

Fig. 6. Demostración de la selección del texto en el campo de edición de una aplicación MQL.


Métodos para eliminar el texto seleccionado

Ahora vamos a considerar los métodos para eliminar el texto seleccionado. Aquí hay que tomar en cuenta que al eliminar el texto seleccionado, dependiendo de que si está seleccionado en una línea o en varias, van a aplicarse los métodos diferentes.

Para eliminar el texto seleccionado en una línea, vamos a llamar al método CTextBox::DeleteTextOnOneLine(). Al principio de este método, se determina el número de caracteres a eliminar. Luego, si el índice inicial del carácter del texto seleccionado se encuentra a la derecha, los caracteres desde esta posición inicial se desplazan a la izquierda al número de caracteres para la eliminación, y después de eso el array de caracteres de la línea se disminuye en la misma cantidad. 

En los casos cuando el índice inicial del carácter del texto seleccionado se encuentra a la izquierda, entonces el cursor del texto también hay que moverlo a la derecha al número de caracteres que se eliminan.

сlass CTextBox : public CElement
  {
private:
   //--- Elimina el texto seleccionado en una línea
   void              DeleteTextOnOneLine(void);
  };
//+------------------------------------------------------------------+
//| Elimina el texto seleccionado en una línea                       |
//+------------------------------------------------------------------+
void CTextBox::DeleteTextOnOneLine(void)
  {
   int symbols_total     =::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
   int symbols_to_delete =::fabs(m_selected_symbol_from-m_selected_symbol_to);
//--- Si el índice inicial del carácter se encuentra a la derecha
   if(m_selected_symbol_to<m_selected_symbol_from)
     {
      //--- Desplazamos los caracteres al sitio liberado en la línea actual
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_from,m_selected_symbol_to);
     }
//--- Si el índice inicial del carácter se encuentra a la izquierda
   else
     {
      //--- Mover el cursor del texto a la izquierda según el número de caracteres eliminados
      m_text_cursor_x_pos-=symbols_to_delete;
      //--- Desplazamos los caracteres al sitio liberado en la línea actual
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_to,m_selected_symbol_from);
     }
//--- Reducimos el tamaño del array de la línea actual al número de caracteres extraídos de ella
   ArraysResize(m_text_cursor_y_pos,symbols_total-symbols_to_delete);
  }

Para eliminar varias líneas del texto seleccionado, se utilizará el método CTextBox::DeleteTextOnMultipleLines(). Aquí el algoritmo es más complejo. Primero, hay que determinar lo siguiente:

  • El número total de caracteres en la línea inicial y final.
  • La cantidad de las líneas intermedias del texto seleccionado (aparte de la línea inicial y final).
  • El número de caracteres a eliminar en la línea inicial y final.

La consecuencia de siguientes acciones se muestra a continuación. Dependiendo de la dirección de la selección del texto (hacia arriba o hacia abajo), van a traspasarse los índices iniciales y finales en otros métodos.

  • Los caracteres para el traspaso de una línea a la otra que se quedarán tras la eliminación se copian al array dinámico temporal.
  • Se establece un tamaño nuevo para el array receptor (línea).
  • Se añaden los datos a los arrays de la estructura de la línea receptora.
  • Se desplazan las líneas según el número de las líneas a eliminar.
  • El array de las líneas obtiene un tamaño nuevo (se disminuye según el número de las líneas a eliminar).
  • En caso si la línea inicial está por encima de la final (selección del texto hacia abajo), el cursor del texto se desplaza hacia los índices iniciales (de la línea y carácter) del texto seleccionado.
class CTextBox : public CElement
  {
private:
   //--- Elimina el texto seleccionado en varias líneas
   void              DeleteTextOnMultipleLines(void);
  };
//+------------------------------------------------------------------+
//| Elimina el texto seleccionado en varias líneas                   |
//+------------------------------------------------------------------+
void CTextBox::DeleteTextOnMultipleLines(void)
  {
//--- El número total de caracteres en la línea inicial y final
   uint symbols_total_line_from =::ArraySize(m_lines[m_selected_line_from].m_symbol);
   uint symbols_total_line_to   =::ArraySize(m_lines[m_selected_line_to].m_symbol);
//--- Número de líneas intermedias para eliminar
   uint lines_to_delete =::fabs(m_selected_line_from-m_selected_line_to);
//--- Número de caracteres a eliminar en la línea inicial y final
   uint symbols_to_delete_in_line_from =::fabs(symbols_total_line_from-m_selected_symbol_from);
   uint symbols_to_delete_in_line_to   =::fabs(symbols_total_line_to-m_selected_symbol_to);
//--- Si la línea inicial está por debajo de la final
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- Copiamos los caracteres a traspasar al array
      string array[];
      CopyWrapSymbols(m_selected_line_from,m_selected_symbol_from,symbols_to_delete_in_line_from,array);
      //--- Establecemos un tamaño nuevo para la línea receptora
      uint new_size=m_selected_symbol_to+symbols_to_delete_in_line_from;
      ArraysResize(m_selected_line_to,new_size);
      //--- Añadir los datos a los arrays de la estructura de la línea receptora
      PasteWrapSymbols(m_selected_line_to,m_selected_symbol_to,array);
      //--- Obtenemos el tamaño del array de líneas
      uint lines_total=::ArraySize(m_lines);
      //--- Desplazamos las líneas en el número de las líneas a eliminar
      MoveLines(m_selected_line_to+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- Establecemos nuevo tamaño para el array de la líneas
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
     }
//--- Si la línea inicial está por encima de la final
   else
     {
      //--- Copiamos los caracteres a traspasar al array
      string array[];
      CopyWrapSymbols(m_selected_line_to,m_selected_symbol_to,symbols_to_delete_in_line_to,array);
      //--- Establecemos un tamaño nuevo para la línea receptora
      uint new_size=m_selected_symbol_from+symbols_to_delete_in_line_to;
      ArraysResize(m_selected_line_from,new_size);
      //--- Añadir los datos a los arrays de la estructura de la línea receptora
      PasteWrapSymbols(m_selected_line_from,m_selected_symbol_from,array);
      //--- Obtenemos el tamaño del array de líneas
      uint lines_total=::ArraySize(m_lines);
      //--- Desplazamos las líneas en el número de las líneas a eliminar
      MoveLines(m_selected_line_from+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- Establecemos nuevo tamaño para el array de la líneas
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
      //--- Mover el cursor a la posición inicial en la selección
      SetTextCursor(m_selected_symbol_from,m_selected_line_from);
     }
  }

En el método principal para eliminar el texto CTextBox::DeleteSelectedText() se determina cuál de los métodos arriba mencionados se invoca. Una vez eliminado el texto seleccionado, los valores de los índices iniciales y finales se resetean. Luego hay que volver a calcular los tamaños del campo de edición porque probablemente se haya cambiado el número de las líneas. Además, es probable que se haya cambiado el ancho máximo de la línea según el que se calcula el ancho del campo de edición. Al final del método, se envía el mensaje sobre el desplazamiento del cursor del texto. Si el texto ha sido seleccionado y eliminado, el método devuelve true. Si al llamar al método resulta que no hay texto seleccionado, el método devuelve false.

class CTextBox : public CElement
  {
private:
   //--- Elimina el texto seleccionado
   void              DeleteSelectedText(void);
  };
//+------------------------------------------------------------------+
//| Elimina el texto seleccionado                                    |
//+------------------------------------------------------------------+
bool CTextBox::DeleteSelectedText(void)
  {
//--- Salir si el texto no está seleccionado
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- Si se eliminan los caracteres en una línea
   if(m_selected_line_from==m_selected_line_to)
      DeleteTextOnOneLine();
//--- Si se eliminan los caracteres en varias líneas
   else
      DeleteTextOnMultipleLines();
//--- Quitar la selección del texto
   ResetSelectedText();
//--- Calcular los tamaños del campo de edición
   CalculateTextBoxSize();
//--- Establecer nuevo tamaño para el campo de edición
   ChangeTextBoxSize();
//--- Ajustamos las barras de desplazamiento
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Actualizar el texto dentro del campo de edición
   DrawTextAndCursor(true);
//--- Enviamos el mensaje sobre ello
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

El método CTextBox::DeleteSelectedText() se invoca no sólo cuando se pulsa la tecla Backspace, sino también: (1) cuando se introduce nuevo carácter y (2) cuando se pulsa la tecla Enter. En estos casos el texto primero se elimina y luego se realiza la acción correspondiente a la tecla pulsada.

Pues así se mostrará todo eso en una aplicación final:

 Fig. 7. Demostración de la eliminación del texto seleccionado.

Fig. 7. Demostración de la eliminación del texto seleccionado.

Clase para trabajar con los datos de la imagen

Como adición, en este artículo vamos a considerar la nueva clase (CImage) para el trabajo con los datos de la imagen. Va a usarse varias veces en muchas clases de los controles de la librería donde es necesario dibujar una imagen. La clase se encuentra en el archivo Objects.mqh

Propiedades de la clase:
  • arrays de los píxeles de la imagen;
  • ancho de la imagen;
  • alto de la imagen;
  • ruta hacia el archivo de la imagen.
//+------------------------------------------------------------------+
//| Clase para almacenar los datos de la imagen                      |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // Array de los píxeles de la imagen
   uint              m_image_width;  // Ancho de la imagen
   uint              m_image_height; // Alto de la imagen
   string            m_bmp_path;     // Ruta hacia el archivo de la imagen
public:
   //--- (1) Tamaño del array de datos, (2) establecer/devolver datos (color del píxel)
   uint              DataTotal(void)                              { return(::ArraySize(m_image_data)); }
   uint              Data(const uint data_index)                  { return(m_image_data[data_index]);  }
   void              Data(const uint data_index,const uint data)  { m_image_data[data_index]=data;     }
   //--- Establecer/devolver el ancho de la imagen
   void              Width(const uint width)                      { m_image_width=width;               }
   uint              Width(void)                                  { return(m_image_width);             }
   //--- Establecer/devolver el alto de la imagen
   void              Height(const uint height)                    { m_image_height=height;             }
   uint              Height(void)                                 { return(m_image_height);            }
   //--- Establecer/devolver la ruta hacia la imagen
   void              BmpPath(const string bmp_file_path)          { m_bmp_path=bmp_file_path;          }
   string            BmpPath(void)                                { return(m_bmp_path);                }

  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CImage::CImage(void) : m_image_width(0),
                       m_image_height(0),
                       m_bmp_path("")
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CImage::~CImage(void)
  {
  }

Para guardar la imagen y sus propiedades, se usa el método CImage::ReadImageData(). Este método lee la imagen según la ruta especificada y guarda sus datos.

class CImage
  {
public:
   //--- Lee y guarda los datos de la imagen enviada
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| Guarda la imagen traspasada en el array                          |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- Resetear el último error
   ::ResetLastError();
//--- Guardamos la ruta hacia la imagen
   m_bmp_file_path=bmp_file_path;
//--- Leer y guardar los datos de la imagen
   if(!::ResourceReadImage(m_bmp_file_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > error: ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

A veces podemos necesitar hacer una copia de la imagen del mismo tipo (CImage). Para eso disponemos del método CImage::CopyImageData(). Al principio del método, al array receptor se le establece el tamaño del array fuente. Luego en el ciclo se copian los datos del array fuente al array receptor.

class CImage
  {
public:
   //--- Copia los datos de la imagen traspasada
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| Copia los datos de la imagen traspasada                          |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- Obtenemos los tamaños del array receptor y array fuente
   uint data_total        =DataTotal();
   uint source_data_total =::GetPointer(array_source).DataTotal();
//--- Cambiar el tamaño del array receptor
   ::ArrayResize(m_image_data,source_data_total);
//--- Copiamos los datos
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=::GetPointer(array_source).Data(i);
  }

Hasta esta actualización, en la clase CCanvasTable se utilizaba la estructura para almacenar los datos de la imagen. Ahora con la presencia de la clase CImage han sido introducidos los cambios correspondientes. 


Conclusión

En este artículo, hemos terminado el desarrollo del control «Campo de edición del texto multilínea». Su principal particularidad consiste en el hecho de que ahora no tiene limitaciones para la cantidad de caracteres introducidos y se puede introducir varias líneas. Eso faltaba mucho en el objeto gráfico estándar del tipo OBJ_EDIT. En el siguiente artículo, seguiremos desarrollando el tema «Controles en las celdas de la tabla»: añadiremos la posibilidad de cambiar los valores en las celdas de la tabla a través del control considerado en este artículo. Además, pasaremos algunos controles en un modo nuevo: ellos van a dibujarse en vez de construirse a base de varios objetos gráficos estándar.

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. 8. Estructura de la librería en la fase actual del desarrollo.

Fig. 8. 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 que han sido presentadas en el artículo.

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.