English 中文 Español Deutsch 日本語 Português
Графические интерфейсы X: Алгоритм переноса слов в многострочном поле ввода (build 12)

Графические интерфейсы X: Алгоритм переноса слов в многострочном поле ввода (build 12)

MetaTrader 5Примеры | 5 апреля 2017, 13:26
4 919 19
Anatoli Kazharski
Anatoli Kazharski

Содержание

Введение

О том, для чего предназначена эта библиотека, более подробно можно прочитать в самой первой статье: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце статей каждой части представлен список глав со ссылками. Там же есть возможность загрузить к себе на компьютер полную версию библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве.

В этой статье продолжим развивать элемент управления "Многострочное поле ввода". С тем, что уже было здесь сделано ранее, можно познакомиться в статье Графические интерфейсы X: Элемент "Многострочное текстовое поле ввода" (build 8). На этот раз задача состоит в том, чтобы сделать автоматический перенос слов на следующую строку в случае переполнения по ширине поля ввода или же обратный перенос на предыдущую строку, если появляется такая возможность.


Режим "Перенос по словам" в многострочном поле ввода

Во всех текстовых редакторах или приложениях с текстовой информацией работает перенос слов, если ширина области, где расположен текст, переполняется. Это избавляет от утомительной необходимости постоянно пользоваться горизонтальной полосой прокрутки. 

По умолчанию режим переноса по словам будет отключен. Чтобы включить этот режим, нужно воспользоваться методом CTextBox::WordWrapMode(). Это единственный публичный метод в реализации переноса по словам. Все остальные будут приватными, ниже мы подробно рассмотрим их организацию.

//+------------------------------------------------------------------+
//| Класс для создания многострочного текстового поля                |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Режим "Перенос по словам"
   bool m_word_wrap_mode;
   //---
public:
   //--- Режим "Перенос по словам"
   void WordWrapMode(const bool mode) { m_word_wrap_mode=mode; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_word_wrap_mode(false)

Для правильной настройки переноса слов и добавления текста в ту или иную строку, нужно, чтобы у каждой строки был признак её окончания.

Рассмотрим простой пример с одной строкой. Откроем любой текстовый редактор, в котором можно включать/отключать режим переноса слов — например, Блокнот. Добавим в документ одну строку:

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

Если режим переноса отключен, то, в зависимости от ширины области текстового поля ввода, строка в него может не поместиться. Тогда, чтоб прочесть ее, нам придётся воспользоваться горизонтальной полосой прокрутки:

 Рис. 1. Режим "Перенос по словам" отключен.

Рис. 1. Режим "Перенос по словам" отключен.


Теперь включим режим переноса слов. Строка должна будет уместиться по ширине поля ввода текстового редактора:

 Рис. 2. Режим "Перенос по словам" включен.


Рис. 2. Режим "Перенос по словам" включен.


Мы видим, что одна изначальная строка поделилась на три подстроки, которые последовательно выстроились одна за одной. Признак окончания здесь есть только у третьей подстроки, и если программно прочитать в этом файле первую строчку, то мы получим весь текст до признака её окончания. 

Проверить это можно простым скриптом:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- Получим хэндл файла
   int file=::FileOpen("Тема 'Перенос по словам'.txt",FILE_READ|FILE_TXT|FILE_ANSI);
//--- Прочитаем файл, если хэндл получен
   if(file!=INVALID_HANDLE)
      ::Print(__FUNCTION__," > ",::FileReadString(file));
   else
      ::Print(__FUNCTION__," > error: ",::GetLastError());
  }
//+------------------------------------------------------------------+

Результат чтения первой (в данном случае единственной) строки и вывода в журнал:

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

Чтобы организовать подобное считывание информации из разрабатываемого многострочного текстового поля ввода, в классе CTextBox в структуру StringOptions (старое название KeySymbolOptions) добавим ещё одно bool-свойство, которое будет хранить в себе признак окончания строки.

   //--- Символы и их свойства
   struct StringOptions
     {
      string            m_symbol[];    // Символы
      int               m_width[];     // Ширина символов
      bool              m_end_of_line; // Признак окончания строки
     };
   StringOptions  m_lines[];

Для реализации переноса слов понадобятся несколько основных и вспомогательных методов. Перечислим их задачи.

Основные методы:

  • Перенос по словам
  • Возвращение индексов первых видимых символа и пробела справа
  • Возвращение количества переносимых символов
  • Перенос текста на следующую строку
  • Перенос текста из следующей строки в текущую

Вспомогательные методы:

  • Возвращение количества слов в указанной строке
  • Возвращение индекса символа пробела по его номеру
  • Перемещение строк
  • Перемещение символов в указанной строке
  • Копирование символов в переданный массив для переноса на другую строку
  • Вставка символов из переданного массива в указанную строку

Рассмотрим подробнее, как устроены вспомогательные методы.


Описание алгоритма и вспомогательных методов

В алгоритме переноса слов есть момент, когда нужно запустить цикл, в котором будет осуществляться поиск индекса символа пробела по его номеру. Чтобы организовать такой цикл, нужен метод для определения количества слов в строке. Ниже показан код метода CTextBox::WordsTotal(), выполняющего эту задачу.

Посчитать слова довольно просто. Нужно в цикле идти по массиву символов указанной строки, отслеживая появление паттерна, когда текущий символ не является символом пробела (' '), а предыдущий является. Это и будет означать начало нового слова. Счётчик также увеличивается, если мы дошли до конца строки, чтобы не пропустить последнее слово.

class CTextBox : public CElement
  {
private:
   //--- Возвращает количество слов в указанной строке
   uint              WordsTotal(const uint line_index);
  };
//+------------------------------------------------------------------+
//| Возвращает количество слов в указанной строке                    |
//+------------------------------------------------------------------+
uint CTextBox::WordsTotal(const uint line_index)
  {
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Предотвращение выхода из диапазона
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Получим размер массива символов указанной строки
   uint symbols_total=::ArraySize(m_lines[l].m_symbol);
//--- Счётчик слов
   uint words_counter=0;
//--- Ищем пробел по указанному индексу
   for(uint s=1; s<symbols_total; s++)
     {
      //--- Считаем, если (1) дошли до конца строки или (2) нашли пробел (конец слова)
      if(s+1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE))
         words_counter++;
     }
//--- Вернуть количество слов
   return(words_counter);
  }

Для определения индекса символа пробела по его номеру будет использоваться метод CTextBox::SymbolIndexBySpaceNumber(). Получив это значение, можно вычислить ширину одного или более слов от начала подстроки, используя метод CTextBox::LineWidth(). 

Для наглядности рассмотрим пример с текстом одной строки. В нем проиндексированы символы (синий), подстроки (зелёный) и пробелы (красный). Видно, что, например, у первого (0) пробела на первой (0) строке индекс символа 6.

 Рис. 3. Индексы символов (синий), подстрок (зелёный) и пробелов (красный).

Рис. 3. Индексы символов (синий), подстрок (зелёный) и пробелов (красный).


Ниже показан код метода CTextBox::SymbolIndexBySpaceNumber(). Здесь всё просто: нужно пройтись в цикле по всем символам указанной подстроки, увеличивая счётчик каждый раз, когда находится символ пробела. Если на какой-то итерации окажется, что счётчик равен указанному в переданном значении второго аргумента индексу пробела, то значение индекса символа запоминается и цикл останавливается. Именно это значение и возвращает метод.

class CTextBox : public CElement
  {
private:
   //--- Возвращает индекс символа пробела по его номеру 
   uint              SymbolIndexBySpaceNumber(const uint line_index,const uint space_index);
  };
//+------------------------------------------------------------------+
//| Возвращает индекс символа пробела по его номеру                  |
//+------------------------------------------------------------------+
uint CTextBox::SymbolIndexBySpaceNumber(const uint line_index,const uint space_index)
  {
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Предотвращение выхода из диапазона
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Получим размер массива символов указанной строки
   uint symbols_total=::ArraySize(m_lines[l].m_symbol);
//--- (1) Для определения индекса символа пробела и (2) счётчик пробелов
   uint symbol_index  =0;
   uint space_counter =0;
//--- Ищем пробел по указанному индексу
   for(uint s=1; s<symbols_total; s++)
     {
      //--- Если нашли пробел
      if(m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE)
        {
         //--- Если счётчик равен указанному индексу пробела, запомним его и остановим цикл
         if(space_counter==space_index)
           {
            symbol_index=s;
            break;
           }
         //--- Увеличим счётчик пробелов
         space_counter++;
        }
     }
//--- Вернуть размер строки, если не нашли индекс пробела
   return((symbol_index<1)? symbols_total : symbol_index);
  }

Рассмотрим подробнее алгоритм переноса слов в той части, которая касается перемещения элементов массивов строк и символов. Пошагово проиллюстрируем, как это выглядит в различных ситуациях. Допустим, у нас есть строка:

The quick brown fox jumped over the lazy dog.

Эта строка не помещается по ширине в текстовое поле, область которого на рисунке 4 обозначена красным прямоугольником. Мы видим, что "лишнюю" часть строки —  'over the lazy dog.' — нужно перенести на следующую строку.

 Рис. 4. Ситуация с переполнением строки текстового поля.

Рис. 4. Ситуация с переполнением строки текстового поля.

Поскольку динамический массив строк на данный момент состоит из одного элемента, то сначала его нужно увеличить на один элемент. В новой строке массив символов нужно установить по количеству символов переносимого текста и после этого перенести часть строки, которая не помещается. Итоговый результат:

 Рис. 5. Часть строки перенесена на следующую новую строку.

Рис. 5. Часть строки перенесена на следующую новую строку.

Теперь посмотрим, как будет работать алгоритм, если сейчас ширина текстового поля уменьшится приблизительно на 30%. Здесь сначала также определяется, какая часть первой (индекс 0) строки выходит за границы текстового поля. В данном случае не поместилась подстрока 'fox jumped'. Далее динамический массив строк увеличивается на один элемент. Затем все подстроки, которые находятся ниже, смещаются на одну строку вниз, освобождая таким образом место для переносимого текста. После этого подстрока 'fox jumped' перемещается на освободившееся место, как это было описано в предыдущем абзаце. Этот шаг показан на рисунке ниже.

 Рис. 6. Перенос текста на вторую (индекс 1) строку.

Рис. 6. Перенос текста на вторую (индекс 1) строку.


На следующей итерации в цикле алгоритм переходит на следующую строку (индекс 1). Здесь снова нужно проверить, не выходит ли часть этой строки за границы текстового поля. Если проверка покажет, что не выходит, то нужно посмотреть, не хватит ли в этой строке места справа, чтобы поместить на него часть следующей строки с индексом 2. Так мы проверяем выполнение условий для обратного переноса текста из начала следующей строки (индекс 2) в конец текущей (индекс 1).

Кроме этого условия, нужно ещё проверить, не имеет ли текущая строка признака окончания. Если он есть, тогда обратный перенос не осуществляется. В нашем случае признака окончания нет, и места хватает для переноса одного слова — 'over'. При обратном переносе размер массивов символов изменяется на количество добавленных и извлечённых символов на текущей и следующей строках соответственно. При обратном переносе, перед изменением размера массива символов, оставшиеся символы перемещаются в начало строки. На рисунке ниже показан этот шаг. 

 Рис. 7. Обратный перенос текста на вторую (индекс 1) строку с третьей (индекс 2) строки.

Рис. 7. Обратный перенос текста на вторую (индекс 1) строку с третьей (индекс 2) строки.


Мы видим, что при сужении текстового поля будет осуществляться прямой и обратный перенос текста. Если же поле будет расширяться, то будет достаточно обратного переноса на освободившиеся места. При переносе текста на следующую строку динамический массив строк будет каждый раз увеличиваться на один элемент, а при обратном переносе всего оставшегося текста следующей строки массив строк будет каждый раз на один элемент уменьшаться. Но перед этим, если дальше есть ещё строки, они должны быть смещены на одну строку вверх, чтобы исключить образование пустой строки при обратном переносе оставшегося текста. 

Всех этих шагов с перестановками строк, прямым и обратным переносом слов мы в процессе прохождения цикла не увидим: холст для рисования поля ввода обновится только один раз, уже после того, как все операции с массивами будут выполнены. На иллюстрации ниже показано, какую примерно картину увидит пользователь при работе с графическим интерфейсом:

 Рис. 8. Демонстрация работы алгоритма переноса слов на примере с текстовым редактором.

Рис. 8. Демонстрация работы алгоритма переноса слов на примере с текстовым редактором.


Но и это ещё не всё. Если на одной строке осталось только одно слово (беспрерывная последовательность символов), то в таком случае перенос осуществляется посимвольно. На иллюстрации ниже показана такая ситуация:

 Рис. 9. Демонстрация посимвольного переноса в случае, когда слово не помещается.


Рис. 9. Демонстрация посимвольного переноса в случае, когда слово не помещается.

Теперь рассмотрим методы для перемещения строк и символов. Для перемещения строк будет использоваться метод CTextBox::MoveLines(). В метод передаются индексы строк, от которого и до которого нужно сместить строки на одну позицию. Третий параметр — направление смещения. По умолчанию он установлен на перемещение строк вниз. 

До этого мы использовали алгоритм смещения строк однократно, при управлении текстовым полем ввода с клавиатуры клавишами 'Enter' и 'Backspace'. Теперь этот же код используется в нескольких методах класса CTextBox, поэтому целесообразно будет реализовать отдельный метод для повторного использования.

Код метода CTextBox::MoveLines():

class CTextBox : public CElement
  {
private:
   //--- Перемещает строки
   void              MoveLines(const uint from_index,const uint to_index,const bool to_down=true);
  };
//+------------------------------------------------------------------+
//| Перемещение строк                                                |
//+------------------------------------------------------------------+
void CTextBox::MoveLines(const uint from_index,const uint to_index,const bool to_down=true)
  {
//--- Смещение строк по направлению вниз
   if(to_down)
     {
      for(uint i=from_index; i>to_index; i--)
        {
         //--- Индекс предыдущего элемента массива строк
         uint prev_index=i-1;
         //--- Получим размер массива символов
         uint symbols_total=::ArraySize(m_lines[prev_index].m_symbol);
         //--- Установим новый размер массивам
         ArraysResize(i,symbols_total);
         //--- Сделать копию строки
         LineCopy(i,prev_index);
         //--- Если это последняя итерация
         if(prev_index==to_index)
           {
            //--- Выйти, если это первая строка
            if(to_index<1)
               break;
           }
        }
     }
//--- Смещение строк по направлению вверх
   else
     {
      for(uint i=from_index; i<to_index; i++)
        {
         //--- Индекс следующего элемента массива строк
         uint next_index=i+1;
         //--- Получим размер массива символов
         uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
         //--- Установим новый размер массивам
         ArraysResize(i,symbols_total);
         //--- Сделать копию строки
         LineCopy(i,next_index);
        }
     }
  }

Для перемещения символов в строке реализован метод CTextBox::MoveSymbols(). Он вызывается не только в новых методах, которые относятся к режиму переноса слов, но и при добавлении/удалении символов посредством клавиатуры в методах CTextBox::AddSymbol() и CTextBox::DeleteSymbol(), рассмотренных ранее. Здесь указываются входные параметры: (1) индекс строки, в которой будут перемещены символы; (2) индексы символов, от которого и до которого будет перемещение; (3) направление перемещения (по умолчанию установлено перемещение влево).

class CTextBox : public CElement
  {
private:
   //--- Перемещение символов в указанной строке
   void              MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true);
  };
//+------------------------------------------------------------------+
//| Перемещение символов в указанной строке                          |
//+------------------------------------------------------------------+
void CTextBox::MoveSymbols(const uint line_index,const uint from_pos,const uint to_pos,const bool to_left=true)
  {
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Разница
   uint offset=from_pos-to_pos;
//--- Если нужно сместить символы влево
   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];
        }
     }
//--- Если нужно сместить символы вправо
   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];
        }
     }
  }

Неоднократно здесь будет использоваться и код вспомогательных методов для копирования и вставки символов: CTextBox::CopyWrapSymbols() и CTextBox::PasteWrapSymbols(). При копировании в метод CTextBox::CopyWrapSymbols() передаётся пустой динамический массив и указывается, на какой строке и начиная с какого символа нужно скопировать указанное количество символов. Для вставки символов в метод CTextBox::PasteWrapSymbols() нужно передать массив со скопированными до этого символами, указав при этом индекс строки и символа, куда  будет осуществляться вставка.

class CTextBox : public CElement
  {
private:
   //--- Копирует в переданный массив символы для переноса на другую строку
   void              CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[]);
   //--- Вставляет символы из переданного массива в указанную строку
   void              PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[]);
  };
//+------------------------------------------------------------------+
//| Копирует в переданный массив символы для переноса                |
//+------------------------------------------------------------------+
void CTextBox::CopyWrapSymbols(const uint line_index,const uint start_pos,const uint symbols_total,string &array[])
  {
//--- Установим размер массиву
   ::ArrayResize(array,symbols_total);
//--- Скопируем в массив символы, которые нужно перенести
   for(uint i=0; i<symbols_total; i++)
      array[i]=m_lines[line_index].m_symbol[start_pos+i];
  }
//+------------------------------------------------------------------+
//| Вставляет символы в указанную строку                             |
//+------------------------------------------------------------------+
void CTextBox::PasteWrapSymbols(const uint line_index,const uint start_pos,string &array[])
  {
   uint array_size=::ArraySize(array);
//--- Добавить данные в массивы структуры новой строки
   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]);
     }
  }

Далее рассмотрим основные методы алгоритма переноса слов.


Описание основных методов

Когда алгоритм начинает свою работу, то в цикле проверяет на каждой строке, нет ли переполнения. Для такой проверки реализован метод CTextBox::CheckForOverflow(). Этот метод возвращает три значения, два из которых сохраняются в переменных, переданных в этот метод, как параметры по ссылкам. 

В начале метода нужно получить ширину текущей строки, индекс которой передаётся в метод в качестве первого параметра. Ширина строки проверяется с учётом отступа от левого края текстового поля и ширины вертикальной полосы прокрутки. Если ширина строки помещается в текстовом поле, то метод возвращает false, что означает "переполнения нет". Если же строка не помещается, то далее нужно определить индексы первых видимых в правой части текстового поля символа и пробела. Для этого в цикле двигаемся от конца строки по всем её символам и проверяем, помещается ли строка от начала до этого символа по ширине текстового поля. Если строка помещается, то индекс этого символа сохраняется. Кроме этого, на каждой итерации проверяется, не является ли текущий символ пробелом, и если да, то сохраняется его индекс, а поиск заканчивается.

После всех этих проверок и поиска метод возвращает истину, если обнаружен хотя бы один из искомых индексов. Это означает, что строка не помещается. Дальше индексы символа и пробела будут использоваться так: если индекс символа обнаружен, а индекс пробела — нет, то значит, в строке нет пробелов и нужно перенести часть символов этой строки. Если же пробел обнаружен, то нужно перенести часть строки, начиная с индекса этого пробела.

class CTextBox : public CElement
  {
private:
   //--- Возвращает индексы первых видимых символа и пробела
   bool              CheckForOverflow(const uint line_index,int &symbol_index,int &space_index);
  };
//+------------------------------------------------------------------+
//| Проверка на переполнение строки                                  |
//+------------------------------------------------------------------+
bool CTextBox::CheckForOverflow(const uint line_index,int &symbol_index,int &space_index)
  {
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Отступы
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Получим полную ширину строки
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- Если ширина этой строки помещается в поле
   if(full_line_width<(uint)m_area_visible_x_size)
      return(false);
//--- Определим индексы символов переполнения
   for(uint s=symbols_total-1; s>0; s--)
     {
      //--- Получим (1) ширину подстроки от начала до текущего символа и (2) символ
      uint   line_width =LineWidth(s,line_index)+x_offset_plus;
      string symbol     =m_lines[line_index].m_symbol[s];
      //--- Если ещё не нашли видимый символ
      if(symbol_index==WRONG_VALUE)
        {
         //--- Если ширина подстроки помещается в область поля ввода, запомним индекс символа
         if(line_width<(uint)m_area_visible_x_size)
            symbol_index=(int)s;
         //--- Перейти к следующему символу
         continue;
        }
      //--- Если это пробел, запомним его индекс и остановим цикл
      if(symbol==SPACE)
        {
         space_index=(int)s;
         break;
        }
     }
//--- Выполнение условия означает, что строка не помещается
   bool is_overflow=(symbol_index!=WRONG_VALUE || space_index!=WRONG_VALUE);
//--- Вернуть результат
   return(is_overflow);
  }

Если строка помещается и метод CTextBox::CheckForOverflow() возвращает false, то нужно проверить, можно ли осуществить обратный перенос. Метод определения количества символов для переноса — CTextBox::WrapSymbolsTotal(). 

Этот метод возвращает в переменную по ссылке количество символов, которые нужно перенести, а также признак того, весь ли это оставшийся ли этот текст или только его часть. В начале метода рассчитываются значения для локальных переменных, например, следующие параметры:

  • Количество символов текущей строки
  • Полная ширина строки
  • Ширина свободного пространства
  • Количество слов в следующей строке
  • Количество символов в следующей строке

После этого в цикле определяется, сколько слов можно перенести со следующей строки на текущую. Получая на каждой итерации ширину подстроки до указанного пробела, проверяем, помещается ли она в свободное место на текущей строке.

Если помещается, то запоминаем индекс символа и проверяем, можно ли вместить сюда ещё одно слово. Если проверка показала, что текст кончился, то отмечаем это в специально предназначенной локальной переменной и останавливаем цикл

Если же подстрока не помещается, то здесь нужно также проверить, последний ли это символ в строке, поставив метку, что это сплошная строка без пробелов, и остановить цикл.

Затем, если в следующей строке есть пробелы или нет свободного места, то метод сразу возвращает результат. Если же эта проверка пройдена, то далее определяем, можно ли перенести на текущую строку часть слова со следующей строки. Обратный перенос части слова осуществляется только если эта строка не помещается в свободное пространство текущей строки, и при этом последние символы текущей и следующей строк — не пробелы. Если эти проверки пройдены, то в следующем цикле будет определено, сколько символов нужно перенести.

class CTextBox : public CElement
  {
private:
   //--- Возвращает количество переносимых символов
   bool              WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total);
  };
//+------------------------------------------------------------------+
//| Возвращает количество переносимых символов с признаком объёма    |
//+------------------------------------------------------------------+
bool CTextBox::WrapSymbolsTotal(const uint line_index,uint &wrap_symbols_total)
  {
//--- Признаки (1) количества символов для переноса и (2) строки без пробелов
   bool is_all_text=false,is_solid_row=false;
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Отступы
   uint x_offset_plus=m_text_x_offset+m_scrollv.ScrollWidth();
//--- Получим полную ширину строки
   uint full_line_width=LineWidth(symbols_total,line_index)+x_offset_plus;
//--- Получим ширину свободного пространства
   uint free_space=m_area_visible_x_size-full_line_width;
//--- Получим количество слов в следующей строке
   uint next_line_index =line_index+1;
   uint words_total     =WordsTotal(next_line_index);
//--- Получим размер массива символов
   uint next_line_symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Определить количество слов, которые можно перенести со следующей строки (поиск по пробелу)
   for(uint w=0; w<words_total; w++)
     {
      //--- Получим (1) индекс пробела и (2) ширину подстроки от начала до пробела
      uint ss_index        =SymbolIndexBySpaceNumber(next_line_index,w);
      uint substring_width =LineWidth(ss_index,next_line_index);
      //--- Если подстрока помещается в свободное пространство текущей строки
      if(substring_width<free_space)
        {
         //--- ...проверим, можно ли добавить ещё одно слово
         wrap_symbols_total=ss_index;
         //--- Остановиться, если это вся строка
         if(next_line_symbols_total==wrap_symbols_total)
           {
            is_all_text=true;
            break;
           }
        }
      else
        {
         //--- Если это сплошная строка без пробела
         if(ss_index==next_line_symbols_total)
            is_solid_row=true;
         //---
         break;
        }
     }
//--- Сразу вернуть результат, если (1) это строка с пробелом или (2) нет свободного места
   if(!is_solid_row || free_space<1)
      return(is_all_text);
//--- Получим полную ширину следующей строки
   full_line_width=LineWidth(next_line_symbols_total,next_line_index)+x_offset_plus;
//--- Если (1) строка не помещается и нет пробелов в конце (2) текущей и (3) предыдущей строках
   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)
     {
      //--- Определить количество символов, которые можно перенести со следующей строки
      for(uint s=next_line_symbols_total-1; s>=0; s--)
        {
         //--- Получим ширину подстроки от начала до указанного символа
         uint substring_width=LineWidth(s,next_line_index);
         //--- Если подстрока не помещается в свободное пространство указанного контейнера, перейти к следующему символу
         if(substring_width>=free_space)
            continue;
         //--- Если подстрока помещается, запомним значение и остановимся
         wrap_symbols_total=s;
         break;
        }
     }
//--- Вернуть истину, если нужно перенести весь текст
   return(is_all_text);
  }

Если строка не помещается, то текст с текущей строки на новую будет переноситься с помощью метода CTextBox::WrapTextToNewLine(). Он будет использоваться в двух режимах: (1) автоматический перенос слов и (2) принудительный, когда, например, нажимается клавиша 'Enter'. По умолчанию, в качестве третьего параметра установлен режим для автоматического переноса. Первые два параметра метода — это (1) индекс строки, с которой будет перенос текста и (2) индекс символа, от которого нужно осуществить перенос текста на следующую (новую) строку. 

В начале метода определяется количество переносимых символов для переноса. Затем (1) в локальный динамический массив копируется нужное количество символов текущей строки, (2) устанавливаются размеры массивам текущей и следующей строк, и (3) скопированные символы добавляются в массив символов следующей строки. После этого нужно определить новое местоположение текстового курсора, если при вводе текста с клавиатуры он находился среди перенесенных символов.

Последняя операция в этом методе — проверка и корректная установка признаков окончания текущей и следующей строк, так как в различных ситуациях должен получаться уникальный результат.

1. Если метод CTextBox::WrapTextToNewLine() был вызван после нажатия клавиши 'Enter', то если текущая строка имеет признак окончания, в следующей строке тоже делается признак окончания. Если же в текущей строке нет признака окончания, то теперь его нужно установить, а в следующей строке убрать.  

2. Когда метод вызывается в автоматическом режиме, то если текущая строка имеет признак окончания, его нужно убрать, а в следующей строке установить. Если же текущая строка не имеет признака окончания, то отсутствие признака нужно установить обеим строкам. 

Код метода:

class CTextBox : public CElement
  {
private:
   //--- Перенос текста на следующую строку
   void              WrapTextToNewLine(const uint curr_line_index,const uint symbol_index,const bool by_pressed_enter=false);
  };
//+------------------------------------------------------------------+
//| Перенос текста на новую строку                                   |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToNewLine(const uint line_index,const uint symbol_index,const bool by_pressed_enter=false)
  {
//--- Получим размер массива символов из строки
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
//--- Последний индекс символа
   uint last_symbol_index=symbols_total-1;
//--- Корректировка в случае пустой строки
   uint check_symbol_index=(symbol_index>last_symbol_index && symbol_index!=symbols_total)? last_symbol_index : symbol_index;
//--- Индекс следующей строки
   uint next_line_index=line_index+1;
//--- Количество символов, которые нужно перенести на новую строку
   uint new_line_size=symbols_total-check_symbol_index;
//--- Скопируем в массив символы, которые нужно перенести
   string array[];
   CopyWrapSymbols(line_index,check_symbol_index,new_line_size,array);
//--- Установим новый размер массивам структуры в строке
   ArraysResize(line_index,symbols_total-new_line_size);
//--- Установим новый размер массивам структуры в новой строке
   ArraysResize(next_line_index,new_line_size);
//--- Добавить данные в массивы структуры новой строки
   PasteWrapSymbols(next_line_index,0,array);
//--- Определим новое положение текстового курсора
   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;
//--- Если указано, что вызов по нажатию на клавише Enter
   if(by_pressed_enter)
     {
      //--- Если строка имела признак окончания, то ставим признак окончания текущей и следующей
      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;
        }
      //--- Если нет, то только текущей
      else
        {
         m_lines[line_index].m_end_of_line      =true;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
   else
     {
      //--- Если строка имела признак окончания, то продолжаем и устанавливаем признак на следующей строке
      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;
        }
      //--- Если строка не имела признак окончания, то продолжаем в обеих строках
      else
        {
         m_lines[line_index].m_end_of_line      =false;
         m_lines[next_line_index].m_end_of_line =false;
        }
     }
  }

Для обратного переноса текста предназначен метод CTextBox::WrapTextToPrevLine(). В него передаётся индекс следующей строки и количество символов, которые будут перенесены на текущую строку. В качестве третьего параметра указывается, переносится ли весь оставшийся текст или только его часть. По умолчанию установлено, что переносится часть текста (false). 

В начале метода в локальный динамический массив копируется указанное количество символов со следующей строки. Затем массив символов текущей строки нужно увеличить на добавляемое количество символов. После этого (1) ранее скопированные символы добавляются в новые элементы массива символов текущей строки; (2) оставшиеся символы следующей строки перемещаются в начало массива; (3) массив символов следующей строки уменьшается на изъятое из неё количество символов. 

Далее местоположение текстового курсора нужно скорректировать. Если он был в той же части слова, которая была перенесена на предыдущую строку, то тогда его тоже нужно перенести вместе с ней.

В самом конце, в случае, когда переносится весь оставшийся текст, нужно (1) в текущую строку добавить признак окончания, (2) сместить все нижние строки на одну позицию вверх и (3) уменьшить массив строк на один элемент.

class CTextBox : public CElement
  {
private:
   //--- Перенос текста из указанной строки в предыдущую
   void              WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false);
  };
//+------------------------------------------------------------------+
//| Перенос текста из следующей строки в текущую                     |
//+------------------------------------------------------------------+
void CTextBox::WrapTextToPrevLine(const uint next_line_index,const uint wrap_symbols_total,const bool is_all_text=false)
  {
//--- Получим размер массива символов из строки
   uint symbols_total=::ArraySize(m_lines[next_line_index].m_symbol);
//--- Индекс предыдущей строки
   uint prev_line_index=next_line_index-1;
//--- Скопируем в массив символы, которые нужно перенести
   string array[];
   CopyWrapSymbols(next_line_index,0,wrap_symbols_total,array);
//--- Получим размер массива символов из предыдущей строки
   uint prev_line_symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
//--- Увеличим размер массива предыдущей строки на добавляемое количество символов
   uint new_prev_line_size=prev_line_symbols_total+wrap_symbols_total;
   ArraysResize(prev_line_index,new_prev_line_size);
//--- Добавить данные в массивы структуры новой строки
   PasteWrapSymbols(prev_line_index,new_prev_line_size-wrap_symbols_total,array);
//--- Сместим символы на освободившееся место в текущей строке
   MoveSymbols(next_line_index,wrap_symbols_total,0);
//--- Уменьшим размер массива текущей строки на извлечённое из неё количество символов
   ArraysResize(next_line_index,symbols_total-wrap_symbols_total);
//--- Скорректировать текстовый курсор
   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--;
     }
//--- Выйти, если это не весь оставшийся текст строки
   if(!is_all_text)
      return;
//--- Добавить признак окончания для предыдущей строки, если у текущей строки он тоже есть
   if(m_lines[next_line_index].m_end_of_line)
      m_lines[next_line_index-1].m_end_of_line=true;
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Сместим строки на одну вверх
   MoveLines(next_line_index,lines_total-1,false);
//--- Установим новый размер массиву строк
   ::ArrayResize(m_lines,lines_total-1);
  }

Осталось рассмотреть последний и самый главный метод — CTextBox::WordWrap(). Чтобы впоследствии перенос слов работал, вызов этого метода нужно разместить в методе CTextBox::ChangeTextBoxSize(). 

В начале метода CTextBox::WordWrap() стоит проверка на то, включены ли режимы многострочного поля ввода и переноса по словам. Если один из этих режимов отключен, то программа выходит из метода. Если режимы включены, то нужно пройти в цикле по всем строкам, чтобы привести в действие алгоритм переноса текста. Здесь на каждой итерации с помощью метода CTextBox::CheckForOverflow() осуществляется проверка на переполнение строкой ширины текстового поля. 

  1. Если строка не помещается, то смотрим, был ли найден ближайший от правой части поля пробел, от которого часть текущей строки будет перенесена на следующую строку. Символ пробела не переносится на следующую строку, поэтому индекс пробела инкрементируется. Затем массив строк увеличивается на один элемент, а нижние строки смещаются на одну позицию вниз. Ещё раз уточняется, по какому индексу будет перенесена часть строки. После этого текст переносится. 
  2. Если строка помещается, то проверяем, не нужно ли осуществить обратный перенос. В начале этого блока стоит проверка на признак окончания текущей строки. Если он есть, программа переходит на следующую итерацию. Если проверка пройдена, то далее определяется количество символов для переноса, после чего текст переносится на предыдущую строку.
//+------------------------------------------------------------------+
//| Класс для создания многострочного текстового поля                |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Перенос по словам
   void              WordWrap(void);
  };
//+------------------------------------------------------------------+
//| Перенос по словам                                                |
//+------------------------------------------------------------------+
void CTextBox::WordWrap(void)
  {
//--- Выйти, если режимы (1) многострочного поля ввода или (2) переноса по словам отключены
   if(!m_multi_line_mode || !m_word_wrap_mode)
      return;
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Проверим, нужно ли выровнять текст по ширине поля ввода
   for(uint i=0; i<lines_total; i++)
     {
      //--- Для определения первых видимых индексов (1) символа и (2) пробела
      int symbol_index =WRONG_VALUE;
      int space_index  =WRONG_VALUE;
      //--- Индекс следующей строки
      uint next_line_index=i+1;
      //--- Если строка не помещается, то перенесём часть текущей строки на новую строку
      if(CheckForOverflow(i,symbol_index,space_index))
        {
         //--- Если пробел найден, то он переносится не будет
         if(space_index!=WRONG_VALUE)
            space_index++;
         //--- Увеличим массив строк на один элемент
         ::ArrayResize(m_lines,++lines_total);
         //--- Сместим строки от текущей позиции на один пункт вниз
         MoveLines(lines_total-1,next_line_index);
         //--- Проверим индекс символа, от которого будет перенос текста
         int check_index=(space_index==WRONG_VALUE && symbol_index!=WRONG_VALUE)? symbol_index : space_index;
         //--- Перенесём текст на новую строку
         WrapTextToNewLine(i,check_index);
        }
      //--- Если строка помещается, то проверим, не нужно ли осуществить обратный перенос
      else
        {
         //--- Пропускаем, если (1) это строка с окончанием или (2) это последняя строка
         if(m_lines[i].m_end_of_line || next_line_index>=lines_total)
            continue;
         //--- Определим количество символов для переноса
         uint wrap_symbols_total=0;
         //--- Если нужно перенести оставшийся текст следующей строки на текущую
         if(WrapSymbolsTotal(i,wrap_symbols_total))
           {
            WrapTextToPrevLine(next_line_index,wrap_symbols_total,true);
            //--- Обновить размер массива для дальнейшего использования в цикле
            lines_total=::ArraySize(m_lines);
            //--- Шаг назад, чтобы избежать пропуск строки для следующей проверки
            i--;
           }
         //--- Перенести только то, что помещается
         else
            WrapTextToPrevLine(next_line_index,wrap_symbols_total);
        }
     }
  }

Мы рассмотрели все методы для автоматического переноса текста. Теперь посмотрим, как это всё работает.


Приложение для теста элемента

Создадим тестовое MQL-приложение для тестов. Возьмём уже готовый вариант из предыдущей статьи о многострочном поле ввода, только удалим из графического интерфейса приложения однострочное поле ввода. Всё остальное останется точно таким же. Вот как это работает на графике в терминале MetaTrader 5:

Рис. 10. Демонстрация переноса слов в элементе "Мультистрочное поле ввода". 

Рис. 10. Демонстрация переноса слов в элементе "Мультистрочное поле ввода".


В конце статьи Вы можете загрузить к себе на компьютер это тестовое приложение для более подробного изучения.


Заключение

На текущем этапе разработки библиотеки для создания графических интерфейсов её общая схема выглядит так:

 Рис. 11. Структура библиотеки на текущей стадии разработки.


Рис. 11. Структура библиотеки на текущей стадии разработки.


Ниже вы можете загрузить к себе на компьютер последнюю версию библиотеки и файлы для тестов.

При возникновении вопросов по использованию материала, предоставленного в этих файлах, вы можете обратиться к подробному описанию процесса разработки библиотеки в одной из статей этой серии или задать вопрос в комментариях к статье. 


Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (19)
Anatoli Kazharski
Anatoli Kazharski | 15 апр. 2017 в 13:06
Alexander:
  Самое странное что программа не удаляется, наверно потому что даже значка эксперта в верхнем правом углу не появляется, а в списке объектов нет ни одного
Если значка эксперта нет, то это значит, что программа уже удалилась. Нажмите на кнопку 'Все' в окне 'Список объектов' (Ctrl + B).
Mikhail Dovbakh
Mikhail Dovbakh | 16 апр. 2017 в 14:37
Спасибо Анатолий!
Очень поучительный пример!
Особенно приятно, что работает и в МТ4.
Еще раз снимаю шляпу.
Ваша настойчивая и кропотливая работа восхищает.
Всех благ и хорошего настроения!
С Праздником.
Anatoli Kazharski
Anatoli Kazharski | 16 апр. 2017 в 17:48
Mikhail Dovbakh:
...
Особенно приятно, что работает и в МТ4.
...

Осторожнее с MT4, так как я в этом терминале, с некоторых пор, вообще не тестирую библиотеку. 

Rashid Umarov
Rashid Umarov | 12 мая 2017 в 08:54

Анатолий, тут нет опечатки?

   for(uint s=1; s<symbols_total; s++)
     {
      //--- Считаем, если (2) дошли до конца строки или (2) нашли пробел (конец слова)
      if(s+1==symbols_total || (m_lines[l].m_symbol[s]!=SPACE && m_lines[l].m_symbol[s-1]==SPACE))
         words_counter++;
     }
//--- Вернуть количество слов
   return(words_counter);

Если нет, то объясните логику, пожалуйста

Anatoli Kazharski
Anatoli Kazharski | 12 мая 2017 в 09:13
Rashid Umarov:

Анатолий, тут нет опечатки?

Если нет, то объясните логику, пожалуйста

Опечатка. (1) (2)

Логика простая. Считаем слова в текущей строке. Словом здесь считается непрерывная последовательность символов (без пробела).

  1. Если дошли до конца строки, то увеличиваем счётчик слов.
  2. Если текущий символ не пробел, а предыдущий пробел, то увеличиваем счётчик слов.
Рецепты MQL5 - Создаем кольцевой буфер для быстрого расчета индикаторов в скользящем окне Рецепты MQL5 - Создаем кольцевой буфер для быстрого расчета индикаторов в скользящем окне
Кольцевой буфер — самый простой и в то же время наиболее эффективный способ организации данных для расчетов в скользящем окне. В статье описано, как устроен этот алгоритм, и показано, как с его помощью сделать вычисление в скользящем окне простым и эффективным процессом.
Торговля по каналам Дончиана Торговля по каналам Дончиана
В статье разрабатываются и тестируются несколько стратегий на основе канала Дончиана с применением различных индикаторных фильтров. Проводится исследование и сравнительный анализ их работы.
Волны Вульфа Волны Вульфа
Графический метод, предложенный Биллом Вульфом, позволяет не только выявить фигуру и тем самым определить момент и направление входа, но и спрогнозировать цель, которую должна достигнуть цена, и время ее достижения. В статье описано, как на основе индикатора Зигзаг создать индикатор для поиска волн Вульфа и простой советник, торгующий по его сигналам.
Секвента ДеМарка (TD SEQUENTIAL) с использованием искусственного интеллекта Секвента ДеМарка (TD SEQUENTIAL) с использованием искусственного интеллекта
В этой статье я расскажу, как с помощью "скрещивания" одной очень известной стратегии и нейронной сети можно успешно заниматься трейдингом. Речь пойдет о стратегии Томаса Демарка "Секвента" с применением системы искусственного интеллекта. Работать будем ТОЛЬКО по первой части стратегии, используя сигналы "Установка" и "Пересечение".