Grafische Interfaces X: Algorithmus für den Zeilenumbruch im mehrzeiligen Textfeld (build 12)
Inhalt
- Einführung
- Der Modus für den Zeilenumbruch im mehrzeiligen Textfeld
- Beschreibung des Algorithmus und der Hilfsmethoden
- Beschreibung der Hauptmethode
- Anwendung zum Testen der Kontrollelemente
- Schlussfolgerung
Einführung
Der erste Artikel Grafische Interfaces I: Vorbereitung der Bibliotheksstruktur (Kapitel 1) beschreibt im Detail den Zweck der Bibliothek. Sie finden eine Liste von Artikeln mit Verweisen am Ende jeden Kapitels. Dort können Sie auch die komplette, aktuelle Version der Bibliothek zum derzeitigen Entwicklungsstand herunterladen. Die Dateien müssen im gleichen Verzeichnis wie das Archiv platziert werden.
Dieser Artikel führt die Entwicklung eines mehrzeiligen Textfeldes weiter. Die vorherigen Entwicklungen finden Sie in dem Artikel Grafisches Interface X: mehrzeiliges Textfeld (build 8). Diesmal ist es unsere Aufgabe einen Algorithmus für den Fall, dass der Text die Breite des Textfeldes überschreitet, zu entwickeln oder, umgekehrt, einen Zeilenumbruch zu entfernen, wenn die möglich ist.
Der Modus für den Zeilenumbruch im mehrzeiligen Textfeld
Alle Texteditoren oder Anwendungen, die mit Text arbeiten, verfügen über einen Zeilenumbruch für Texte, die die Breite der Anwendung überschreiten. Dadurch kann das eine oder andere Mal die lästige Bildlaufleiste vermieden werden.
Der Modus für einen Zeilenumbruch ist standardmäßig deaktiviert. Mit der Methode CTextBox::WordWrapMode() wird er aktiviert. Das ist die einzige public Methode der Umsetzung des Zeilenumbruchs. Alle anderen sind private, wir werden sie weiter unten besprechen.
//+------------------------------------------------------------------+ //| Klasse zur Erstellung eines mehrzeiligen Textfeldes | //+------------------------------------------------------------------+ class CTextBox : public CElement { private: //--- Der Modus des Zeilenumbruchs bool m_word_wrap_mode; //--- public: //--- Der Modus des Zeilenumbruchs void WordWrapMode(const bool mode) { m_word_wrap_mode=mode; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CTextBox::CTextBox(void) : m_word_wrap_mode(false)
Für die Konfiguration eines Zeilenumbruchs und dem Hinzufügen von Text in eine Zeile, muss jede Zeile ein Zeilenende-Zeichen an ihrem Ende haben.
Hier ist ein einfaches Beispiel mit einer einzigen Zeile. Öffnen sie irgendeinen Texteditor, bei dem Sie den Zeilenumbruch an/abschalten können, zum Beispiel den Notepad. Fügen Sie einem Dokument diese Zeile hinzu:
Google is an American multinational technology company specializing in Internet-related services and products.
Ist der Modus für den Zeilenumbruch deaktiviert, könnte, abhängig von der Breite des Textfeldes, die Zeile nicht in das Textfeld passen. Dann muss man, um die Zeile lesen zu können, die horizontale Bildlaufleiste verschoben werden.
Fig. 1. Zeilenumbruch ist deaktiviert.
Jetzt aktivieren wir den Zeilenumbruch. Die Zeile passt in in die Breite des Textfeld des Editors:
Fig. 2. Zeilenumbruch ist aktiviert.
Wie wir sehen können, wird die ganze Zeichenkette dreigeteilt und in drei aufeinanderfolgenden Zeilen dargestellt. Jetzt ist das Zeilenende-Zeichen nur in der dritten Zeile. Wird die erste Zeile diese Dokumentes durch das Programm gelesen, wird der ganze Text bis zum Zeilenende-Zeichen zurückgegeben.
Das kann mit einem einfachen Skript überprüft werden:
//+------------------------------------------------------------------+ //| Script Programm Start Funktion | //+------------------------------------------------------------------+ void OnStart(void) { //--- Abruf eines Dateihandles int file=::FileOpen("Topic 'Word wrapping'.txt",FILE_READ|FILE_TXT|FILE_ANSI); //--- Lesen der Datei nach Erhalt des Handles if(file!=INVALID_HANDLE) ::Print(__FUNCTION__," > ",::FileReadString(file)); else ::Print(__FUNCTION__," > error: ",::GetLastError()); } //+------------------------------------------------------------------+
Ergebnis des Lesens der ersten Zeile (in unserem Fall ist es die einzige) und der Ausdruck ins Log:
OnStart > Google is an American multinational technology company specializing in Internet-related services and products.
Um die Information in dieser Weise aus dem mehrzeiligen Textfeld auszulesen, ergänzen wir eine weitere bool-Eigenschaft für das Schreiben des Zeilenende-Zeichens in der Struktur StringOptions (früher KeySymbolOptions) in der Klasse CTextBox.
//--- Zeichen und ihre Eigenschaften struct StringOptions { string m_symbol[]; // Zeichen int m_width[]; // Breite der Zeichenkette bool m_end_of_line; // Zeilenende-Zeichen }; StringOptions m_lines[];
Für den Zeilenumbruch werden mehrere Haupt- und Hilfsmethoden benötigt. Zählen wir ihre Aufgaben auf.
Die Hauptmethoden:
- Zeilenumbruch
- Rückgabe des Index des ersten, sichtbaren Buchstabens und Leerzeichens rechts davon
- Rückgabe der Anzahl der zu verschiebenden Zeichen
- Umbrechen des Textes in die nächste Zeile
- Übertragen von Text der nächsten Zeile in die aktuelle
Hilfsmethoden:
- Rückgabe der Anzahl von Wörtern in der angegebenen Zeile
- Rückgabe des Index des Leerzeichens über seine Nummer
- Verschieben der Zeile
- Verschieben der Zeichen der angegebenen Zeile
- Kopieren der Zeichen des übergebenen Arrays für die nächste Zeile
- Einfügen der Zeichen des übergebenen Arrays in die angegebene Zeile
Betrachten wir die Struktur der Hilfsmethoden etwas genauer.
Beschreibung des Algorithmus und der Hilfsmethoden
Der Algorithmus für den Zeilenumbruch erkennt den Augenblick, die Schleife zu starten, die den Index des Leerzeichens über dessen Nummer findet. Diese Schleife benötigt die Anzahl der Wörter in der Zeile. Unten ist der Code der Methode CTextBox::WordsTotal(), die diese Aufgabe ausführt.
Das Zählen von Worten ist einfach. Es muss über den Zeichenarray der angegebenen Zeile iteriert werden, um Muster aus einem Leerzeichen (' ') und einem nachfolgenden Nichtleerzeichen zu entdecken. Das kennzeichnet ein neues Wort. Der Zähler wird auch erhöht, wenn das Zeilenende erreicht wurde, damit das letzte Wort nicht ausgelassen wird.
class CTextBox : public CElement { private: //--- Rückgabe der Anzahl der Wörter in der angegebenen Zeile uint WordsTotal(const uint line_index); }; //+------------------------------------------------------------------+ //| Rückgabe der Anzahl der Wörter in der angegebenen Zeile | //+------------------------------------------------------------------+ uint CTextBox::WordsTotal(const uint line_index) { //--- Abfrage der Größe des Zeilenarrays uint lines_total=::ArraySize(m_lines); //--- Verhinderung des Überschreitens der Arraygröße uint l=(line_index<lines_total)? line_index : lines_total-1; //--- Abfrage der Größe des Zeichenarrays der angegebenen Zeile uint symbols_total=::ArraySize(m_lines[l].m_symbol); //--- Zähler der Wörter uint words_counter=0; //--- Suchen nach dem Leerzeichen am angegebenen Index for(uint s=1; s<symbols_total; s++) { //--- Zählen, wenn (1) Zeilenende erreicht oder (2) ein Leerzeichen gefunden (Wortende) wurde if(s+1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE)) words_counter++; } //--- Rückgabe der Anzahl der Worte return(words_counter); }
Die Methode CTextBox::SymbolIndexBySpaceNumber() wird zur Bestimmung des Index des Leerzeichens verwendet. Wurde dessen Wert erhalten, kann die Breite von einem oder mehreren Worten berechnet werden, beginnend mit dem Teil der Zeichenkette der Methode CTextBox::LineWidth().
Zur Verdeutlichung betrachten wir ein Beispiel mit einer Textzeile. Man sieht die Indices der Buchstaben (blau), die der Teilstringe (grün) und der Leerzeichen (rot). So hat zum Beispiel das erste Leerzeichen (0) der ersten Zeile den Index 6 der Zeichenkette.
Fig. 3. Die Indices der Buchstaben (blau), der Teilstringe (grün) und der Leerzeichen (rot).
Unten ist der Code der Methode CTextBox::SymbolIndexBySpaceNumber(). Hier ist alles einfach: Iterieren über alle Zeichen des angegebenen Teilstrings in einer Schleife, die Erhöhung der Zähler jedes Mal, wenn ein neues Leerzeichen gefunden wird. Ergibt sich in einer Iteration, dass der Zähler gleich dem im zweiten Argument übergebenen Index des Leerzeichens ist, wird der Indexwert gesichert und die Schleife beendet. Dieser Wert wird von der Methode zurückgegeben.
class CTextBox : public CElement { private: //--- Rückgabe des Indexwertes des Leerzeichen uint SymbolIndexBySpaceNumber(const uint line_index,const uint space_index); }; //+------------------------------------------------------------------+ //| Rückgabe des Indexwertes des Leerzeichen | //+------------------------------------------------------------------+ uint CTextBox::SymbolIndexBySpaceNumber(const uint line_index,const uint space_index) { //--- Abfrage der Größe des Zeilenarrays uint lines_total=::ArraySize(m_lines); //--- Verhinderung des Überschreitens der Arraygröße uint l=(line_index<lines_total)? line_index : lines_total-1; //--- Abfrage der Größe des Zeichenarrays der angegebenen Zeile uint symbols_total=::ArraySize(m_lines[l].m_symbol); //--- (1) For determining the space character index and (2) counter of spaces uint symbol_index =0; uint space_counter =0; //--- Suchen nach dem Leerzeichen am angegebenen Index for(uint s=1; s<symbols_total; s++) { //--- Wenn ein Leerzeichen gefunden wurde if(m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE) { //--- Wenn der Zähler gleich dem angegebenen Index ist, Wert sichern und die Schleife beenden if(space_counter==space_index) { symbol_index=s; break; } //--- Erhöhen des Zählers der Leerzeichen space_counter++; } } //--- Rückgabe der Zeilenlänge, wenn kein Leerzeichen gefunden werden konnte return((symbol_index<1)? symbols_total : symbol_index); }
Betrachten wir den Algorithmus für den Zeilenumbruch in Zusammenhang mit dem Verschieben der Elemente der Zeile und des Zeichenarrays. Illustrieren wir das in verschiedenen Situationen. Nehmen wir zum Beispiel diese Zeile:
The quick brown fox jumped over the lazy dog.
Diese Zeile ist zu breit für das Textfeld. Das Textfeld wird in Fig. 4 durch das rote Rechteck angezeigt. Offensichtlich muss der "überschüssige" Teil der Zeile — 'over the lazy dog.' — in die nächste Zeile verschoben werden.
Fig. 4. Eine zulange Zeile im Textfeld.
Da der dynamische Array der Zeile aktuell nur ein Element hat, muss die Größe des Arrays um Eins erhöht werden. Die Größe des Zeichenarrays der neuen Zeile wird durch die Zeichenzahl des verschobenen Textes bestimmt. Danach wird der nicht passende Teil der Zeile verschoben. Das Endergebnis:
Fig. 5. Ein Teil der Zeile wurde in die neue, nächste Zeile verschoben.
Kommen wir jetzt zum Fall, wenn das Textfeld sich um 30% verkleinert, wie geht der Algorithmus damit um. Er bestimmt als erstes den Teil der ersten Zeile (Index 0), der die Grenze des Textfeldes überschreitet. In dem Fall passt der Teil 'fox jumped' nicht hinein. Dann wird zunächst die Größe des dynamische Arrays der Zeilen um Eins erhöht. Als nächstes werden alle Teilstringe unterhalb um eine Zeile nach unten verschoben, um so Platz für den zu schiebenden Text zu erhalten. Danach wird der Teilstring 'fox jumped' in den freien Platz verschoben, wie das oben beschrieben bereits beschrieben wurde. Diesen Schritt zeigt das Bild unten.
Fig. 6. Verschieben des Textes in die zweite Zeile (Index 1).
Der Algorithmus wechselt zur nächsten Zeile (Index 1) bei nächsten Schleifendurchlauf. Jetzt muss aber wieder überprüft werden, ob ein Teil des Textes dieser Zeile zu breit ist für das Textfeld. Wenn sich zeigt, dass das nicht der Fall ist, muss überprüft werden, ob noch Platz am Ende dieser Zeile ist, um Textteile der nächsten Zeile mit dem Index 2 dort einzufügen. Das prüft die Bedingung für einen 'umgekehrten' Zeilenumbruch, um Textteile vom Anfang der nächsten Zeile (Index 2) an das Ende der aktuellen Zeile (Index 1) zu kopieren.
Zusätzlich zu dieser Bedingung muss auch überprüft werden, ob die aktuelle Zeile durch das Zeilenende-Zeichen beendet wird. Falls ja wird der 'umgekehrte' Zeilenumbruch nicht durchgeführt. Im Beispiel gibt es einerseits kein Zeilenende-Zeichen und andererseits genug Platz für einen 'umgekehrten' Zeilenumbruch, das Wort — 'over'. Während eines 'umgekehrten' Zeilenumbruchs wird die Größe des Zeichenarrays um die Anzahl der hinzugefügten oder weggenommenen Zeichen in der aktuellen und der nächsten Zeile geändert. Während eines 'umgekehrten' Zeilenumbruchs werden vor der Änderung der Größe des Zeichenarrays die verbleibenden Zeichen an den Anfang der Zeile verschoben. Das Bild unten zeigt diesen Schritt.
Fig. 7. Ein 'umgekehrter' Zeilenumbruch von der zweiten (Index 1) und dritten Zeile (Index 2).
Es ist zu erwarten, dass, wenn das Textfeld verengt wird, sowohl normale wie 'umgekehrte' Zeilenumbrüche durchgeführt werden müssen. Andererseits, wenn das Textfeld verbreitet wird, wird nur der 'umgekehrte' Zeilenumbruch angewendet. Jedes Mal, wenn Text in die nächste Zeile umgebrochen wird, wird die Größe des dynamische Arrays um Eins erhöht. Und jedes Mal, wenn der ganze verbleibende Text einer Zeile durch einen 'umgekehrten' Zeilenumbruch ihrem Vorgänger angehängt wird, wir der Zeilenarray um Eins erniedrigt. Davor müssen allerdings, falls es weitere Zeilen gibt, diese um eine nach oben verschoben werden, um leere Zeilen zu vermeiden, die bei einem 'umgekehrten' Zeilenumbruch entstehen könnten.
Alle diese Schritte der Zeilenneuordnung, Zeilenumbruchs und des 'umgekehrten' Zeilenumbruchs werden innerhalb der Schleife nicht angezeigt: Das Bild unten zeigt, was der Nutzer sieht, wenn er so mit dem grafischen Interface arbeitet:
Fig. 8. Veranschaulichung des Algorithmus für den Zeilenumbruch am Beispiel eines Texteditors.
Aber das ist nicht alles. Im Falle von nur einem Wort (stetige Reihe von Zeichen) in der Zeile, wird zwischen einzelnen Buchstaben getrennt. Diese Situation wird im Bild unten gezeigt:
Fig. 9. Veranschaulichung der Buchstabentrennung, wenn keine einzelnen Wörter gefunden wurden.
Kommen wir jetzt zu den Methoden zum Verschieben von Zeilen und Zeichen. Die Methode CTextBox::MoveLines() verschiebt die Zeilen. Ihr werden die Indices der Zeilen übergeben, ab der und bis zu der alle Zeilen um eine Position verschoben werden müssen. Der dritte Parameter kennzeichnet die Richtung der Verschiebung. Standardmäßig wird nach unten verschoben.
Ursprünglich wurde der Algorithmus zum Verschieben von Zeilen im Textfeld nur zur Behandlung eines Tastendrucks von 'Enter' und 'Backspace' verwendet. Jetzt wird derselbe Code in mehreren Methoden der Klasse CTextBox verwendet, es bietet sich daher dafür eine eigene Methode an, die wiederholt verwendet werden kann.
Der Code der Methode CTextBox::MoveLines():
class CTextBox : public CElement { private: //--- Verschieben der Zeilen void MoveLines(const uint from_index,const uint to_index,const bool to_down=true); }; //+------------------------------------------------------------------+ //| Verschieben der Zeilen | //+------------------------------------------------------------------+ void CTextBox::MoveLines(const uint from_index,const uint to_index,const bool to_down=true) { //--- Verschieben nach unten if(to_down) { for(uint i=from_index; i>to_index; i--) { //--- Index des vorherigen Elementes des Zeilenarrays uint prev_index=i-1; //--- Abfrage der Größe des Arrays mit den Zeichen uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol); //--- Größenänderung des Arrays ArraysResize(i,symbols_total); //--- Erstelle eine Kopie der Zeile LineCopy(i,prev_index); //--- Wenn dies die letzte Iteration ist if(prev_index==to_index) { //--- Verlassen, wenn es die erste Zeile ist if(to_index<1) break; } } } //--- Verschieben nach oben else { for(uint i=from_index; i<to_index; i++) { //--- Index des nächsten Elementes des Zeilenarrays uint next_index=i+1; //--- Abfrage der Größe des Arrays mit den Zeichen uint symbols_total=::ArraySize(m_lines[next_index].m_symbol); //--- Größenänderung des Arrays ArraysResize(i,symbols_total); //--- Erstelle eine Kopie der Zeile LineCopy(i,next_index); } } }
Die Methode CTextBox::MoveSymbols() wurde implementiert, um innerhalb der Zeile Zeichen zu verschieben. Sie wird nicht nur von den neuen Methoden aufgerufen, sondern auch für das Hinzufügen/Entfernen von Zeichen über die Tastatur in den Methoden CTextBox::AddSymbol() und CTextBox::DeleteSymbol(), die hier bereits besprochen wurden. Die Parameter sind: (1) Index der Zeile innerhalb der die Zeichen bewegt werden; (2) Beginn- und End-Index der zu bewegenden Zeichen; (3) Richtung der Verschiebung (standardmäßig nach links).
class CTextBox : public CElement { private: //--- Zeichenverschiebung in der angegebenen Zeile void MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true); }; //+------------------------------------------------------------------+ //| Zeichenverschiebung in der angegebenen Zeile | //+------------------------------------------------------------------+ void CTextBox::MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true) { //--- Abfrage der Größe des Arrays mit den Zeichen uint symbols_total=::ArraySize(m_lines[line_index].m_symbol); //--- Differenz uint offset=from_pos-to_pos; //--- Wenn die Zeichen nach links verschoben werden müssen 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]; } } //--- Wenn die Zeichen nach rechts verschoben werden müssen 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]; } } }
Der Code der Hilfsmethoden zum Kopieren und Einfügen von Zeichen (die Methoden CTextBox::CopyWrapSymbols() und CTextBox::PasteWrapSymbols()) werden hier auch öfters verwendet. Zum Kopieren wird der Methode CTextBox::CopyWrapSymbols() ein leerer dynamischer Array übergeben. Weiters werden die Zeile, das erste Zeichen und ihre zu kopierende Anzahl angegeben. Um die Zeichen wieder einzufügen, muss der Methode CTextBox::PasteWrapSymbols() das Array mit den vorher kopierten Zeichen übergeben werden, zusammen mit dem Index der Zeile und der des Zeichens, für den Beginn des Einfügens.
class CTextBox : public CElement { private: //--- Kopiert die Zeichen in das übergebene Array zum Verschieben in die nächste Zeile void CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[]); //--- Einfügen der Zeichen aus dem übergebenen Array in die angegebene Zeile void PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[]); }; //+------------------------------------------------------------------+ //| Kopiert die Zeichen in das übergebene Array zum Verschieben | //+------------------------------------------------------------------+ void CTextBox::CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[]) { //--- Festlegen der Arraygröße ::ArrayResize(array,symbols_total); //--- Kopieren der zu verschiebenen Zeichen in das Array for(uint i=0; i<symbols_total; i++) array[i]=m_lines[line_index].m_symbol[start_pos+i]; } //+------------------------------------------------------------------+ //| Einfügen der Zeichen in die angegebene Zeile | //+------------------------------------------------------------------+ void CTextBox::PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[]) { uint array_size=::ArraySize(array); //--- Hinzufügen der Daten zu den des Arrays der Struktur der neuen Zeile 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]); } }
Betrachten wir jetzt die Hauptmethode des Algorithmus für den Zeilenumbruch.
Beschreibung der Hauptmethode
Zu Beginn überprüft der Algorithmus in einer Schleife jede Zeile, ob es einen Überlauf gibt. Die Methode CTextBox::CheckForOverflow() wurde dazu implementiert. Sie liefert drei Werte, zwei werden in Variablen gesichert, die als Referenz der Methode übergeben werden.
Zu Beginn der Methode muss die Breite der aktuellen Zeile, bestimmt über ihren Index, es ist das erste Argument, ermittelt werden. Die Zeilenbreite wird unter Berücksichtigung des Abstandes von der linken Kante des Textfeldes und der Breite der vertikalen Bildlaufleiste errechnet. Passt die Zeile in das Textfeld, gibt die Methode den Wert false zurück, was so viel wie "kein Überlauf" bedeutet. Ist die Zeile aber zu breit, muss der Index des ersten sichtbaren Zeichens und Leerzeichens auf der rechten Seite des Textfeldes bestimmt werden. Dafür wird in einer Schleife über alle Zeichen vom Ende her überprüft, ob vom Anfang der Zeile bis zu diesem Zeichen die Zeile in das Textfeld passen würde. Passt sie, wird der Index des Zeichen gesichert. Zusätzlich wird bei jeder Iteration überprüft, ob das aktuelle Zeichen ein Leerzeichen ist. Trifft das zu, wird dieser Index gesichert und die Suche beendet.
Nach all dem Prüfen und Suchen liefert die Methode ein 'true' zurück, wenn zumindest einer der Indices bestimmt werden konnte. Das zeigt an, diese Zeile passt nicht. Die Indices des Zeichens und des Leerzeichens werden später so verwendet: Wurde der Index eines Zeichens, aber nicht der eines Leerzeichens gefunden, heißt das, die Zeile hat gar keine Leerzeichen und es muss einfach ein Teil der Zeichen verschoben werden. Wurde ein Leerzeichen gefunden, muss der Teil der Zeile ab diesem Index verschoben werden.
class CTextBox : public CElement { private: //--- Rückgabe der Indices des ersten sichtbaren Zeichen und des Leerzeichens bool CheckForOverflow(const uint line_index,int &symbol_index,int &space_index); }; //+------------------------------------------------------------------+ //| Prüfen auf Überlauf | //+------------------------------------------------------------------+ bool CTextBox::CheckForOverflow(const uint line_index,int &symbol_index,int &space_index) { //--- Abfrage der Größe des Arrays mit den Zeichen uint symbols_total=::ArraySize(m_lines[line_index].m_symbol); //--- Abstände uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth(); //--- Abrufen der Gesamtbreite der Zeile uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus; //--- Wenn die Zeile in das Textfeld passt if(full_line_width<(uint)m_area_visible_x_size) return(false); //--- Ermitteln der Indices Zeichen des Überlaufs for(uint s=symbols_total-1; s>0; s--) { //--- Ermitteln (1) Breite des Teilstrings vom Anfang bis zum aktuellen Zeichen und (2) des Zeichens uint line_width =LineWidth(s,line_index)+x_offset_plus; string symbol =m_lines[line_index].m_symbol[s]; //--- Wenn das sichtbare Zeichen noch nicht gefunden wurde if(symbol_index==WRONG_VALUE) { //--- Sichern des Index des Zeichens, wenn der Teilstring in das Textfeld passt if(line_width<(uint)m_area_visible_x_size) symbol_index=(int)s; //--- Gehe zum nächsten Zeichen continue; } //--- Wenn es ein Leerzeichen ist, Sichern dessen Index und Schleife beenden if(symbol==SPACE) { space_index=(int)s; break; } } //--- Ist diese Bedingung erfüllt, dann passt die Zeile nicht bool is_overflow=(symbol_index!=WRONG_VALUE || space_index!=WRONG_VALUE); //--- Rückgabe des Ergebnisses return(is_overflow); }
Passt die Zeile und liefert die Methode CTextBox::CheckForOverflow() ein false, dann muss überprüft werden, ob eine 'umgekehrter' Zeilenumbruch durchgeführt werden kann. Die Methode CTextBox::WrapSymbolsTotal() ermittelt die Anzahl der Zeichen die umgebrochen werden müssen.
Diese Methode sichert die Anzahl der umzubrechenden Zeichen in der als Referenz übergebenen Variablen, so wie auch, ob das der ganze verbleibende Test ist oder nur ein Teil. Die Werte der lokalen Variablen werden zu Beginn der Methode berechnet, zum Beispiel die folgende Parameter:
- Die Anzahl der Zeichen in der aktuellen Zeile
- Die ganze Breite der Zeile
- Die Größe des leeren Teils
- Die Anzahl der Wörter in der nächsten Zeile
- Die Anzahl der Zeichen in der nächsten Zeile
Danach wird in einer Schleife ermittelt, wie viele Wörter von der nächsten Zeile in die aktuelle verschoben werden können. In jeder Iteration wird, nach dem Erhalt des Breite des Teilstrings bis zum angegebenen Leerzeichen, überprüft, ob der Teilstring in den freien Platz der aktuellen Zeile passt.
Passt er, wird der des Zeichens gesichert und überprüft, ob ein weiteres Wort eingefügt werden könnte. Zeigt diese Überprüfung, dass das Textende erreicht wurde, wird das in einer eigenen Variablen gesichert und die Schleife beendet.
Passt der Teilstring nicht, muss auch geprüft werden, ob es das letzte Zeichen der Zeile ist, um ihn zu markieren, dass er eine ununterbrochenen Zeichenfolge ist, und die Schleife zu beenden.
Dann, wenn die nächste Zeile Leerzeichen enthält oder keinen freien Platz hat, gibt die Methode sofort das Ergebnis zurück. Falls diese Prüfungen bestanden wurden, muss im Weiteren ermittelt werden, ob ein Teil eines Wortes aus der nächsten Zeile, in die aktuallen verschoben werden kann. Der 'umgekehrte' Zeilenumbruch mit dem Teil eines Wortes wird nur durchgeführt, wenn diese Zeile nicht in den freien Platz der aktuellen Zeile passt und gleichzeitig die letzten Zeichen der aktuellen und der nächsten Zeile keine Leerzeichen sind. In diesem Fall Falls diese Prüfung bestanden wurde, wird in der nächsten Schleife die Zahl der zu verschiebenen Zeichen ermittelt.
class CTextBox : public CElement { private: //--- Rückgabe der Anzahl der umzubrechenden Zeichen bool WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total); }; //+------------------------------------------------------------------+ //| Rückgabe der Zahl mit ihrer Bedeutung der umzubrechenden Zeichen | //+------------------------------------------------------------------+ bool CTextBox::WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total) { //--- Kennzeichnung von (1) der Zahl der umzubrechenden Zeichen und (2) einer Zeile ohne Leerzeichen bool is_all_text=false,is_solid_row=false; //--- Abfrage der Größe des Arrays mit den Zeichen uint symbols_total=::ArraySize(m_lines[line_index].m_symbol); //--- Abstände uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth(); //--- Abrufen der Gesamtbreite der Zeile uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus; //--- Ermitteln der Breite der leere Bereichs der Zeile uint free_space=m_area_visible_x_size-full_line_width; //--- Ermitteln der Anzahl der Wörter in der nächsten Zeile uint next_line_index =line_index+1; uint words_total =WordsTotal(next_line_index); //--- Abfrage der Größe des Arrays mit den Zeichen uint next_line_symbols_total=::ArraySize(m_lines[next_line_index].m_symbol); //--- Ermitteln der der Anzahl der Wörter, die aus nächsten Zeile verschoben werden sollen (Leerzeichensuche) for(uint w=0; w<words_total; w++) { //--- Ermitteln (1) des Index des Leerzeichens und (2) der Breite des Teilstrings vom Anfang bis zum Leerzeichen uint ss_index =SymbolIndexBySpaceNumber(next_line_index,w); uint substring_width =LineWidth(ss_index,next_line_index); //--- Wenn der Teilstring in den leeren Bereich der aktuellen Zeile passt if(substring_width<free_space) { //--- ...prüfen, ob ein weiteres Wort verschoben werden kann wrap_symbols_total=ss_index; //--- Stopp, wenn es die ganze Zeile ist if(next_line_symbols_total==wrap_symbols_total) { is_all_text=true; break; } } else { //--- Wenn es eine Zeile ohne Leerzeichen ist if(ss_index==next_line_symbols_total) is_solid_row=true; //--- break; } } //--- Sofortige Rückgabe des Ergebnisses, wenn (1) diese Zeile Leerzeichen enthält oder (2) es keinen leeren Bereich gibt if(!is_solid_row || free_space<1) return(is_all_text); //--- Ermitteln der Gesamtbreite der nächsten Zeile full_line_width=LineWidth(next_line_symbols_total,next_line_index)+x_offset_plus; //--- Wenn (1) die Zeile nicht passt und es keinen leeren Bereich am Ende der (2) aktuellen und (3) den vorherigen Zeilen gibt 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) { //--- Ermitteln der Zeichenzahl, die aus der nächsten Zeile verschoben werden müssen for(uint s=next_line_symbols_total-1; s>=0; s--) { //--- Ermitteln der Breite des Teilstrings vom Anfang bis zum angegebenen Zeichen uint substring_width=LineWidth(s,next_line_index); //--- Wenn der Teilstring nicht in den freien Platz passt, gehe zum nächsten Zeichen if(substring_width>=free_space) continue; //--- Passt der Teilstring, sichere den Wert und fertig wrap_symbols_total=s; break; } } //--- Rückgabe von true, wenn der ganze Text verschoben werden muss return(is_all_text); }
Passt die Zeile nicht, wird der Text von der aktuellen Zeile in die nächste Zeile mit der Methode CTextBox::WrapTextToNewLine() verschoben. Sie wird auf zwei Arten verwendet: (1) automatischer und (2) erzwungener Wortumbruch: zum Beispiel durch das Drücken der Taste 'Enter'. Standardmäßig ist der automatische Wortumbruch durch den dritten Parameter gesetzt. Die ersten beiden Parameter der Methode sind der (1) Index der Zeile mit dem zu verschiebenden Text und (2) der Index des Zeichens, ab dem der Text in die nächste (neue) Zeile zu verschieben ist.
Die Zahl der zu verschiebenden Zeichen wird zu Beginn der Methode ermittelt. Dann wird (1) die benötigte Zeichenzahl der aktuellen Zeile in ein lokales dynamisches Array kopiert, und (2) die Arraygröße der aktuellen und der nächsten Zeilen gesetzt, und (3) die kopierten Zeichen dem Zeichenarray der nächsten Zeile hinzugefügt. Danach muss der Ort des Textkursor bestimmt werden, wenn er sich während der Eingabe über die Tastatur innerhalb der umzubrechenden Zeichen befindet.
Als Letztes prüft und setzt die Methode das Zeilenende-Zeichen der aktuellen und der nächsten Zeile, da die Ergebnisse aus den unterschiedlichen Situationen gleich sein müssten.
1. Wurde CTextBox::WrapTextToNewLine() nach dem Drücken von 'Enter' aufgerufen, dann wird, falls die aktuelle Zeile eine Zeilenende-Zeichen hat, das Zeilenende-Zeichen auch in die nächsten Zeile eingetragen. Hat die aktuelle Zeile kein Zeilenende-Zeichen, dann muss dieses gesetzt werden und das der nächsten Zeile entfernt werden.
Der Code dieser Methode:
class CTextBox : public CElement { private: //--- Umbrechen des Textes in die nächste Zeile void WrapTextToNewLine(const uint curr_line_index,const uint symbol_index,const bool by_pressed_enter=false); }; //+------------------------------------------------------------------+ //| Umbrechen des Textes in eine neue Zeile | //+------------------------------------------------------------------+ void CTextBox::WrapTextToNewLine(const uint line_index,const uint symbol_index,const bool by_pressed_enter=false) { //--- Abfrage der Größe des Arrays der Zeichen der Zeile uint symbols_total=::ArraySize(m_lines[line_index].m_symbol); //--- Der Indes des letzten Zeichens uint last_symbol_index=symbols_total-1; //--- Anpassen im Falle einer leeren Zeile uint check_symbol_index=(symbol_index>last_symbol_index && symbol_index!=symbols_total)? last_symbol_index : symbol_index; //--- Index der nächsten Zeile uint next_line_index=line_index+1; //--- Die Zahl der in die neue Zeile zu verschiebenden Zeichen uint new_line_size=symbols_total-check_symbol_index; //--- Kopieren der zu verschiebenen Zeichen in das Array string array[]; CopyWrapSymbols(line_index,check_symbol_index,new_line_size,array); //--- Größenanpassung des Arrays der Struktur der Zeile ArraysResize(line_index,symbols_total-new_line_size); //--- Größenänderung der Arrays der Struktur der neuen Zeile ArraysResize(next_line_index,new_line_size); //--- Hinzufügen der Daten zu den des Arrays der Struktur der neuen Zeile PasteWrapSymbols(next_line_index,0,array); //--- Bestimmen der neuen Position des Textkursors 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; //--- Falls der Aufruf durch die Entertaste ausgelöst wurde if(by_pressed_enter) { //--- Falls die Zeile ein Zeilenende-Zeichen hat, dann wird eines der aktuellen und der nächsten Zeile hinzugefügt 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; } //--- Falls nicht, nur der aktuellen Zeile else { m_lines[line_index].m_end_of_line =true; m_lines[next_line_index].m_end_of_line =false; } } else { //--- Wenn die Zeile ein Zeilenende-Zeichen hat, gehe weiter und füge es der nächsten Zeile hinzu 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; } //--- Wenn die Zeile kein Zeilenende-Zeichen hat, dann weiter mit beiden Zeilen else { m_lines[line_index].m_end_of_line =false; m_lines[next_line_index].m_end_of_line =false; } } }
Die Methode CTextBox::WrapTextToPrevLine() vollzieht den 'umgekehrten' Zeilenumbruch. Ihr wird der Index der nächsten Zeile übergeben und die Anzahl der Teichen, die in dier aktuelle Zeile verschoben werden können. Der dritte Parameter zeigt an, ob der ganze verbleibende Text oder nur ein Teil verschoben werden muss. Der Umbruch nur eines Teiles des Textes ist standardmäßig (false) eingestellt.
Zu Beginn der Methode wird die angegebene Zahl von Zeichen der nächsten Zeile in einen lokalen, dynamischen Array kopiert. Dann wird der Array der Zeichen der aktuellen Zeile um ide Zahl der zu ergänzenden Zeichen erhöht werden. Danach werden (1) die Zeichen, die vorher bereits kopiert wurden, dem vergrößerten Teil des Zeichenarrays der aktuellen Zeile hinzugefügt; (2) die verbleibenden Zeichen der nächsten Zeile werden an den Anfang des Arrays verschoben; (3) der Zeichenarray der nächsten Zeile wird um die Zahl der verschobenen Zeichen reduziert.
Später muss noch die Position des Textkursors angepasst werden. Wenn die in dem Teil ist, der in die vorhergehende Zeile verschoben wurde, muss auch der Textkursor verschoben werden.
Ganz zum Schluss, wenn der ganze verbleibende Text umgebrochen wurde, muss (1) das Zeilenende-Zeichen der aktuellen Zeile hinzugefügt werden, (2) alle Zeilen darunter um Eins nach oben verschoben und (3) der Zeilenarray um Eins erniedrigt werden.
class CTextBox : public CElement { private: //--- Umbrechen eines Textes einer angegebenen Zeile in die vorherige Zeile void WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false); }; //+------------------------------------------------------------------+ //| Umbrechen eines Textes von der nächsten Zeile in die aktuelle | //+------------------------------------------------------------------+ void CTextBox::WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false) { //--- Abfrage der Größe des Arrays der Zeichen der Zeile uint symbols_total=::ArraySize(m_lines[next_line_index].m_symbol); //--- Index der vorherigen Zeile uint prev_line_index=next_line_index-1; //--- Kopieren der zu verschiebenen Zeichen in das Array string array[]; CopyWrapSymbols(next_line_index,0,wrap_symbols_total,array); //--- Ermitteln der Größe des Zeichenarrays der vorherigen Zeile uint prev_line_symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol); //--- Erhöhen der Größe des Arrays der vorherigen Zeile um die Zahl der zu ergänzenden Zeichen uint new_prev_line_size=prev_line_symbols_total+wrap_symbols_total; ArraysResize(prev_line_index,new_prev_line_size); //--- Hinzufügen der Daten zu den des Arrays der Struktur der neuen Zeile PasteWrapSymbols(prev_line_index,new_prev_line_size-wrap_symbols_total,array); //--- Verschieben der Zeichen in den freien Bereich der aktuellen Zeile MoveSymbols(next_line_index,wrap_symbols_total,0); //--- Verringern der Arraygröße der aktuellen Zeile um die Zahl der zu verschiebenden Zeichen ArraysResize(next_line_index,symbols_total-wrap_symbols_total); //--- Anpassen des Textkursors 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--; } //--- Verlassen, wenn es nicht der ganze, verbleibende Text der Zeile ist if(!is_all_text) return; //--- Hinzufügen des Zeilenende-Zeichens zur vorherigen Zeile, falls die aktuelle eines hat if(m_lines[next_line_index].m_end_of_line) m_lines[next_line_index-1].m_end_of_line=true; //--- Abfrage der Größe des Zeilenarrays uint lines_total=::ArraySize(m_lines); //--- Verschieben der Zeilen um Eins nach oben MoveLines(next_line_index,lines_total-1,false); //--- Größenanpassung des Zeilenarrays ::ArrayResize(m_lines,lines_total-1); }
Jetzt ist schlussendlich Zeit sich der letzten und wichtigsten Methode zuzuwenden — CTextBox::WordWrap(). Damit ein Wortumbruch funktioniert, muss diese Methode in der Methode CTextBox::ChangeTextBoxSize() platziert werden.
Zu Beginn der Methode CTextBox::WordWrap() wird überprüft, ob es ein mehrzeiliges Textfeld ist und, ob der Wortumbruch aktiviert ist. Ist eines der beiden deaktiviert, wird die Methode verlassen. Sind beide aktiviert, dann muss über alle Zeilen iteriert werden, um den Algorithmus des Wortumbruchs zu aktivieren. Hier wird in jeder Iteration die Methode CTextBox::CheckForOverflow() verwendet, um einen Überlauf von Zeilen im Textfeld zu erkennen.
- Passt die Zeile nicht, dann wird geschaut, ob das nächste Leerzeichen zur rechten Kante des Textfeldes gefunden wurde. Der Teil der aktuellen Zeile beginnend mit diesem Leerzeichen wird in die nächste Zeile verschoben. Das Leerzeichen selbst wird nicht verschoben; daher wird der Index des Leerzeichens erhöht. Dann wird das Zeilenarray um Eins erhöht und die unteren Zeilen um Eins nach unten verschoben. Der Index des zu verschiebenen Teils der Zeile wird ein weiteres Mal verifiziert. Danach wird der Text umgebrochen.
- Passt die Zeile, dann wird überprüft, ob ein 'umgekehrten' Zeilenumbruch durchgeführt werden sollte. Das Zeilenende-Zeichen der aktuellen Zeile wird zu Beginn dieses Bereiches überprüft. Existiert es, geht das Programm zur nächsten Iteration. Wurde die Prüfung bestanden, wird die zu verschiebende Zeichenzahl ermittelt, um danach den Text in die vorherige Zeile umzubrechen.
//+------------------------------------------------------------------+ //| Klasse zur Erstellung eines mehrzeiligen Textfeldes | //+------------------------------------------------------------------+ class CTextBox : public CElement { private: //--- Wortumbruch void WordWrap(void); }; //+------------------------------------------------------------------+ //| Wortumbruch | //+------------------------------------------------------------------+ void CTextBox::WordWrap(void) { //--- Verlassen, wenn (1) das mehrzeilige Textfeld und (2) der Wortumbruch nicht aktiv sind if(!m_multi_line_mode || !m_word_wrap_mode) return; //--- Abfrage der Größe des Zeilenarrays uint lines_total=::ArraySize(m_lines); //--- Prüfen, ob der Text in das Textfeld eingepasst werden muss for(uint i=0; i<lines_total; i++) { //--- Bestimmen des ersten sichtbaren (1) Zeichens und (2) Leerzeichens int symbol_index =WRONG_VALUE; int space_index =WRONG_VALUE; //--- Index der nächsten Zeile uint next_line_index=i+1; //--- Passt die Zeile nicht, dann wird ein Teil der aktuellen Zeile in die neue Zeile umgebrochen if(CheckForOverflow(i,symbol_index,space_index)) { //--- Wurde ein Leerzeichen gefunden, wird es nicht verschoben if(space_index!=WRONG_VALUE) space_index++; //--- Erhöhen des Zeilenarrays um Eins ::ArrayResize(m_lines,++lines_total); //--- Verschieben der Zeilen ab der aktuellen um eine Position MoveLines(lines_total-1,next_line_index); //--- Prüfen des Index des Zeichens, ab dem der Text verschoben werden soll int check_index=(space_index==WRONG_VALUE && symbol_index!=WRONG_VALUE)? symbol_index : space_index; //--- Umbrechen des Textes in die neue Zeile WrapTextToNewLine(i,check_index); } //--- Passt die Zeile, prüfe ob ein 'umgekehrter' Zeilenumbruch durchgeführt werden sollte else { //--- Überspringen, wenn (1) diese Zeile hat ein Zeilenende-Zeichen oder (2) dies ist die letzte Zeile if(m_lines[i].m_end_of_line || next_line_index>=lines_total) continue; //--- Ermitteln der Anzahl der umzubrechenden Zeichen uint wrap_symbols_total=0; //--- Wenn der verbleibende Text der nächsten Zeile in die aktuelle Zeile umzubrechen ist if(WrapSymbolsTotal(i,wrap_symbols_total)) { WrapTextToPrevLine(next_line_index,wrap_symbols_total,true); //--- Aktualisieren der Arraygröße zur weiteren Verwendung in der Schleife lines_total=::ArraySize(m_lines); //--- Schritt zurück, um das Überspringen einer Zeile für den nächsten Prüfung zu vermeiden i--; } //--- Umbrechen nur, wenn es passt else WrapTextToPrevLine(next_line_index,wrap_symbols_total); } } }
Alle Methode für den automatischen Wortumbruch wurden besprochen. Schauen wir nun, wie alles funktioniert.
Anwendung zum Testen der Kontrollelemente
Erstellen wir eine MQL-Anwendung zum Testen. Wir verwenden die vorhandene Version eines mehrzeiligen Textfeldes aus den vorigen Artikel, der wir den einzeiligen Modus im grafischen Interface der Anwendung abstellen. Sonst bleibt alles gleich. Und so funktioniert alles im Terminal des Metatrader 5:
Fig. 10. Demonstration des Wortumbruchs in einem mehrzeiligen Texteingabefelde
Die Testanwendung dieses Artikels kann mittel des Links unten heruntergeladen werden, für ein weiteres Studium.
Schlussfolgerung
Zur Zeit schaut das Schema der Bibliothek zum Erstellen einer grafische Benutzeroberfläche wie folgt aus:
Fig. 11. Struktur der Bibliothek im augenblicklichen Entwicklungszustand.
Unten könne Sie die letzten Versionen der Bibliothek und der Dateien zum Testen herunterladen.
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.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/3173
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.