Русский 中文 Español Deutsch 日本語 Português
Graphical Interfaces X: Word wrapping algorithm in the Multiline Text box (build 12)

Graphical Interfaces X: Word wrapping algorithm in the Multiline Text box (build 12)

MetaTrader 5Examples | 10 May 2017, 16:24
9 495 1
Anatoli Kazharski
Anatoli Kazharski

Contents

Introduction

The first article Graphical Interfaces I: Preparation of the Library Structure (Chapter 1) considers in detail what this library is for. You will find a list of articles with links at the end of each chapter. There, you can also download a complete version of the library at the current stage of development. The files must be located in the same directories as in the archive.

This article continues the development of the Multiline Text box control. The earlier progress can be seen in the article Graphical Interfaces X: The Multiline Text box control (build 8). The task this time is to implement an automatic word wrapping in case a text box width overflow occurs, or a reverse word wrapping of the text to the previous line if the opportunity arises.


Word wrap mode in a multiline text box

All text editors or applications feature word wrapping for working with text information in case the text overflows the width of the application area. This delivers from the nuisance of having to use the horizontal scrollbar all the time. 

The word wrapping mode will be disabled by default. To activate this mode, use the CTextBox::WordWrapMode() method. This is the only public method in the implementation of word wrapping. All others will be private, their arrangement will be discussed in details below.

//+------------------------------------------------------------------+
//| Class for creating a multiline text box                          |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- The Word wrap mode
   bool m_word_wrap_mode;
   //---
public:
   //--- The Word wrap mode
   void WordWrapMode(const bool mode) { m_word_wrap_mode=mode; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_word_wrap_mode(false)

In order to configure the word wrapping and addition of text to a line, it is necessary for each line to have a sign of its end.

Here is a simple example with a single line. Open any text editor, where the word wrapping mode can be enabled/disabled — for example, Notepad. Add one line to a document:

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

If the word wrap mode is disabled, depending on the width of the text box, the line might not fit the text box. Then, to read the line, the horizontal scrollbar will have to be used.

 Fig. 1. Word wrap mode is disabled.

Fig. 1. Word wrap mode is disabled.


Now, enable the word wrap mode. The line should fit the width of the text box of the editor:

 Fig. 2. Word wrap mode is enabled.


Fig. 2. Word wrap mode is enabled.


It can be seen that the initial string was split into three substrings, which were consecutively lined up one after another. Here, the end sign is present only in the third substring. Reading the first line of this file programmatically returns the entire text until the end sign. 

This can be checked using a simple script:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- Get the file handle
   int file=::FileOpen("Topic 'Word wrapping'.txt",FILE_READ|FILE_TXT|FILE_ANSI);
//--- Read the file if the handle was obtained
   if(file!=INVALID_HANDLE)
      ::Print(__FUNCTION__," > ",::FileReadString(file));
   else
      ::Print(__FUNCTION__," > error: ",::GetLastError());
  }
//+------------------------------------------------------------------+

Result of reading the first line (in this case, the only one) and printing to the log:

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

In order to implement such reading of information from the developed multiline text box, add another bool property for storing the end of line sign to the StringOptions structure (formerly KeySymbolOptions) in the CTextBox class.

   //--- Characters and their properties
   struct StringOptions
     {
      string            m_symbol[];    // Characters
      int               m_width[];     // Width of the characters
      bool              m_end_of_line; // End of line sign
     };
   StringOptions  m_lines[];

Several main and auxiliary methods will be required to implement the word wrapping. Let us enumerate their tasks.

Main methods:

  • Word wrapping
  • Returning the indexes of the first visible character and space on the right
  • Returning the number of characters to be moved
  • Wrapping text to the next line
  • Wrapping text from the next line to the current line

Auxiliary methods:

  • Returning the number of words in the specified line
  • Returning the index of the space character by its number
  • Moving lines
  • Moving characters in the specified line
  • Copying characters to the passed array for moving to the next line
  • Pasting characters from the passed array to the specified line

Let us take a closer look at the structure of the auxiliary methods.


Description of the algorithm and auxiliary methods

The word wrap algorithm has a moment when it is necessary to start a cycle for finding the index of a space character by its number. To arrange such a cycle, a method is needed for determining the number of words in a line. Below is the code of the CTextBox::WordsTotal() method, which performs this task.

Counting words is quite simple. It is necessary to iterate over the array of characters of the specified line, tracking the appearance of the pattern, where the current character is not a space character (' '), while the previous one is. This will indicate a beginning of a new word. The counter also increases if the end of line is reached, so that the last word is not skipped.

class CTextBox : public CElement
  {
private:
   //--- Returns the number of words in the specified line
   uint              WordsTotal(const uint line_index);
  };
//+------------------------------------------------------------------+
//| Returns the number of words in the specified line                |
//+------------------------------------------------------------------+
uint CTextBox::WordsTotal(const uint line_index)
  {
//--- Get the size of the lines array
   uint lines_total=::ArraySize(m_lines);
//--- Prevention of exceeding the array size
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Get the size of the array of characters for the specified line
   uint symbols_total=::ArraySize(m_lines[l].m_symbol);
//--- Word counter
   uint words_counter=0;
//--- Search for a space at the specified index
   for(uint s=1; s<symbols_total; s++)
     {
      //--- Count, if (1) reached the end of line or (2) found a space (end of word)
      if(s+1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE))
         words_counter++;
     }
//--- Return the number of words
   return(words_counter);
  }

The CTextBox::SymbolIndexBySpaceNumber() method will be used for determining the index of the space character. Once this value is obtained, it is possible to calculate the width of one or more words starting from the beginning of a substring by using the CTextBox::LineWidth() method. 

For clarity, consider an example with one line of text. Its characters (blue), substrings (green) and spaces (red) have been indexed. For example, it can be seen that the first (0) space on the first (0) line has a character index 6.

 Fig. 3. Indexes of characters (blue), substrings (green) and spaces (red).

Fig. 3. Indexes of characters (blue), substrings (green) and spaces (red).


Below is the code of the CTextBox::SymbolIndexBySpaceNumber() method. Here, everything is simple: iterate over all characters of the specified substring in a loop, increasing the counter every time a new space character is found. If any iteration shows that counter is equal to the space index specified in the passed value of the second argument, the value of the character index value is stored and the cycle is stopped. This is the value returned by the method.

class CTextBox : public CElement
  {
private:
   //--- Returns the space character index by its number 
   uint              SymbolIndexBySpaceNumber(const uint line_index,const uint space_index);
  };
//+------------------------------------------------------------------+
//| Returns the space character index by its number                  |
//+------------------------------------------------------------------+
uint CTextBox::SymbolIndexBySpaceNumber(const uint line_index,const uint space_index)
  {
//--- Get the size of the lines array
   uint lines_total=::ArraySize(m_lines);
//--- Prevention of exceeding the array size
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Get the size of the array of characters for the specified line
   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;
//--- Search for a space at the specified index
   for(uint s=1; s<symbols_total; s++)
     {
      //--- If found a space
      if(m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE)
        {
         //--- If the counter is equal to the specified space index, store it and stop the cycle
         if(space_counter==space_index)
           {
            symbol_index=s;
            break;
           }
         //--- Increase the counter of spaces
         space_counter++;
        }
     }
//--- Return the line size if space index was not found
   return((symbol_index<1)? symbols_total : symbol_index);
  }

Let us consider the part of the word wrapping algorithm related to moving the elements of the line and character arrays. Let us illustrate this in different situations. For example, there is a line:

The quick brown fox jumped over the lazy dog.

This line does not fit the width of the text box. The area of this text box is shown in figure 4 by a red rectangle. It is evident that the "excessive" part of the line — 'over the lazy dog.' — needs to be moved to the next line.

 Fig. 4. Situation with overflowing the line of the text box.

Fig. 4. Situation with overflowing the line of the text box.

As the dynamic array of lines currently consists of a single element, the array needs to be increased by one element. The array of characters in the new line must be set according to the number of characters of the moved text. After this, the part of the line that does not fit should be moved. The final result:

 Fig. 5. A part of the line was moved to the next new line.

Fig. 5. A part of the line was moved to the next new line.

Now let us see how the algorithm will work if the width of the text box decreases by approximately 30%. Here, it also first determines which part of the first (index 0) line exceeds the boundaries of the text box. In this case, the 'fox jumped' substring did not fit. Then the dynamic array of lines is increased by one element. Next, all substrings located below are shifted down by one line, thus freeing a slot for the moved text. After that, the 'fox jumped' substring is moved to the freed slot, as it was described in the previous passage. This step is shown the figure below.

 Fig. 6. Moving text to the second (index 1) line.

Fig. 6. Moving text to the second (index 1) line.


The algorithm goes to the next line (index 1) at the next iteration of the cycle. Here, it is necessary to check if a part of this line exceeds the boundaries of the text box again. If the check shows that it does not exceed, it is necessary to see if this line has enough room on the right to accommodate a part of the next line with the index 2. This checks the conditions for the reverse word wrapping of the text from the beginning of the next line (index 2) to the end of the current (index 1).

In addition to this condition, it is necessary to check if the current line contains an end of line sign. If it does, then the reverse word wrapping is not performed. In the example, there is no end of line sign and there is enough room for reverse wrapping one word — 'over'. During a reverse word wrap, the size of array of characters is changed by the number of added and extracted characters on the current and the next lines, respectively. During a reverse word wrap, before changing the size of the array of characters, the remaining characters are moved to the beginning of the line. The figure below demonstrates this step. 

 Fig. 7. Reverse word wrapping to the second (index 1) line from the third (index 2) line.

Fig. 7. Reverse word wrapping to the second (index 1) line from the third (index 2) line.


It can be seen that when the text box area becomes narrower, the direct and reverse word wrapping will be performed. On the other hand, when the text box extends, reverse word wrapping to the freed space is sufficient. Every time the text is wrapped to the next line, the dynamic array is increased by one element. And every time all the remaining text of the next line is reverse wrapped, the array of lines is decreased by one element. But before that, in case there are more lines ahead, they must be shifted up by one line, in order to eliminate formation of an empty line when the remaining text is reverse wrapped. 

All these steps with realignment of line, direct and reverse word wrapping will not be seen in the course of the cycle: The figure below shows a crude example of what users will see when working with the graphical interface:

 Fig. 8. Demonstration of the word wrapping algorithm by the example of a text editor.

Fig. 8. Demonstration of the word wrapping algorithm by the example of a text editor.


And that is not all. In case only one word (continuous sequence of characters) is left on a line, hyphenation is performed character-by-character. This situation is shown in the figure below:

 Fig. 9. Demonstration of a character-wise wrapping when a word cannot be fit.


Fig. 9. Demonstration of a character-wise wrapping when a word cannot be fit.

Now consider the methods for moving lines and characters. The CTextBox::MoveLines() method will be used for moving lines. The method is passed the indexes of lines, from which and up to which the lines need to be shifted by one position. The third parameter is the shifting direction. It is set to shifting downwards by default. 

Previously, the line shifting algorithm has been used nonrecurrently when controlling the text box using the 'Enter' and 'Backspace' keys. Now, the same code is used in multiple methods of the CTextBox class, therefore it would be reasonable to implement a separate method for repeated use.

The code of the CTextBox::MoveLines() method:

class CTextBox : public CElement
  {
private:
   //--- Moves the lines
   void              MoveLines(const uint from_index,const uint to_index,const bool to_down=true);
  };
//+------------------------------------------------------------------+
//| Moving lines                                                     |
//+------------------------------------------------------------------+
void CTextBox::MoveLines(const uint from_index,const uint to_index,const bool to_down=true)
  {
//--- Shifting lines downwards
   if(to_down)
     {
      for(uint i=from_index; i>to_index; i--)
        {
         //--- Index of the previous element of the lines array
         uint prev_index=i-1;
         //--- Get the size of the array of characters
         uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol);
         //--- Resize the arrays
         ArraysResize(i,symbols_total);
         //--- make a copy of the line
         LineCopy(i,prev_index);
         //--- If this is the last iteration
         if(prev_index==to_index)
           {
            //--- Leave, if this is the first line
            if(to_index<1)
               break;
           }
        }
     }
//--- Shifting lines upwards
   else
     {
      for(uint i=from_index; i<to_index; i++)
        {
         //--- Index of the next element of the lines array
         uint next_index=i+1;
         //--- Get the size of the array of characters
         uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
         //--- Resize the arrays
         ArraysResize(i,symbols_total);
         //--- make a copy of the line
         LineCopy(i,next_index);
        }
     }
  }

The CTextBox::MoveSymbols() method has been implemented for moving characters in a line. It is called not only in the new methods related to the word wrapping mode, but also when adding/removing characters using the keyboard in the CTextBox::AddSymbol() and CTextBox::DeleteSymbol() methods considered earlier. The input parameters set here are: (1) index of the line where the characters are to be moved; (2) start and end character indexes for moving; (3) direction of moving (moving left is set by default).

class CTextBox : public CElement
  {
private:
   //--- Moving characters in the specified line
   void              MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true);
  };
//+------------------------------------------------------------------+
//| Moving characters in the specified line                          |
//+------------------------------------------------------------------+
void CTextBox::MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true)
  {
//--- Get the size of the array of characters
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Difference
   uint offset=from_pos-to_pos;
//--- If the characters are to be moved to the left
   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];
        }
     }
//--- If the characters are to be moved to the right
   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];
        }
     }
  }

The code of the auxiliary methods for copying and pasting characters (the CTextBox::CopyWrapSymbols() and CTextBox::PasteWrapSymbols() methods) will also be frequently used here. When copying, the CTextBox::CopyWrapSymbols() method is passed an empty dynamic array. It is also indicated the line and the starting character for copying the specified number of characters. To paste the characters, the CTextBox::PasteWrapSymbols() method must be passed an array with the previously copied characters, at the same time indicating the index of the line and character, where the insertion will be done.

class CTextBox : public CElement
  {
private:
   //--- Copies the characters to the passed array for moving to the next line
   void              CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[]);
   //--- Pastes characters from the passed array to the specified line
   void              PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[]);
  };
//+------------------------------------------------------------------+
//| Copies the characters to the passed array for moving             |
//+------------------------------------------------------------------+
void CTextBox::CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[])
  {
//--- Set the array size
   ::ArrayResize(array,symbols_total);
//--- Copy the characters to be moved into the array
   for(uint i=0; i<symbols_total; i++)
      array[i]=m_lines[line_index].m_symbol[start_pos+i];
  }
//+------------------------------------------------------------------+
//| Pastes the characters to the specified line                      |
//+------------------------------------------------------------------+
void CTextBox::PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[])
  {
   uint array_size=::ArraySize(array);
//--- Add the data to the arrays of the structure for the new line
   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]);
     }
  }

Now, let us consider the main methods of the word wrapping algorithm.


Description of the main methods

When the algorithm starts its operation, it checks overflowing on each line in a cycle. The CTextBox::CheckForOverflow() method has been implemented for such checks. This method returns three values, two of which are stored in variables passed to the method as reference parameters. 

At the beginning of the method, it is necessary to obtain the width of the current line, the index of which is passed to the method as the first parameter. The line width is checked with consideration of the indent from the left edge of the text box and the width of the vertical scrollbar. If the line width fits the text box, the method returns false, which means "no overflow". If the line does not fit, then it is necessary to determine the indexes of the first visible character and space in the right side of the text box. To do this, loop through the line characters starting from the end, and check if the line fits the text box width from the beginning till that character. If the line fits, the index of the character is stored. In addition, every iteration checks if the current character is a space. If so, its index is stored and the search is complete.

After all these checks and searching, the method returns true if it finds at least one of the sought indexes. That would indicate that the line does not fit. The indexes of the character and space will later be used as follows: if a character index is found while a space index is not, that means the line does not contain spaces and it is necessary to move a part of the characters of this line. If a space is found, then it is necessary to move a part of the line starting from the index of this space character.

class CTextBox : public CElement
  {
private:
   //--- Returns the indexes of the first visible character and space
   bool              CheckForOverflow(const uint line_index,int &symbol_index,int &space_index);
  };
//+------------------------------------------------------------------+
//| Checking for line overflow                                       |
//+------------------------------------------------------------------+
bool CTextBox::CheckForOverflow(const uint line_index,int &symbol_index,int &space_index)
  {
//--- Get the size of the array of characters
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Indents
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Get the full width of the line
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- If the width of the line fits the text box
   if(full_line_width<(uint)m_area_visible_x_size)
      return(false);
//--- Determine the indexes of the overflowing characters
   for(uint s=symbols_total-1; s>0; s--)
     {
      //--- Get the (1) width of the substring from the beginning to the current character and (2) the character
      uint   line_width =LineWidth(s,line_index)+x_offset_plus;
      string symbol     =m_lines[line_index].m_symbol[s];
      //--- If a visible character has not been found yet
      if(symbol_index==WRONG_VALUE)
        {
         //--- If the substring width fits the text box area, store the character index
         if(line_width<(uint)m_area_visible_x_size)
            symbol_index=(int)s;
         //--- Go to the next character
         continue;
        }
      //--- If this is a space, store its index and stop the cycle
      if(symbol==SPACE)
        {
         space_index=(int)s;
         break;
        }
     }
//--- If this condition is met, then it indicates that the line does not fit
   bool is_overflow=(symbol_index!=WRONG_VALUE || space_index!=WRONG_VALUE);
//--- Return the result
   return(is_overflow);
  }

If the line fits and the CTextBox::CheckForOverflow() method returns false, then it is necessary to check if a reverse word wrapping can be done. The method for determining the number of characters to be wrapped is CTextBox::WrapSymbolsTotal(). 

This method returns the number of characters to be wrapped into the reference variable, as well as the sign of whether it is all the remaining text or only a part of it. The values for the local variables are calculated at the beginning of the method, for example, the following parameters:

  • The number of characters in the current line
  • The full width of the line
  • Width of free space
  • The number of words in the next line
  • The number of characters in the next line

After that, a cycle determines how many words can be moved from the next line to the current one. In each iteration, after getting the width of a substring till the specified space, check if the substring fits the free area on the current line.

If it fits, store the index of the character and check if another word can be inserted here. If the check shows that the text has ended, then this is marked in a dedicated local variable and the cycle is stopped. 

If the substring does not fit, then it is also necessary to check if it is the last character in the line, placing a mark that it is a continuous string without spaces, and to stop the cycle.

Then, if the next line contains spaces or does not have free space, the method immediately returns the result. In case this check is passed, it is further determined if a part of a word from the next line can be moved to the current line. Reverse wrapping of part of a word is performed only if this line does not fit the free space on the current line, and at the same time, the last characters of the current and the next lines are not spaces. In case these checks are passed, the next cycle determines the number of characters to be moved.

class CTextBox : public CElement
  {
private:
   //--- Returns the number of wrapped characters
   bool              WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total);
  };
//+------------------------------------------------------------------+
//| Returns the number of wrapped characters with the volume signs   |
//+------------------------------------------------------------------+
bool CTextBox::WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total)
  {
//--- Signs of (1) the number of characters to be wrapped and (2) line without spaces
   bool is_all_text=false,is_solid_row=false;
//--- Get the size of the array of characters
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Indents
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Get the full width of the line
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- Get the width of the free space
   uint free_space=m_area_visible_x_size-full_line_width;
//--- Get the number of words in the next line
   uint next_line_index =line_index+1;
   uint words_total     =WordsTotal(next_line_index);
//--- Get the size of the array of characters
   uint next_line_symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Determine the number of words to be moved from the next line (search by spaces)
   for(uint w=0; w<words_total; w++)
     {
      //--- Get the (1) space index and (2) width if the substring from the beginning till the space
      uint ss_index        =SymbolIndexBySpaceNumber(next_line_index,w);
      uint substring_width =LineWidth(ss_index,next_line_index);
      //--- If the substring fits the free space of the current line
      if(substring_width<free_space)
        {
         //--- ...check if another word can be inserted
         wrap_symbols_total=ss_index;
         //--- Stop if is the whole line
         if(next_line_symbols_total==wrap_symbols_total)
           {
            is_all_text=true;
            break;
           }
        }
      else
        {
         //--- If this is a continuous line without spaces
         if(ss_index==next_line_symbols_total)
            is_solid_row=true;
         //---
         break;
        }
     }
//--- Return the result immediately, if (1) this is a line with a space character or (2) there is no free space
   if(!is_solid_row || free_space<1)
      return(is_all_text);
//--- Get the full width of the next line
   full_line_width=LineWidth(next_line_symbols_total,next_line_index)+x_offset_plus;
//--- If (1) the line does not fit and there are no spaces at the end of the (2) current and (3) previous lines
   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)
     {
      //--- Determine the number of characters to be moved from the next line
      for(uint s=next_line_symbols_total-1; s>=0; s--)
        {
         //--- Get the width of the substring from the beginning till the specified character
         uint substring_width=LineWidth(s,next_line_index);
         //--- If the substring does not fit the free space of the specified container, go to the next character
         if(substring_width>=free_space)
            continue;
         //--- If the substring fits, store the value and stop
         wrap_symbols_total=s;
         break;
        }
     }
//--- Return true, if it is necessary to move the entire text
   return(is_all_text);
  }

If the line does not fit, the text will be moved from the current line to the next line using the CTextBox::WrapTextToNewLine() method. It will be used in two modes: (1) automatic word wrap and (2) forced: for instance, when pressing the 'Enter' key. By default, the automatic word wrap mode is set as the third parameter. The first two parameters of the method are the (1) index of the line to move the text from and (2) index of the character, starting from which the text is to be moved to the next (new) line. 

The number of characters to be moved for wrapping is determined at the beginning of the method. Then, (1) the required number of characters of the current line is copied to the local dynamic array, (2) array sizes of the current and the next lines are set, and (3) the copied characters are added to the array of characters of the next line. After that it is necessary to determine the location of the text cursor, if it was among the wrapped characters while entering text from the keyboard.

The last operation in this method is checking and correctly setting the end signs for the current and the next lines, as the results obtained in different situations are supposed to be unique.

1. If the CTextBox::WrapTextToNewLine() was called after pressing the 'Enter' key, then in case the current line has an end of line sign, the end of line sign is also added to the next line. If the current line does not have the end of line sign, then it must be set in the current line and removed from the next line.  

2. When the method is called in automatic mode, then in case the current line has the end of line sign, it must be removed from the current line and set in the next line. If the current line has no end sign, then the absence of the sign must be set to both lines. 

Code of the method:

class CTextBox : public CElement
  {
private:
   //--- Wrapping the text to the next line
   void              WrapTextToNewLine(const uint curr_line_index,const uint symbol_index,const bool by_pressed_enter=false);
  };
//+------------------------------------------------------------------+
//| Wrapping the text to a new line                                  |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToNewLine(const uint line_index,const uint symbol_index,const bool by_pressed_enter=false)
  {
//--- Get the size of the array of characters in the line
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- The last character index
   uint last_symbol_index=symbols_total-1;
//--- Adjustment in case of an empty line
   uint check_symbol_index=(symbol_index>last_symbol_index && symbol_index!=symbols_total)? last_symbol_index : symbol_index;
//--- Index of the next row
   uint next_line_index=line_index+1;
//--- The number of characters to be moved to the new line
   uint new_line_size=symbols_total-check_symbol_index;
//--- Copy the characters to be moved into the array
   string array[];
   CopyWrapSymbols(line_index,check_symbol_index,new_line_size,array);
//--- Resize the arrays of the structure for the line
   ArraysResize(line_index,symbols_total-new_line_size);
//--- Resize the arrays of the structure for the new line
   ArraysResize(next_line_index,new_line_size);
//--- Add the data to the arrays of the structure for the new line
   PasteWrapSymbols(next_line_index,0,array);
//--- Determine the new location of the text cursor
   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;
//--- If indicated that the call was initiated by pressing Enter
   if(by_pressed_enter)
     {
      //--- If the line had an end sign, then set the end sign to the current and to the next lines
      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;
        }
      //--- If not, then only to the current
      else
        {
         m_lines[line_index].m_end_of_line      =true;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
   else
     {
      //--- If the line had an end sign, then continue and set the sign to the next line
      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;
        }
      //--- If the line did not have the end sign, then continue in both lines
      else
        {
         m_lines[line_index].m_end_of_line      =false;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
  }

The CTextBox::WrapTextToPrevLine() method is designed for the reverse word wrapping. It is passed the index of the next line and the number of characters to be moved to the current line. The third parameter indicates if the entire remaining text or only its part is to be moved. Wrapping a part of the text (false) is set by default. 

In the beginning of the method, the specified number of characters of the next line is copied to the local dynamic array. Then, the array of the current line characters must be increased by the added number of characters. After this, (1) the characters copied earlier are added to the new elements of the array of characters of the current line; (2) the remaining characters of the next line are moved to the beginning of the array; (3) the array of the next line characters is decreased by the number of extracted characters. 

Later on, the location of the text cursor must be adjusted. If it was located in the same part of the word that was wrapped to the previous line, then it must also be moved along with that part.

At the very end, in case all the remaining text is wrapped, it is necessary (1) to add the end sign to the current line, (2) shift all lower lines by one position up and (3) decrease the array of lines by one element.

class CTextBox : public CElement
  {
private:
   //--- Wrapping text from the specified line to the previous line
   void              WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false);
  };
//+------------------------------------------------------------------+
//| Wrapping text from the next line to the current line             |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false)
  {
//--- Get the size of the array of characters in the line
   uint symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Index of the previous row
   uint prev_line_index=next_line_index-1;
//--- Copy the characters to be moved into the array
   string array[];
   CopyWrapSymbols(next_line_index,0,wrap_symbols_total,array);
//--- Get the size of the array of characters in the previous line
   uint prev_line_symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
//--- Increase the array size of the previous line by the number of added characters
   uint new_prev_line_size=prev_line_symbols_total+wrap_symbols_total;
   ArraysResize(prev_line_index,new_prev_line_size);
//--- Add the data to the arrays of the structure for the new line
   PasteWrapSymbols(prev_line_index,new_prev_line_size-wrap_symbols_total,array);
//--- Shift the characters to the freed area in the current line
   MoveSymbols(next_line_index,wrap_symbols_total,0);
//--- Decrease the array size of the current line by the number of extracted characters
   ArraysResize(next_line_index,symbols_total-wrap_symbols_total);
//--- Adjust the text cursor
   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--;
     }
//--- Leave, if this is not all the remaining text of the line
   if(!is_all_text)
      return;
//--- Add the end sign to the previous line, if the current line has it
   if(m_lines[next_line_index].m_end_of_line)
      m_lines[next_line_index-1].m_end_of_line=true;
//--- Get the size of the lines array
   uint lines_total=::ArraySize(m_lines);
//--- Shift lines up by one
   MoveLines(next_line_index,lines_total-1,false);
//--- Resize the lines array
   ::ArrayResize(m_lines,lines_total-1);
  }

It is finally the time to consider the last and the most important method — CTextBox::WordWrap(). For the word wrap to be operational, a call to this method must be placed in the CTextBox::ChangeTextBoxSize() method. 

At the beginning of the CTextBox::WordWrap() method, there is a check if multiline text box and word wrapping modes are enabled. If one of the modes is disabled, the program leaves the method. If the modes are enabled, then it is necessary to iterate over all lines in order to activate the text wrapping algorithm. Here, each iteration uses the CTextBox::CheckForOverflow() method to check if a line overflows the text box width. 

  1. If the line does not fit, then see if a space character nearest to the right edge of the text box was found. A part of the current line starting from this space character will be moved to the next line. The space character is not moved to the next line; therefore, the space index is incremented. Then, the lines array is increased by one element, and the lower lines are shifted down by one position. The index for moving the part of the line is verified once more. After that, the text is wrapped. 
  2. If the line fits, then check if a reverse word wrapping should be done. An end sign of the current line is checked at the beginning of this block. If it is present, the program goes to the next iteration. If the check is passed, the number of characters to be moved is determined, after which the text is wrapped to the previous line.
//+------------------------------------------------------------------+
//| Class for creating a multiline text box                          |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Word wrapping
   void              WordWrap(void);
  };
//+------------------------------------------------------------------+
//| Word wrapping                                                    |
//+------------------------------------------------------------------+
void CTextBox::WordWrap(void)
  {
//--- Leave, if the (1) multiline text box and (2) word wrapping modes are disabled
   if(!m_multi_line_mode || !m_word_wrap_mode)
      return;
//--- Get the size of the lines array
   uint lines_total=::ArraySize(m_lines);
//--- Check if it is necessary to adjust the text to the text box width
   for(uint i=0; i<lines_total; i++)
     {
      //--- To determine the first visible (1) character and (2) space
      int symbol_index =WRONG_VALUE;
      int space_index  =WRONG_VALUE;
      //--- Index of the next row
      uint next_line_index=i+1;
      //--- If the line does not fit, then wrap a part of the current line to the new line
      if(CheckForOverflow(i,symbol_index,space_index))
        {
         //--- If a space character is found, it will not be wrapped
         if(space_index!=WRONG_VALUE)
            space_index++;
         //--- Increase the lines array by one element
         ::ArrayResize(m_lines,++lines_total);
         //--- Shift the lines down starting from the current position by one item
         MoveLines(lines_total-1,next_line_index);
         //--- Check the index of the character, from which the text will be moved
         int check_index=(space_index==WRONG_VALUE && symbol_index!=WRONG_VALUE)? symbol_index : space_index;
         //--- Wrap the text to the new line
         WrapTextToNewLine(i,check_index);
        }
      //--- If the line fits, then check if a reverse word wrapping should be done
      else
        {
         //--- Skip, if (1) this line has the end of line sign or (2) this is the last line
         if(m_lines[i].m_end_of_line || next_line_index>=lines_total)
            continue;
         //--- Determine the number of characters to be wrapped
         uint wrap_symbols_total=0;
         //--- If it is necessary to wrap the remaining text of the next line to the current line
         if(WrapSymbolsTotal(i,wrap_symbols_total))
           {
            WrapTextToPrevLine(next_line_index,wrap_symbols_total,true);
            //--- Update the array size for further use in the cycle
            lines_total=::ArraySize(m_lines);
            //--- Step back in order to avoid skipping a line for the next check
            i--;
           }
         //--- Wrap only what fits
         else
            WrapTextToPrevLine(next_line_index,wrap_symbols_total);
        }
     }
  }

All the methods for the automatic word wrapping have been considered. Now, let us see how all this works.


Application for testing the controls

Let us create an MQL application for tests. We will take the existing version from the previous article on Multiline Text box, with the single-line text box removed from the graphical interface of the application. Everything else remains the same. This is how everything works on the chart in the MetaTrader 5 terminal:

Fig. 10. Demonstration of word wrap in the Multiline Text box control 

Fig. 10. Demonstration of word wrap in the Multiline Text box control


The test application featured in the article can be downloaded using the below link for further studying.


Conclusion

Currently, the general schematic of the library for creating graphical interfaces looks as shown below:

 Fig. 11. Structure of the library at the current stage of development.


Fig. 11. Structure of the library at the current stage of development.


You can download the latest version of the library and files for testing below.

If you have questions on using the material presented in those files, you can refer to the detailed description of the library development in one of the articles of this series or ask your question in the comments of this article. 


Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/3173

Attached files |
Last comments | Go to discussion (1)
Dmitry Zhakov
Dmitry Zhakov | 17 Jun 2024 at 00:15
If I add new lines programmatically, then I call UpdateTextBox(), but the scrollbars and wordwrapping is not getting applied. I need to press some key. How to trigger this without pressing any key?
Ready-made Expert Advisors from the MQL5 Wizard work in MetaTrader 4 Ready-made Expert Advisors from the MQL5 Wizard work in MetaTrader 4
The article offers a simple emulator of the MetaTrader 5 trading environment for MetaTrader 4. The emulator implements migration and adjustment of trade classes of the Standard Library. As a result, Expert Advisors generated in the MetaTrader 5 Wizard can be compiled and executed in MetaTrader 4 without changes.
Universal Trend with the Graphical Interface Universal Trend with the Graphical Interface
In this article a universal trend indicator is created based on a number of standard indicators. An additionally created graphical interface allows selecting the type of indicator and adjusting its parameter. The indicator is displayed in a separate window with rows of colored icons.
MQL5 Cookbook - Pivot trading signals MQL5 Cookbook - Pivot trading signals
The article describes the development and implementation of a class for sending signals based on pivots — reversal levels. This class is used to form a strategy applying the Standard Library. Improving the pivot strategy by adding filters is considered.
Patterns available when trading currency baskets. Part II Patterns available when trading currency baskets. Part II
We continue our discussion of the patterns traders can come across while trading currency baskets. In this part, we will consider the patterns formed when using combined trend indicators. Indicators based on a currency index are to be used as the analytical tool.