Graphisches Interface X: Textauswahl im mehrzeiligen Textfeld (build 13)

Anatoli Kazharski | 4 Juli, 2017


Inhaltsverzeichnis


Einführung

Der erste Artikel Grafische Interfaces I: Vorbereitung der Bibliotheksstruktur (Kapitel 1) beschreibt im Detail den Zweck der Bibliothek. Eine vollständige Liste mit den Verweisen auf die Artikel des ersten Teils findet sich am Ende jeden Kapitels. Dort können Sie auch die komplette, aktuelle Version der Bibliothek zum derzeitigen Entwicklungsstand herunterladen. Die Dateien müssen in die gleichen Verzeichnissen kopiert werden, wie sie sich in dem Archiv befinden.

Um das mehrzeilige Textfeld aus den unten angegebenen Artikeln richtig nutzen zu können, sollte es möglich sein, Text zu markieren, denn ein Löschen von Text, Buchstabe für Buchstabe, ist ziemlich unbequem.

Die Textauswahl mittels verschiedener Tastenkombinationen und das Löschen von markiertem Text funktioniert genau so wie in jedem anderen Texteditor. Zusätzlich wird der Code weiter optimiert, und es werden die Klassen für die zweite Stufe in Richtung der endgültigen Version der Bibliothek vorbereitet, die alle Elemente als Einzelbilder vor einem Hintergrund darstellt. 

Die endgültige Version dieses Elementes wird hier entwickelt. Jede spätere Änderung wird nur dann vorgenommen werden, wenn für einen bestimmten Algorithmus eine effizientere Lösung gefunden werden würde.

Abfangen der gedrückten Shift-Taste

Ergänzen wir als aller erstes die bereits besprochenen Klasse CKeys mit der Methode CKeys::KeyShiftState(), um den aktuellen Zustand der Shift-Taste abzufragen. Diese Taste wird in mehreren Kombinationen zur Textauswahl verwendet. Unten ist der Code dieser einfachen Methode aufgelistet. Die Shift-Taste gilt als gedrückt, wenn die Funktion ::TerminalInfoInteger() mit der Identifikator TERMINAL_KEYSTATE_SHIFT einen Wert ungleich Null zurückgibt.

//+------------------------------------------------------------------+
//| Arbeitsklasse der Tastatur                                       |
//+------------------------------------------------------------------+
class CKeys
  {
public:
   //--- Rückgabe des Zustandes der Shift-Taste
   bool              KeyShiftState(void);
  };
//+------------------------------------------------------------------+
//| Rückgabe des Zustandes der Shift-Taste                           |
//+------------------------------------------------------------------+
bool CKeys::KeyShiftState(void)
  {
   return(::TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0);
  }



Tastenkombinationen zum Markieren von Text

Kommen wir jetzt zu allen Tastenkombinationen für die Textauswahl, die wir im Textfeld verwenden wollen. Beginnen wir mit der Kombination von zwei Tasten.

  • Die Kombinationen von 'Shift + Links' und 'Shift + Rechts' bewegen den Kursor nach links bzw. rechts, jeweils um einen Buchstaben. Der Hintergrund und die Buchstaben des Textes werden durch andere Farben (anpassbar durch Benutzer) hervorgehoben:

 Fig. 1 Textauswahl durch eine buchstabenweise Kursorbewegung nach links und rechts.

Fig. 1 Textauswahl durch eine buchstabenweise Kursorbewegung nach links und rechts.


  • Die Kombinationen 'Shift + Pos1' and 'Shift + Ende' bewegen den Textkursor an den Anfang oder das Ende einer Zeile und markieren alle Buchstaben ab der Kursorposition.

 Fig. 2. Textauswahl mit dem Kursor ab der Startposition bis zum Anfang und zum Ende der Zeile.

Fig. 2. Textauswahl mit dem Kursor ab der Startposition bis zum Anfang und zum Ende der Zeile.


  • Die Kombinationen von 'Shift + Hoch' und 'Shift + Runter' bewegen den Kursor eine Zeile nach oben oder unten. Der Text wird ab der Startposition des Kursors markiert, nach oben über den Zeilenanfang und nach unten über das Zeilenende bis zur Endposition des Kursor. Gibt es mehr als eine Zeile zwischen Anfangs- und Endzeile, werden diese zur Gänze markiert.

 Fig. 3. Textauswahl durch Auf- und Abbewegungen.

Fig. 3. Textauswahl durch Auf- und Abbewegungen.


Manchmal werden Kombination aus drei Tasten zur Textauswahl verwendet. Wenn zum Beispiel schnell mehrere Worte einer Zeile ausgewählt werden sollen, ein Markieren, Buchstabe für Buchstabe, wäre zu langwierig. Auch wenn es notwendig sein sollte, Text auszuwählen, der mehrere Zeilen umfasst, wäre eine Auswahl Zeile für Zeile nicht so bequem. 

Die Kombination aus drei Tasten verwendet die Taste Ctrl zusammen mit Shift. Betrachten wir alle Kombinationen, die in diesem Artikel beschrieben werden:

  • Die Kombinationen 'Ctrl + Shift + Left' und 'Ctrl + Shift + Right' wählen ganze Wörter durch die Kursorbewegungen nach links oder rechts:

 Fig. 4. Fig. 1 Textauswahl durch eine wortweise Kursorbewegung nach links und rechts.

Fig. 4. Fig. 1 Textauswahl durch eine wortweise Kursorbewegung nach links und rechts.


  • Die Kombinationen 'Ctrl + Shift + Home' und 'Ctrl + Shift + End' markieren den gesamten Text von der aktuellen Kursorposition bis zum Anfang der ersten oder dem Ende der letzten Zeile:

 Fig. 5. Textauswahl mit dem Kursor ab der Startposition bis zum Anfang und zum Ende des Dokumentes.

Fig. 5. Textauswahl mit dem Kursor ab der Startposition bis zum Anfang und zum Ende des Dokumentes.


Der nächste Abschnitt behandelt die Methoden der Textauswahl.


Verfahren zum Auswählen von Text

Standardmäßig wird der markierte Text mit weißen Buchstaben vor einem blauen Hintergrund dargestellt. Falls nötig, können mit den Methoden CTextBox:: SelectedBackColor() und CTextBox:: SelectedTextColor() die Farben geändert werden. 

class CTextBox : public CElement
  {
private:
   //--- Hintergrund- und Buchstabenfarben des markierten Textes
   color             m_selected_back_color;
   color             m_selected_text_color;
   //---
private:
   //--- Hintergrund- und Buchstabenfarben des markierten Textes
   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')
  {
//...
  }

Um Text zu markieren, werden Variablen und Methoden benötigt, die die Indizes der ersten und letzten Zeilen und Buchstaben des markierten Textes bestimmen. Weiters eine Methode, die diese Werte zurücksetzt, wenn die Markierung aufgehoben wird.

Jedes Mal, wenn eine der Tastenkombinationen gedrückt wird, wird die Methode CTextBox::SetStartSelectedTextIndexes() aufgerufen, bevor der Kursor verschoben wird. Sie bestimmt die Indices der Zeile und des Buchstabens, wo der Textkursor sich gerade befindet. Diese Werte werden nur gesetzt, wenn es der erste Aufruf der Methode ist, nach einer vorherigen Zurücksetzung. Der Kursor wird erst nach dem Aufruf dieser Methode bewegt. Dann wird die Methode CTextBox::SetEndSelectedTextIndexes() aufgerufen, sie bestimmt die Endwerte der Indices der Zeile und des Buchstabens (es sind die der Position des Textkursors). Wenn es sich während des Verschiebens des Textkursors im Markiermodus herausstellt, dass der Kursor sich wieder genau dort befindet, von wo er startete, werden die Werte durch die Methode CTextBox::ResetSelectedText() zurückgesetzt. Die Werte werden auch bei jeder Bewegung des Textkursors, durch das Löschen der Textauswahl oder bei der Deaktivierung des Textfeldes.

class CTextBox : public CElement
  {
private:
   //--- Die Indices der Zeilen und Buchstaben des Anfangs und Endes (der Textauswahl)
   int               m_selected_line_from;
   int               m_selected_line_to;
   int               m_selected_symbol_from;
   int               m_selected_symbol_to;
   //---
private:
   //--- Bestimmen der Indices (1) des Anfangs und (2) Endes der Textauswahl
   void              SetStartSelectedTextIndexes(void);
   void              SetEndSelectedTextIndexes(void);
   //--- Rücksetzen der Textauswahl
   void              ResetSelectedText(void);
  };
//+------------------------------------------------------------------+
//| Bestimmen der Startindices für die Textauswahl                   |
//+------------------------------------------------------------------+
void CTextBox::SetStartSelectedTextIndexes(void)
  {
//--- Wenn der die Startindices für die Textauswahl noch nicht bestimmt wurden
   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;
     }
  }
//+------------------------------------------------------------------+
//| Bestimmen der Endindices für die Textauswahl                     |
//+------------------------------------------------------------------+
void CTextBox::SetEndSelectedTextIndexes(void)
  {
//--- Bestimmen der Endindices für die Textauswahl
   m_selected_line_to   =(int)m_text_cursor_y_pos;
   m_selected_symbol_to =(int)m_text_cursor_x_pos;
//--- Löschen die Auswahl, wenn alle Indices gleich sind
   if(m_selected_line_from==m_selected_line_to && m_selected_symbol_from==m_selected_symbol_to)
      ResetSelectedText();
  }
//+------------------------------------------------------------------+
//| Rücksetzen der Textauswahl                                       |
//+------------------------------------------------------------------+
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;
  }

Teile des Codes, die früher von Methoden zur Bewegung des Kursors verwendet wurden, werden jetzt in eine eigene Methode verschoben, da sie für die Textauswahl wiederholt verwendet werden. Dasselbe gilt für den Code zur Anpassung der Bildlaufleisten, falls sich der Textkursor aus dem sichtbaren Teil bewegt. 

class CTextBox : public CElement
  {
private:
   //--- Bewegen des Textkursors um einen Buchstaben nach links
   void              MoveTextCursorToLeft(void);
   //--- Bewegen des Textkursors um einen Buchstaben nach rechts
   void              MoveTextCursorToRight(void);
   //--- Bewegen des Textkursors um einen Buchstaben nach oben
   void              MoveTextCursorToUp(void);
   //--- Bewegen des Textkursors um einen Buchstaben nach unten
   void              MoveTextCursorToDown(void);

   //--- Anpassen der horizontalen Bildlaufleiste
   void              CorrectingHorizontalScrollThumb(void);
   //--- Anpassen der vertikalen Bildlaufleiste
   void              CorrectingVerticalScrollThumb(void);
  };

Alle Methoden, die auf die gedrückten Tastenkombinationen reagieren, (eine von ihnen ist Shift) beinhalten praktisch den gleichen Code, bis auf jene, die den Kursor bewegt. Daher ist eine weitere Methode sinnvoll, der einfach nur die Bewegungsrichtung des Textkursors übergeben wird. Die Enumeration ENUM_MOVE_TEXT_CURSOR mit mehreren Identifikatoren (siehe die Auflistung unten) wurde der Datei Enums.mqh hinzugefügt. Sie kann für die Bewegungsrichtung des Textkursor verwendet werden:

  • TO_NEXT_LEFT_SYMBOL — ein Buchstabe nach links.
  • TO_NEXT_RIGHT_SYMBOL — ein Buchstabe nach rechts.
  • TO_NEXT_LEFT_WORD — ein Wort nach links.
  • TO_NEXT_RIGHT_WORD — ein Wort nach rechts.
  • TO_NEXT_UP_LINE — eine Zeile nach oben.
  • TO_NEXT_DOWN_LINE — eine Zeile nach oben.
  • TO_BEGIN_LINE — an den Anfang der aktuellen Zeile.
  • TO_END_LINE — an das Ende der aktuellen Zeile.
  • TO_BEGIN_FIRST_LINE — an den Anfang der ersten Zeile.
  • TO_END_LAST_LINE — an das Ende der letzten Zeile.

//+------------------------------------------------------------------+
//| Enumeration der Bewegungsrichtung des Textkursors                |
//+------------------------------------------------------------------+
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
  };

Jetzt können wir die allgemeine Methode zur Bewegung des Textkursors erstellen — CTextBox::MoveTextCursor(), ihr muss nur eine der Identifikatoren der oberen Liste übergeben werden. Die gleiche Methode wird in praktisch allen Methoden verwendet, die auf Tastendrücke im Element CTextBox reagieren.

class CTextBox : public CElement
  {
private:
   //--- Bewegen des Textkursor in die angegebene Richtung
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction);
  };
//+------------------------------------------------------------------+
//| Bewegen des Textkursor in die angegebene Richtung                |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction)
  {
   switch(direction)
     {
      //--- Bewegen des Kursors um einen Buchstaben nach links
      case TO_NEXT_LEFT_SYMBOL  : MoveTextCursorToLeft();        break;
      //--- Bewegen des Kursors um einen Buchstaben nach rechts
      case TO_NEXT_RIGHT_SYMBOL : MoveTextCursorToRight();       break;
      //--- Bewegen des Kursors um einen Wort nach links
      case TO_NEXT_LEFT_WORD    : MoveTextCursorToLeft(true);    break;
      //--- Bewegen des Kursors um einen Wort nach rechts
      case TO_NEXT_RIGHT_WORD   : MoveTextCursorToRight(true);   break;
      //--- Bewegen des Kursors um eine Zeile nach oben
      case TO_NEXT_UP_LINE      : MoveTextCursorToUp();          break;
      //--- Bewegen des Kursors um eine Zeile nach unten
      case TO_NEXT_DOWN_LINE    : MoveTextCursorToDown();        break;
      //--- Bewegen des Kursors an den Zeilenanfang
      case TO_BEGIN_LINE : SetTextCursor(0,m_text_cursor_y_pos); break;
      //--- Bewegen des Kursors an das Zeilenende
      case TO_END_LINE :
        {
         //--- Abfrage der Zeichenzahl der aktuellen Zeile
         uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
         //--- Bewegen des Kursors
         SetTextCursor(symbols_total,m_text_cursor_y_pos);
         break;
        }
      //--- Bewegen des Kursors an den Anfang der ersten Zeile
      case TO_BEGIN_FIRST_LINE : SetTextCursor(0,0); break;
      //--- Bewegen des Kursors an das Ende der letzten Zeile
      case TO_END_LAST_LINE :
        {
         //--- Ermitteln der Anzahl der Zeilen und Buchstaben der letzten Zeile
         uint lines_total   =::ArraySize(m_lines);
         uint symbols_total =::ArraySize(m_lines[lines_total-1].m_symbol);
         //--- Bewegen des Kursors
         SetTextCursor(symbols_total,lines_total-1);
         break;
        }
     }
  }

Der Codes dieser Datei kann deutlich reduziert werden, weil die Methoden zur Bewegung des Textkursors und zur Textauswahl viele, sich wiederholende Teile aufweisen. 

Beispiel eines sich wiederholenden Codabschnitts in der Methode für das Bewegen des Textkursors:

//+------------------------------------------------------------------+
//| Handhabung der gedrückten Taste Links                            |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyLeft(const long key_code)
  {
//--- Verlassen, wenn (1) es nicht die Taste Links ist oder (2) die Taste Ctrl gedrückt ist oder (3) die Taste Shift gedrückt ist
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || m_keys.KeyShiftState())
      return(false);
//--- Rücksetzen der Auswahl
   ResetSelectedText();
//--- Bewegen des Textkursors um einen Buchstaben nach links
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- Anpassen der Bildlaufleiste
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Aktualisieren des Textes des Textfeldes
   DrawTextAndCursor(true);
//--- Senden einer Nachricht darüber
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

Beispiel eines sich wiederholenden Codes in der Methode zur Textauswahl:

//+------------------------------------------------------------------+
//| Zeitgleiches Drücken der Tasten Shift + Links                    |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyShiftAndLeft(const long key_code)
  {
//--- Verlassen, wenn (1) es nicht die Taste Links ist oder (2) die Taste Ctrl gedrückt ist oder (3) die Taste Shift nicht gedrückt ist
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_keys.KeyShiftState())
      return(false);
//--- Bestimmen der Startindices für die Textauswahl
   SetStartSelectedTextIndexes();
//--- Bewegen des Textkursors um einen Buchstaben nach links
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- Bestimmen der Endindices für die Textauswahl
   SetEndSelectedTextIndexes();
//--- Anpassen der Bildlaufleiste
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Aktualisieren des Textes des Textfeldes
   DrawTextAndCursor(true);
//--- Senden einer Nachricht darüber
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }


Führen wir noch eine weitere (überladene) Methode CTextBox::MoveTextCursor() ein. Der Methode muss der Identifikator der Kursorbewegung übergeben werden, zusammen mit dem Kennzeichnen, dass es entweder (1) eine Bewegung des Textkursors oder oder (2) eine Textauswahl ist.

class CTextBox : public CElement
  {
private:
   //--- Bewegen des Textkursor in die angegebene Richtung
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text);
  };
//+------------------------------------------------------------------+
//| Bewegen des Textkursors in die angegebenen Richtung und          |
//| mit einer Bedingung                                              |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text)
  {
//--- Wenn sich nur der Textkursor bewegt
   if(!with_highlighted_text)
     {
      //--- Rücksetzen der Auswahl
      ResetSelectedText();
      //--- Bewegen des Kursors an den Anfang der ersten Zeile
      MoveTextCursor(direction);
     }
//--- Wenn Text ausgewählt wird
   else
     {
      //--- Bestimmen der Startindices für die Textauswahl
      SetStartSelectedTextIndexes();
      //--- Bewegen des Textkursors um einen Buchstaben nach links
      MoveTextCursor(direction);
      //--- Bestimmen der Endindices für die Textauswahl
      SetEndSelectedTextIndexes();
     }
//--- Anpassen der Bildlaufleiste
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Aktualisieren des Textes des Textfeldes
   DrawTextAndCursor(true);
//--- Senden einer Nachricht darüber
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
  }


Die Methoden der Tastenkombinationen, die die Textauswahl betreffen, sind unten gezeigt. Deren Code ist fast identisch (sie unterscheiden sich nur in den Parametern). Daher können Sie den Code in den dem Artikel beigefügten Dateien studieren:

class CTextBox : public CElement
  {
private:
   //--- Reagieren auf die gedrückten Tasten Shift + Links
   bool              OnPressedKeyShiftAndLeft(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Shift + Rechts
   bool              OnPressedKeyShiftAndRight(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Shift + Hoch
   bool              OnPressedKeyShiftAndUp(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Shift + Runter
   bool              OnPressedKeyShiftAndDown(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Shift + Home
   bool              OnPressedKeyShiftAndHome(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Shift + End
   bool              OnPressedKeyShiftAndEnd(const long key_code);

   //--- Reagieren auf die gedrückten Tasten Ctrl + Shift + Links
   bool              OnPressedKeyCtrlShiftAndLeft(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Ctrl + Shift + Rechts
   bool              OnPressedKeyCtrlShiftAndRight(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Ctrl + Shift + Home
   bool              OnPressedKeyCtrlShiftAndHome(const long key_code);
   //--- Reagieren auf die gedrückten Tasten Ctrl + Shift + End
   bool              OnPressedKeyCtrlShiftAndEnd(const long key_code);
  };


Bis jetzt wurde nur Text in ganzen Zeilen vor einem Hintergrund behandelt. Da aber die ausgewählten Buchstaben und ihr Hintergrund ihre Farben ändern, muss der Text Buchstabe für Buchstabe ausgegeben werden. Dafür machen wir ein paar Änderungen in der Methode CTextBox::TextOut(). 

Dazu aber benötigen wir eine weitere Methode CTextBox::CheckSelectedText(), die die ausgewählten Buchstaben überprüft. Wir wissen bereits, dass während der Textauswahl die Indices des Textkursors der Zeilen und Buchstaben des Anfangs und des Endes gesichert werden. Daher ist es einfach zu bestimmen, ob ein Buchstabe einer Zeile ausgewählt wurde oder nicht, einfach durch ein Iteration in einer Schleife über die Buchstaben. Die Logik ist einfach:

  1. Falls der erste Index der Zeile kleiner ist als der letzte, wird der Buchstabe ausgewählt:
    • Wenn dies die letzte Zeile ist und der Buchstabe steht rechts des zuletzt gewählten
    • Wenn dies die Anfangszeile ist und der Buchstabe links vom anfänglich gewählten
    • Alle Buchstaben der dazwischenliegenden Zeilen werden ausgewählt
  2. Falls der erste Index der Zeile größer ist als der letzte, wird der Buchstabe ausgewählt:
    • Wenn dies die letzte Zeile ist und der Buchstabe steht links des zuletzt gewählten
    • Wenn dies die Anfangszeile ist und der Buchstabe rechts vom anfänglich gewählten
    • Alle Buchstaben der dazwischenliegenden Zeilen werden ausgewählt
  3. Befindet sich der ausgewählte Text nur in einer Zeile, wird ein Buchstaben dann gewählt, wenn er zwischen dem ersten und dem letzten Index des Buchstaben liegt.

class CTextBox : public CElement
  {
private:
   //--- Prüfen der Existenz des ausgewählten Textes
   bool              CheckSelectedText(const uint line_index,const uint symbol_index);
  };
//+------------------------------------------------------------------+
//| Prüfen der Existenz des ausgewählten Textes                      |
//+------------------------------------------------------------------+
bool CTextBox::CheckSelectedText(const uint line_index,const uint symbol_index)
  {
   bool is_selected_text=false;
//--- Verlassen, wenn es keine Textauswahl gibt
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- Wenn der erste Index auf einer Zeile weiter unten ist
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- Die Endzeile und der Buchstabe rechts des zuletzt gewählten ist
      if((int)line_index==m_selected_line_to && (int)symbol_index>=m_selected_symbol_to)
        { is_selected_text=true; }
      //--- Die Anfangszeile und der Buchstabe links vom anfänglich gewählten
      else if((int)line_index==m_selected_line_from && (int)symbol_index<m_selected_symbol_from)
        { is_selected_text=true; }
      //--- Zeilen dazwischen (alle Buchstaben werden ausgewählt)
      else if((int)line_index>m_selected_line_to && (int)line_index<m_selected_line_from)
        { is_selected_text=true; }
     }
//--- Wenn der erste Index auf einer Zeile weiter oben ist
   else if(m_selected_line_from<m_selected_line_to)
     {
      //--- Die letzte Zeile und der Buchstabe ist links des zuletzt gewählten
      if((int)line_index==m_selected_line_to && (int)symbol_index<m_selected_symbol_to)
        { is_selected_text=true; }
      //--- Die Anfangszeile und der Buchstabe ist rechts vom anfänglich gewählten
      else if((int)line_index==m_selected_line_from && (int)symbol_index>=m_selected_symbol_from)
        { is_selected_text=true; }
      //--- Zeilen dazwischen (alle Buchstaben werden ausgewählt)
      else if((int)line_index<m_selected_line_to && (int)line_index>m_selected_line_from)
        { is_selected_text=true; }
     }
//--- Wenn Anfangs- und Endindex auf derselben Zeile liegen
   else
     {
      //--- Finden der geprüften Zeile
      if((int)line_index>=m_selected_line_to && (int)line_index<=m_selected_line_from)
        {
         //--- Wenn sich der Kursor nach rechts bewegt und der Buchstabe ist innerhalb des gewählten Bereiches
         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;
           }
         //--- Wenn sich der Kursor nach links bewegt und der Buchstabe ist innerhalb des gewählten Bereiches
         else
           {
            if((int)symbol_index>=m_selected_symbol_from && (int)symbol_index<m_selected_symbol_to)
               is_selected_text=true;
           }
        }
     }
//--- Rückgabe des Ergebnisses
   return(is_selected_text);
  }

In der Methode CTextBox::TextOut(), die den Text ausgibt, muss eine interne Schleife mit einer Iteration über die Buchstaben der Zeile ergänzt werden, anstatt die Zeile als Ganzes auszugeben. Sie bestimmt, ob der überprüfte Buchstabe ausgewählt wurde. Falls der Buchstabe gewählt wurde, wird dessen Farbe bestimmt und ein ausgefülltes Rechteck wird gezeichnet unter dem Buchstaben. Nach all dem wird der Buchstabe selbst ausgegeben

class CTextBox : public CElement
  {
private:
   //--- Ausgabe des Textes auf dem Hintergrund
   void              TextOut(void);
  };
//+------------------------------------------------------------------+
//| Ausgabe des Textes auf dem Hintergrund                           |
//+------------------------------------------------------------------+
void CTextBox::TextOut(void)
  {
//--- Löschen des Hintergrundes
   m_canvas.Erase(AreaColorCurrent());
//--- Abfrage der Größe des Zeilenarrays
   uint lines_total=::ArraySize(m_lines);
//--- Korrektur, wenn die Größe überschritten wurde
   m_text_cursor_y_pos=(m_text_cursor_y_pos>=lines_total)? lines_total-1 : m_text_cursor_y_pos;
//--- Abfrage der Größe des Arrays mit den Zeichen
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Wenn mehrzeilig oder die Anzahl der Zeichen ist größer Null
   if(m_multi_line_mode || symbols_total>0)
     {
      //--- Abfrage der Zeilenbreite
      int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos);
      //--- Abfragen der Zeilenhöhe und Iteration über alle Zeilen in einer Schleife
      int line_height=(int)LineHeight();
      for(uint i=0; i<lines_total; i++)
        {
         //--- Abfrage der Koordinaten für den Text
         int x=m_text_x_offset;
         int y=m_text_y_offset+((int)i*line_height);
         //--- Abfragen der Größe der Zeichenkette
         uint string_length=::ArraySize(m_lines[i].m_symbol);
         //--- Zeichnen des Textes
         for(uint s=0; s<string_length; s++)
           {
            uint text_color=TextColorCurrent();
            //--- Gibt es einen ausgewählten Text, bestimme dessen Farbe und die Hintergrundfarbe des aktuellen Buchstaben
            if(CheckSelectedText(i,s))
              {
               //--- Farbe des gewählten Textes
               text_color=::ColorToARGB(m_selected_text_color);
               //--- Berechnung der Koordinaten für das Zeichnen des Hintergrunds
               int x2=x+m_lines[i].m_width[s];
               int y2=y+line_height-1;
               //--- Zeichnen der Hintergrundfarbe des Zeichens
               m_canvas.FillRectangle(x,y,x2,y2,::ColorToARGB(m_selected_back_color));
              }
            //--- Zeichnen des Buchstabens
            m_canvas.TextOut(x,y,m_lines[i].m_symbol[s],text_color,TA_LEFT);
            //--- X-Koordinate des nächsten Buchstabens
            x+=m_lines[i].m_width[s];
           }
        }
     }
//--- Wenn nicht mehrzeilig und keine Zeichen angegeben, wird der Standardtext angezeigt
   else
     {
      //--- Textausgabe, wenn angegeben
      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);
     }
  }

Die Methoden zur Textauswahl wurden beschrieben, und so verhält sich jetzt die fertige Programm:

 Fig. 6. Demonstration einer Textauswahl in einem Textfeld eines MQL-Programms.

Fig. 6. Demonstration einer Textauswahl in einem Textfeld eines MQL-Programms.


Methoden zum Löschen des ausgewählten Textes

Kommen wir nun zu den Methoden zum Löschen des ausgewählten Textes. Hier ist es wichtig zu beachten, dass verschiedene Methoden zum Löschen des Textes verwendet werden, je nachdem, ob nur eine oder mehrere Zeilen ausgewählt wurden. 

Die Methode CTextBox::DeleteTextOnOneLine() wird aufgerufen, wenn nur eine einzelne Zeile gelöscht werden soll. Die Anzahl der zu löschenden Buchstaben wird zu Beginn der Methode festgelegt. Dann, wenn der Anfangsindex des Buchstabens des gewählten Textes rechts liegt, werden die Buchstaben um die Anzahl der zu löschenden Buchstaben nach rechts verschoben, ausgehend von der Anfangsposition. Danach wird der Array der Buchstaben der Zeile um die gleiche Zahl reduziert. 

Falls der Index des Buchstabens am Anfang der Textauswahl links ist, muss auch der Textkursor um die Anzahl der zu löschenden Buchstaben nach rechts verschoben werden.

class CTextBox : public CElement
  {
private:
   //--- Löschen der Textauswahl in der Zeile
   void              DeleteTextOnOneLine(void);
  };
//+------------------------------------------------------------------+
//| Löschen der Textauswahl in der Zeile                             |
//+------------------------------------------------------------------+
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);
//--- Wenn der Anfangsindex des Buchstabens rechts ist
   if(m_selected_symbol_to<m_selected_symbol_from)
     {
      //--- Verschieben der Zeichen in den freien Bereich der aktuellen Zeile
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_from,m_selected_symbol_to);
     }
//--- Wenn der Anfangsindex des Buchstabens links ist
   else
     {
      //--- Verschieben des Textkursors um die Anzahl der zu löschenden Buchstaben nach links
      m_text_cursor_x_pos-=symbols_to_delete;
      //--- Verschieben der Zeichen in den freien Bereich der aktuellen Zeile
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_to,m_selected_symbol_from);
     }
//--- Verringern der Arraygröße der aktuellen Zeile um die Zahl der zu verschiebenden Zeichen
   ArraysResize(m_text_cursor_y_pos,symbols_total-symbols_to_delete);
  }


Die Methode CTextBox::DeleteTextOnMultipleLines() wird zum Löschen mehrerer Zeilen der Textauswahl verwendet. Ihr Algorithmus ist etwas komplizierter. Zuerst muss bestimmt werden:

  • Die Gesamtzahl der Buchstaben der Anfangs- und der Endzeile
  • Die Zahl der Zeilen der Textauswahl (ohne die Anfangs- und die Endzeile)
  • Die Zahl der zu löschenden Buchstaben der Anfangs- und der Endzeile.

Die weiteren Schritten sind unten angeführt. Abhängig von der Richtung der Textauswahl (hoch oder runter), werden der Anfangs- und der Endindex jeweils anderen Methoden übergeben.

  • Die nicht zu löschenden Buchstaben, die von einer Zeile in einer anderen verschoben werden müssen, werden in ein temporäres, dynamisches Array kopiert.
  • Das empfangende Array (Zeile) wird neu dimensioniert.
  • Die Daten werden dem Array mit der Struktur der Zeilen hinzugefügt.
  • Die Zeilen werden um die Anzahl der zu löschenden Zeilen verschoben.
  • Der Array der Zeilen wird neu dimensioniert (verringert um die Anzahl der zu löschenden Zeilen).
  • Falls die Anfangszeile über der Endzeile liegt (die Textauswahl erfolgte abwärts), wird der Textkursor auf die Anfangsindices (Zeile und Buchstabe) der Textauswahl gesetzt.

class CTextBox : public CElement
  {
private:
   //--- Löschen der Textauswahl mit mehreren Zeilen
   void              DeleteTextOnMultipleLines(void);
  };
//+------------------------------------------------------------------+
//| Löschen der Textauswahl mit mehreren Zeilen                      |
//+------------------------------------------------------------------+
void CTextBox::DeleteTextOnMultipleLines(void)
  {
//--- Die Gesamtzahl der Buchstaben der Anfangs- und der Endzeile
   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);
//--- Die zu löschenden Zeilen dazwischen
   uint lines_to_delete =::fabs(m_selected_line_from-m_selected_line_to);
//--- Die zu löschenden Buchstabenzahl auf der ersten und letzten Zeile
   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);
//--- Falls die Anfangszeile unter der Endzeile liegt
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- Kopieren der zu verschiebenen Zeichen in das Array
      string array[];
      CopyWrapSymbols(m_selected_line_from,m_selected_symbol_from,symbols_to_delete_in_line_from,array);
      //--- Neu dimensionieren der Empfängerzeile
      uint new_size=m_selected_symbol_to+symbols_to_delete_in_line_from;
      ArraysResize(m_selected_line_to,new_size);
      //--- Hinzufügen der Daten zum Array der Struktur der Zeilen
      PasteWrapSymbols(m_selected_line_to,m_selected_symbol_to,array);
      //--- Abfrage der Größe des Zeilenarrays
      uint lines_total=::ArraySize(m_lines);
      //--- Verschieben nach oben um die Zahl der zu löschenden Zeilen
      MoveLines(m_selected_line_to+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- Größenanpassung des Zeilenarrays
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
     }
//--- Falls die Anfangszeile über der Endzeile liegt
   else
     {
      //--- Kopieren der zu verschiebenen Zeichen in das Array
      string array[];
      CopyWrapSymbols(m_selected_line_to,m_selected_symbol_to,symbols_to_delete_in_line_to,array);
      //--- Neu dimensionieren der Empfängerzeile
      uint new_size=m_selected_symbol_from+symbols_to_delete_in_line_to;
      ArraysResize(m_selected_line_from,new_size);
      //--- Hinzufügen der Daten zum Array der Struktur der Zeilen
      PasteWrapSymbols(m_selected_line_from,m_selected_symbol_from,array);
      //--- Abfrage der Größe des Zeilenarrays
      uint lines_total=::ArraySize(m_lines);
      //--- Verschieben nach oben um die Zahl der zu löschenden Zeilen
      MoveLines(m_selected_line_from+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- Größenanpassung des Zeilenarrays
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
      //--- Verschieben des Kursors an die Anfangsposition der Auswahl
      SetTextCursor(m_selected_symbol_from,m_selected_line_from);
     }
  }

Welche der Methoden zum Löschen des Textes aufgerufen wird, geschieht in der Hauptmethode — CTextBox::DeleteSelectedText(). Ist die Textauswahl gelöscht, werden die Werte der Anfangs- und Endindizes zurückgesetzt. Danach muss die Größe des Textfeldes neu berechnet werden, da sich die Zeilenzahl geändert haben könnte. Die maximale Breite der Zeilen, die für die Berechnung des Textfeldes herangezogen werden, könnte sich auch geändert haben. Zuletzt sendet die Methode eine Nachricht, dass der Textkursor verschoben wurde. Die Methode gibt ein true zurück, wenn Text ausgewählt und gelöscht wurde. Falls kein Text ausgewählt worden war, als die Methode aufgerufen wurde, gibt sie false zurück. 

class CTextBox : public CElement
  {
private:
   //--- Löschen der Textauswahl
   void              DeleteSelectedText(void);
  };
//+------------------------------------------------------------------+
//| Löschen der Textauswahl                                          |
//+------------------------------------------------------------------+
bool CTextBox::DeleteSelectedText(void)
  {
//--- Verlassen, wenn kein Text ausgewählt wurde
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- Falls nur Buchstaben einer Zeile zu löschen sind
   if(m_selected_line_from==m_selected_line_to)
      DeleteTextOnOneLine();
//--- Falls Buchstaben mehrerer Zeilen zu löschen sind
   else
      DeleteTextOnMultipleLines();
//--- Rücksetzen der Textauswahl
   ResetSelectedText();
//--- Berechne die Größe des Textfeldes
   CalculateTextBoxSize();
//--- Setze die neue Größe des Textfeldes
   ChangeTextBoxSize();
//--- Anpassen der Bildlaufleiste
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Aktualisieren des Textes des Textfeldes
   DrawTextAndCursor(true);
//--- Senden einer Nachricht darüber
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

Die Methode CTextBox::DeleteSelectedText() wird nicht nur aufgerufen, wenn die Taste Backspace gedrückt wurde, sondern auch: (1) wenn ein neuer Buchstabe eingegeben wurde und (2) wenn die Taste Enter gedrückt wurde. In diesem Fall wird zuerst der Text gelöscht und dann die Aktion gemäß der gedrückten Taste ausgeführt.

So sieht es in der fertigen Anwendung aus:

 Fig. 7. Demonstration des Löschens einer Textauswahl.

Fig. 7. Demonstration des Löschens einer Textauswahl.


Klasse für das Arbeiten mit Bilddaten

Als Ergänzung in diesem Artikel betrachten wir eine neue Klasse (CImage) für die Arbeit mit Bilddaten. Sie wird wiederholt in vielen Klassen der Bibliothek verwendet, wenn sie Bilder zeichnen sollen. Die Klasse befindet sich in der Datei Objects.mqh

Die Eigenschaften der Klasse:
  • Array der Pixel des Bildes;
  • Bildbreite;
  • Bildhöhe;
  • Pfad zu Bilddatei.

//+------------------------------------------------------------------+
//| Klasse zum Sichern der Bilddaten                                 |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // Array of the image pixels
   uint              m_image_width;  // Image width
   uint              m_image_height; // Image height
   string            m_bmp_path;     // Path to the image file
public:
   //--- (1) Größe des Datenarrays, (2) Bestimmen/Rückgabe der Daten (Pixelfarbe)
   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;     }
   //--- Bestimmen/Rückgabe der Bildbreite
   void              Width(const uint width)                      { m_image_width=width;               }
   uint              Width(void)                                  { return(m_image_width);             }
   //--- Bestimmen/Rückgabe der Bildhöhe
   void              Height(const uint height)                    { m_image_height=height;             }
   uint              Height(void)                                 { return(m_image_height);            }
   //--- Bestimmen/Rückgabe des Pfads zur Bilddatei
   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)
  {
  }

Die Methode CImage::ReadImageData() soll das Bild mit seinen Eigenschaften sichern. Sie lädt das Bild des gegebenen Pfads und sichert dessen Daten.

class CImage
  {
public:
   //--- Lesen und sichern der Daten des übergebenen Bildes
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| Sichern des übergebenen Bildes in einen Array                    |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- Rücksetzen der Fehlervariablen
   ::ResetLastError();
//--- Sichern des Pfads zur Bilddatei
   m_bmp_file_path=bmp_file_path;
//--- Lesen und Sichern der Daten des Bildes
   if(!::ResourceReadImage(m_bmp_file_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > error: ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

Manchmal könnte es notwendig sein, eine Kopie eines Bildes des gleichen Typs (CImage) zu erstellen. Das erledigt die Methode CImage::CopyImageData(). Zu Beginn der Methode wird das Empfängerarray auf die Größe des Quellarrays gesetzt. Dann werden in einer Schleife die Daten des Quellarrays in das Empfängerarray kopiert.

class CImage
  {
public:
   //--- Kopieren der Daten des übergebenen Bildes
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| Kopieren der Daten des übergebenen Bildes                        |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- Abfragen der Größe des Empfänger- und des Quellarrays
   uint data_total        =DataTotal();
   uint source_data_total =::GetPointer(array_source).DataTotal();
//--- Größenänderung des Empfängerarrays
   ::ArrayResize(m_image_data,source_data_total);
//--- Kopieren der Daten
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=::GetPointer(array_source).Data(i);
  }

Vor dieser Aktualisierung verwendete die Klasse CCanvasTable eine Struktur, um Bilddaten zu sichern. Jetzt nach der Einführung der Klasse CImage wurden die entsprechenden Änderungen vollzogen. 


Schlussfolgerung

Dieser Artikel beschließt die Entwicklung eines mehrzeiligen Texteingabefeldes. Dessen Schlüsselfunktion ist, dass es jetzt keine Beschränkungen bezüglich der Zahl der eingegebenen Buchstaben und Zeilen mehr gibt, ein Manko der standardmäßigen Grafikobjekte des Typs OBJ_EDIT. Im nächsten Artikel werden wir das Thema "Elemente in Tabellenzellen" weiterentwickeln, mit der Möglichkeit, Werte der Tabellenzellen zu ändern, und das mit den Elementen aus diesem Artikel. Darüber hinaus werden mehrere Elemente in einen neuen Modus überführt: Sie werden übergeben und nicht aus mehreren standardmäßigen Grafikobjekt erstellt.

Zur Zeit schaut das Schema der Bibliothek zum Erstellen einer grafische Benutzeroberfläche wie folgt aus:

 Fig. 8. Struktur der Bibliothek im augenblicklichen Entwicklungsstand.

Fig. 8. Struktur der Bibliothek im augenblicklichen Entwicklungsstand.


Unten könne Sie die letzten Versionen der Bibliothek und der Dateien zum Testen herunterladen, wie sie hier in diesem Artikel beschrieben wurden.

Bei Fragen zur Verwendung des bereitgestellten Materials, können Sie auf die detaillierten Beschreibungen im Lauf der Entwicklung der Bibliothek in den vorangegangenen Artikeln dieser Serie zurückgreifen oder Sie stellen Ihre Fragen im Kommentarteil diese Artikels.