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

Anatoli Kazharski | 2 февраля, 2017


Содержание

 

Введение

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

В этой статье рассмотрим новый элемент управления: "Многострочное поле ввода". В отличие от графического объекта типа OBJ_EDIT, предлагаемого терминалом, в представленной версии не будет ограничений на количество вводимых символов. Становится доступен режим превращения поля ввода в простой текстовый редактор. То есть, в него можно будет ввести несколько строк, а текстовый курсор можно будет двигать также и мышью, и клавишами. Если видимая часть элемента переполнится строками, появится полоса прокрутки. Элемент "Многострочное текстовое поле ввода" будет полностью нарисован, и по качеству он будет максимально приближен к аналогичному элементу в операционных системах.


Группы клавиш и клавиатурные раскладки

Прежде чем начать описание кода элемента типа CTextBox ("Текстовая область"), очень кратко поговорим о клавиатуре, ведь именно она станет инструментом ввода данных. Также обозначим, нажатие каких клавиш будет обрабатываться в первой версии класса элемента. 

Клавиши на клавиатуре можно разделить на несколько групп (см. обозначения на рис. 1):

  • Управляющие клавиши (оранжевый цвет)
  • Функциональные клавиши (фиолетовый)
  • Клавиши ввода (синий)
  • Клавиши перехода (зелёный)
  • Цифровая клавиатура (красный)

 Рис. 1. Группы клавиш (клавиатурная раскладка QWERTY).

Рис. 1. Группы клавиш (клавиатурная раскладка QWERTY).


Существует несколько латинских клавиатурных раскладок для английского языка. Самая популярная из них — QWERTY. В нашем случае основной язык русский, поэтому мы используем русскоязычную раскладку ЙЦУКЕН. QWERTY мы оставляем для английского языка, который выбран в качестве дополнительного. 

Начиная с билда 1510, в языке MQL есть функция ::TranslateKey(). С ее помощью мы получаем символ, по переданному коду нажатой клавиши соответствующий установленной в ОС раскладке и языку. Ранее массивы символов для каждого языка и раскладки нужно было формировать самостоятельно, что вызывало сложности ввиду большого объёма работ. Теперь всё стало намного проще.


Обработка события нажатия клавиш

Отследить событие нажатия клавиши можно в системной функции ::OnChartEvent() по идентификатору CHARTEVENT_KEYDOWN:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Нажатие кнопки на клавиатуре
   if(id==CHARTEVENT_KEYDOWN)
     {
      ::Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam,"; symbol: ",::ShortToString(::TranslateKey((int)lparam)));
      return;
     }
  }

В качестве остальных трёх параметров в функцию приходят следующие значения:

  • long-параметр (lparam) – код нажатой клавиши, то есть код ASCII-символа или код клавиши, которая относится к группе управляющих. 
  • dparam-параметр (dparam) – количество нажатий клавиши, сгенерированных за время её удержания в нажатом состоянии. Значение всегда равно 1. Если нужно получить количество вызовов от момента зажатия клавиши, подсчёт производится самостоятельно.
  • sparam-параметр (sparam) – строковое значение битовой маски, описывающее статус кнопок клавиатуры. Событие генерируется сразу же, в момент нажатия клавиши. Если кнопку нажать и сразу отпустить, не удерживая, то здесь будет значение скан-кода клавиши. При нажатии и последующем удерживании будет генерироваться значение, которое формируется из скан-кода + 16384 бита.

Например, в листинге ниже (вывод в журнал терминала) показан результат нажатия и удерживания клавиши 'Esc'. У этой клавиши код 27 (lparam), скан-код в момент нажатия равен 1 (sparam), а при удерживании клавиши в течение приблизительно около 500 миллисекунд начинает генерироваться значение 16385 (скан-код + 16384 бита).

2017.01.20 17:53:33.240 id: 0; lparam: 27; dparam: 1.0; sparam: 1
2017.01.20 17:53:33.739 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.772 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.805 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.837 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
2017.01.20 17:53:33.870 id: 0; lparam: 27; dparam: 1.0; sparam: 16385
...

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

Рис. 2. Кнопки занятые терминалом или не генерирующие событие CHARTEVENT_KEYDOWN. 

Рис. 2. Кнопки, занятые терминалом или не генерирующие событие CHARTEVENT_KEYDOWN.


ASCII-коды символов и управляющих клавиш

Информация из Википедии (подробнее): 

ASCII (англ. American standard code for information interchange) — название таблицы (кодировки, набора), в которой некоторым распространённым печатным и непечатным символам сопоставлены числовые коды. Таблица была разработана и стандартизована в США в 1963 году.

На рисунке ниже показаны ASCII-коды клавиш:

 Рис. 3. ASCII-коды символов и управляющих клавиш.

Рис. 3. ASCII-коды клавиш.


Все ASCII-коды были размещены в файле KeyCodes.mqh в виде макроподстановок (#define). В листинге ниже показана часть этих кодов:

//+------------------------------------------------------------------+
//|                                                     KeyCodes.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Коды символов ASCII и управляющих клавиш                         |
//| для обработки события нажатия клавиш (long-параметр события)     |
//+------------------------------------------------------------------+
#define KEY_BACKSPACE          8
#define KEY_TAB                9
#define KEY_NUMPAD_5           12
#define KEY_ENTER              13
#define KEY_SHIFT              16
#define KEY_CTRL               17
#define KEY_BREAK              19
#define KEY_CAPS_LOCK          20
#define KEY_ESC                27
#define KEY_SPACE              32
#define KEY_PAGE_UP            33
#define KEY_PAGE_DOWN          34
#define KEY_END                35
#define KEY_HOME               36
#define KEY_LEFT               37
#define KEY_UP                 38
#define KEY_RIGHT              39
#define KEY_DOWN               40
#define KEY_INSERT             45
#define KEY_DELETE             46
...

 


Скан-коды клавиш

Информация из Википедии (подробнее):  

Скан-код (англ. scan code) — в IBM-совместимых компьютерах код, присвоенный каждой клавише, с помощью которого драйвер клавиатуры распознает, какая клавиша была нажата.

На рисунке ниже показаны скан-коды клавиш:

Рис. 4. Скан-коды клавиш. 

Рис. 4. Скан-коды клавиш.


Как и ASCII-коды, скан-коды содержатся в файле KeyCodes.mqh. В листинге ниже представлена лишь часть этого списка:

//+------------------------------------------------------------------+
//|                                                     KeyCodes.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
...
//--- Бит
#define KEYSTATE_ON            16384
//+------------------------------------------------------------------+
//| Скан-коды клавиш (string-параметр события)                       |
//+------------------------------------------------------------------+
//| Нажата один раз: KEYSTATE_XXX                                    |
//| Зажата: KEYSTATE_XXX + KEYSTATE_ON                               |
//+------------------------------------------------------------------+
#define KEYSTATE_ESC           1
#define KEYSTATE_1             2
#define KEYSTATE_2             3
#define KEYSTATE_3             4
#define KEYSTATE_4             5
#define KEYSTATE_5             6
#define KEYSTATE_6             7
#define KEYSTATE_7             8
#define KEYSTATE_8             9
#define KEYSTATE_9             10
#define KEYSTATE_0             11
//---
#define KEYSTATE_MINUS         12
#define KEYSTATE_EQUALS        13
#define KEYSTATE_BACKSPACE     14
#define KEYSTATE_TAB           15
//---
#define KEYSTATE_Q             16
#define KEYSTATE_W             17
#define KEYSTATE_E             18
#define KEYSTATE_R             19
#define KEYSTATE_T             20
#define KEYSTATE_Y             21
#define KEYSTATE_U             22
#define KEYSTATE_I             23
#define KEYSTATE_O             24
#define KEYSTATE_P             25
...

 


Вспомогательный класс для работы с клавиатурой

Для более удобной работы с клавиатурой реализован класс CKeys. Он содержится в файле Keys.mqh, и к нему подключен файл KeyCodes.mqh со всеми кодами клавиш и символов. 

//+------------------------------------------------------------------+
//|                                                         Keys.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include <EasyAndFastGUI\KeyCodes.mqh>
//+------------------------------------------------------------------+
//| Класс для работы с клавиатурой                                   |
//+------------------------------------------------------------------+
class CKeys
  {
public:
                     CKeys(void);
                    ~CKeys(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CKeys::CKeys(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CKeys::~CKeys(void)
  {
  }

Чтобы определить нажатие клавиши с:

(1) алфавитным символом (включая пробел)

(2)  символом цифровой клавиши

или (3) специальным символом,

нужно использовать метод CKeys::KeySymbol(). Если в него передать значение long-параметра события с идентификатором CHARTEVENT_KEYDOWN, он вернёт символ в строковом формате (string) либо пустую строку (''), если была нажата клавиша не из указанных диапазонов. 

class CKeys
  {
public:
   //--- Возвращает символ нажатой клавиши
   string            KeySymbol(const long key_code);
  };
//+------------------------------------------------------------------+
//| Возвращает символ нажатой клавиши                                |
//+------------------------------------------------------------------+
string CKeys::KeySymbol(const long key_code)
  {
   string key_symbol="";
//--- Если нужно ввести пробел (клавиша "Space")
   if(key_code==KEY_SPACE)
     {
      key_symbol=" ";
     }
//--- Если нужно ввести (1) алфавитный символ, или (2) символ цифровой клавиши, или (3) специальный символ
   else if((key_code>=KEY_A && key_code<=KEY_Z) ||
           (key_code>=KEY_0 && key_code<=KEY_9) ||
           (key_code>=KEY_SEMICOLON && key_code<=KEY_SINGLE_QUOTE))
     {
      key_symbol=::ShortToString(::TranslateKey((int)key_code));
     }
//--- Вернуть символ
   return(key_symbol);
  }

И, наконец, нам понадобится метод для определения текущего состояния клавиши 'Ctrl'. Она будет использоваться в различных комбинациях одновременного нажатия двух клавиш при перемещении текстового курсора в поле ввода.

Для получения текущего состояния клавиши 'Ctrl' воспользуемся системной функцией языка ::TerminalInfoInteger(). У этой функции есть несколько идентификаторов для определения текущего состояния клавиш. Для клавиши 'Ctrl' предназначен идентификатор TERMINAL_KEYSTATE_CONTROL. Со всеми остальными идентификаторами этого типа можно ознакомиться в справке по языку MQL5.

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

class CKeys
  {
public:
   //--- Возвращает состояние клавиши Ctrl
   bool              KeyCtrlState(void);
  };
//+------------------------------------------------------------------+
//| Возвращает состояние клавиши Ctrl                                |
//+------------------------------------------------------------------+
bool CKeys::KeyCtrlState(void)
  {
   return(::TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<0);
  }

Теперь у нас всё готово к разработке класса для создания элемента "Текстовое поле ввода". 

 


Элемент "Многострочное текстовое поле ввода"

Элемент "Многострочное текстовое поле ввода" может использоваться в комбинированных элементах управления. Он относится к группе сложных составных элементов, поскольку содержит полосы прокрутки. Многострочное текстовое поле ввода можно использовать  использовать и для ввода текста, и для отображения ранее сохраненного текста из файла.

Ранее уже рассматривались элементы с полями ввода для ввода числовых значений (класс CSpinEdit) или произвольного текста (CTextEdit). В них использовался графический объект типа OBJ_EDIT. Он имеет серьезные ограничения: ввести можно только 63 символа, да еще и уместить их надо в одну строку. Поэтому сейчас наша задача — создать текстовое поле ввода без таких ограничений.  


 

Рис. 5. Элемент "Многострочное текстовое поле ввода".

Теперь рассмотрим подробнее, как устроен класс CTextBox для создания этого элемента.

 

Разработка класса CTextBox для создания элемента

Создаём файл TextBox.mqh с классом CTextBox со всеми стандартными методами каждого элемента управления разрабатываемой библиотеки и подключаем к нему следующие файлы:

  • С базовым классом элемента — Element.mqh.
  • С классами полос прокрутки — Scrolls.mqh.
  • С классом для работы с клавиатурой — Keys.mqh.
  • С классом для работы с временным счётчиком — TimeCounter.mqh.
  • С классом для работы с графиком, на котором размещено MQL-приложение — Chart.mqh
//+------------------------------------------------------------------+
//|                                                      TextBox.mqh |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Scrolls.mqh"
#include "..\Keys.mqh"
#include "..\Element.mqh"
#include "..\TimeCounter.mqh"
#include <Charts\Chart.mqh>

//+------------------------------------------------------------------+
//| Класс для создания многострочного текстового поля                |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Экземпляр класса для работы с клавиатурой
   CKeys             m_keys;
   //--- Экземпляр класса для управления графиком
   CChart            m_chart;
   //--- Экземпляр класса для работы со счётчиком таймера
   CTimeCounter      m_counter;
   //---
public:
                     CTextBox(void);
                    ~CTextBox(void);
   //--- Обработчик событий графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Таймер
   virtual void      OnEventTimer(void);
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y,const bool moving_mode=false);
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Установка, (2) сброс приоритетов на нажатие левой кнопки мыши
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
   //--- Сбросить цвет
   virtual void      ResetColors(void) {}
   //---
private:
   //--- Изменить ширину по правому краю окна
   virtual void      ChangeWidthByRightWindowSide(void);
   //--- Изменить высоту по нижнему краю окна
   virtual void      ChangeHeightByBottomWindowSide(void);
  };



Свойства и внешний вид

Нам понадобится структура, назовём её KeySymbolOptions, с массивами символов и их свойств. В текущей версии в ней будут два динамических массива: 

  • В массиве m_symbol[] будут содержаться все символы строки по отдельности.
  • В массиве m_width[] будет содержаться ширина всех символов строки по отдельности.

Экземпляр этой структуры тоже объявим как динамический массив. Его размер будет всегда равен количеству строк в поле ввода.

class CTextBox : public CElement
  {
private:
   //--- Символы и их свойства
   struct KeySymbolOptions
     {
      string            m_symbol[]; // Символы
      int               m_width[];  // Ширина символов
     };
   KeySymbolOptions  m_lines[];
  };

В первой версии элемента текст будет выводится целыми строками, поэтому перед выводом текста её нужно собрать из массива m_symbol[]. Для этих целей предназначен метод CTextBox::CollectString(), в который нужно передать индекс строки:

class CTextBox : public CElement
  {
private:
   //--- Переменная для работы со строкой
   string            m_temp_input_string;
   //---
private:
   //--- Собирает строку из символов
   string            CollectString(const uint line_index);
  };
//+------------------------------------------------------------------+
//| Собирает строку из символов                                      |
//+------------------------------------------------------------------+
string CTextBox::CollectString(const uint line_index)
  {
   m_temp_input_string="";
   uint symbols_total=::ArraySize(m_lines[line_index].m_symbol);
   for(uint i=0; i<symbols_total; i++)
      ::StringAdd(m_temp_input_string,m_lines[line_index].m_symbol[i]);
//---
   return(m_temp_input_string);
  }

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

  • Цвет фона в разных состояниях
  • Цвет текста в разных состояниях
  • Цвет рамки в разных состояниях
  • Текст по умолчанию
  • Цвет текста по умолчанию
  • Многострочный режим
  • Режим "Только для чтения"
class CTextBox : public CElement
  {
private:
   //--- Цвет фона
   color             m_area_color;
   color             m_area_color_locked;
   //--- Цвет текста
   color             m_text_color;
   color             m_text_color_locked;
   //--- Цвет рамки
   color             m_border_color;
   color             m_border_color_hover;
   color             m_border_color_locked;
   color             m_border_color_activated;
   //--- Текст по умолчанию
   string            m_default_text;
   //--- Цвет текста по умолчанию
   color             m_default_text_color;
   //--- Многострочный режим
   bool              m_multi_line_mode;
   //--- Режим "Только для чтения"
   bool              m_read_only_mode;
   //---
public:
   //--- Цвет фона в разных состояниях
   void              AreaColor(const color clr)                { m_area_color=clr;                 }
   void              AreaColorLocked(const color clr)          { m_area_color_locked=clr;          }
   //--- Цвет текста в разных состояниях
   void              TextColor(const color clr)                { m_text_color=clr;                 }
   void              TextColorLocked(const color clr)          { m_text_color_locked=clr;          }
   //--- Цвета рамки в разных состояниях
   void              BorderColor(const color clr)              { m_border_color=clr;               }
   void              BorderColorHover(const color clr)         { m_border_color_hover=clr;         }
   void              BorderColorLocked(const color clr)        { m_border_color_locked=clr;        }
   void              BorderColorActivated(const color clr)     { m_border_color_activated=clr;     }
   //--- (1) Текст по умолчанию и (2) цвет текста по умолчанию
   void              DefaultText(const string text)            { m_default_text=text;              }
   void              DefaultTextColor(const color clr)         { m_default_text_color=clr;         }
   //--- (1) Многострочный режим, (2) режим "Только для чтения"
   void              MultiLineMode(const bool mode)            { m_multi_line_mode=mode;           }
   bool              ReadOnlyMode(void)                  const { return(m_read_only_mode);         }
   void              ReadOnlyMode(const bool mode)             { m_read_only_mode=mode;            }
  };

Само поле ввода (фон, текст, рамка и мигающий текстовый курсор) будет полностью рисоваться на одном графическом объекте типа OBJ_BITMAP_LABEL. По сути, это просто картинка. Она будет перерисовываться в двух случаях:

  • при взаимодействии с элементом
  • через заданный промежуток времени для мигания курсора, когда поле ввода активировано. 

При наведении курсора в область поля ввода его рамка будет изменять свой цвет. Чтобы исключить слишком частую перерисовку картинки, нужно отслеживать момент пересечения курсором границы поля ввода. То есть, перерисовываться элемент должен только один раз, в момент входа курсора в область поля ввода или в момент выхода из неё. Для этих целей в базовый класс элемента добавлены методы CElementBase::IsMouseFocus(). С их помощью устанавливается и получается флаг, указывающий на факт пересечения:

//+------------------------------------------------------------------+
//| Базовый класс элемента управления                                |
//+------------------------------------------------------------------+
class CElementBase
  {
protected:
   //--- Для определения момента пересечения курсором мыши границ элемента
   bool              m_is_mouse_focus;
   //---
public:
   //--- Момент входа/выхода в/из фокуса элемента
   bool              IsMouseFocus(void)                        const { return(m_is_mouse_focus);             }
   void              IsMouseFocus(const bool focus)                  { m_is_mouse_focus=focus;               }
  };

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

class CTextBox : public CElement
  {
private:
   //--- Возвращает текущий цвет фона
   uint              AreaColorCurrent(void);
   //--- Возвращает текущий цвет текста
   uint              TextColorCurrent(void);
   //--- Возвращает текущий цвет рамки
   uint              BorderColorCurrent(void);
  };
//+------------------------------------------------------------------+
//| Возвращает цвет фона относительно текущего состояния элемента    |
//+------------------------------------------------------------------+
uint CTextBox::AreaColorCurrent(void)
  {
   uint clr=::ColorToARGB((m_text_box_state)? m_area_color : m_area_color_locked);
//--- Вернуть цвет
   return(clr);
  }
//+------------------------------------------------------------------+
//| Возвращает цвет текста относительно текущего состояния элемента  |
//+------------------------------------------------------------------+
uint CTextBox::TextColorCurrent(void)
  {
   uint clr=::ColorToARGB((m_text_box_state)? m_text_color : m_text_color_locked);
//--- Вернуть цвет
   return(clr);
  }
//+------------------------------------------------------------------+
//| Возвращает цвет рамки относительно текущего состояния элемента   |
//+------------------------------------------------------------------+
uint CTextBox::BorderColorCurrent(void)
  {
   uint clr=clrBlack;
//--- Если элемент не заблокирован
   if(m_text_box_state)
     {
      //--- Если поле ввода активировано
      if(m_text_edit_state)
         clr=m_border_color_activated;
      //--- Если не активировано, то проверяем фокус элемента
      else
         clr=(CElementBase::IsMouseFocus())? m_border_color_hover : m_border_color;
     }
//--- Если элемент заблокирован
   else
      clr=m_border_color_locked;
//--- Вернуть цвет
   return(::ColorToARGB(clr));
  }

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

class CTextBox : public CElement
  {
private:
   //--- Возвращает высоту строки
   uint              LineHeight(void);
  };
//+------------------------------------------------------------------+
//| Возвращает высоту строки                                         |
//+------------------------------------------------------------------+
uint CTextBox::LineHeight(void)
  {
//--- Установим шрифт для отображения на холсте (нужно, чтобы получить высоту строки)
   m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- Вернём высоту строки
   return(m_canvas.TextHeight("|"));
  }

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

class CTextBox : public CElement
  {
private:
   //--- Рисует рамку
   void              DrawBorder(void);
  };
//+------------------------------------------------------------------+
//| Рисует рамку поля ввода                                          |
//+------------------------------------------------------------------+
void CTextBox::DrawBorder(void)
  {
//--- Получим цвет рамки относительно текущего состояния элемента
   uint clr=BorderColorCurrent();
//--- Получим смещение по оси X
   int xo=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int yo=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);

//--- Границы
   int x_size =m_canvas.X_Size()-1;
   int y_size =m_canvas.Y_Size()-1;
//--- Координаты: Сверху/Справа/Снизу/Слева
   int x1[4]; x1[0]=x;         x1[1]=x_size+xo; x1[2]=xo;        x1[3]=x;
   int y1[4]; y1[0]=y;         y1[1]=y;         y1[2]=y_size+yo; y1[3]=y;
   int x2[4]; x2[0]=x_size+xo; x2[1]=x_size+xo; x2[2]=x_size+xo; x2[3]=x;
   int y2[4]; y2[0]=y;         y2[1]=y_size+yo; y2[2]=y_size+yo; y2[3]=y_size+yo;
//--- Рисуем рамку по указанным координатам
   for(int i=0; i<4; i++)
      m_canvas.Line(x1[i],y1[i],x2[i],y2[i],clr);
  }

Метод CTextBox::DrawBorder() также будет использоваться в методе CTextBox::ChangeObjectsColor(), когда нужно просто изменить цвет рамки поля ввода при наведении на него курсора мыши (см. листинг кода ниже). Для этого достаточно просто перерисовать рамку (а не всё поле ввода) и обновить картинку. Метод CTextBox::ChangeObjectsColor() будет вызываться в обработчике событий элемента. Именно здесь отслеживается факт пересечения курсором границ элемента, чтобы исключить слишком частую перерисовку.

class CTextBox : public CElement
  {
private:
   //--- Изменение цвета объектов
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| Изменение цвета объектов                                         |
//+------------------------------------------------------------------+
void CTextBox::ChangeObjectsColor(void)
  {
//--- Если не в фокусе
   if(!CElementBase::MouseFocus())
     {
      //--- Если ещё не отмечено, что не в фокусе
      if(CElementBase::IsMouseFocus())
        {
         //--- Поставить флаг
         CElementBase::IsMouseFocus(false);
         //--- Изменить цвет
         DrawBorder();
         m_canvas.Update();
        }
     }
   else
     {
      //--- Если ещё не отмечено, что в фокусе
      if(!CElementBase::IsMouseFocus())
        {
         //--- Поставить флаг
         CElementBase::IsMouseFocus(true);
         //--- Изменить цвет
         DrawBorder();
         m_canvas.Update();
        }
     }
  }

Для вывода текста на холст предназначен метод CTextBox::TextOut(). Здесь в самом начале очищаем холст, закрашивая его указанным цветом. Далее программа может пойти двумя путями:

  • Если многострочный режим отключен и при этом в строке нет ни одного символа, то нужно вывести текст по умолчанию (если он указан). Он будет выводиться по центру поля ввода.
  • Если же многострочный режим включен или в строке есть хотя бы один символ, то получаем высоту строки и в цикле выводим все строки, предварительно собирая их из массива символов. По умолчанию для текста заданы отступы от левого верхнего угла поля ввода. По оси X это 5 пикселей, а по оси Y4 пикселя. С помощью методов CTextBox::TextXOffset() и CTextBox::TextYOffset() можно переопределить эти значения. 
class CTextBox : public CElement
  {
private:
   //--- Отступы для текста от краёв поля ввода
   int               m_text_x_offset;
   int               m_text_y_offset;
   //---
public:
   //--- Отступы для текста от краёв поля ввода
   void              TextXOffset(const int x_offset)           { m_text_x_offset=x_offset;         }
   void              TextYOffset(const int y_offset)           { m_text_y_offset=y_offset;         }

   //---
private:
   //--- Вывод текста на холст
   void              TextOut(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_text_x_offset(5),
                           m_text_y_offset(4)
  {
...
  }
//+------------------------------------------------------------------+
//| Вывод текста на холст                                            |
//+------------------------------------------------------------------+
void CTextBox::TextOut(void)
  {
//--- Очистить холст
   m_canvas.Erase(AreaColorCurrent());
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Если включен многострочный режим или количество символов больше нуля
   if(m_multi_line_mode || symbols_total>0)
     {
      //--- Получим высоту строки
      int line_height=(int)LineHeight();
      //--- Получим размер массива строк
      uint lines_total=::ArraySize(m_lines);
      //---
      for(uint i=0; i<lines_total; i++)
        {
         //--- Получим координаты для текста
         int x=m_text_x_offset;
         int y=m_text_y_offset+((int)i*line_height);
         //--- Собираем строку из массива символов
         CollectString(i);
         //--- Нарисовать текст
         m_canvas.TextOut(x,y,m_temp_input_string,TextColorCurrent(),TA_LEFT);
        }
     }
//--- Если же многострочный режим отключен и нет ни одного символа, то будет отображаться текст по умолчанию
   else
     {
      //--- Нарисовать текст, если указан
      if(m_default_text!="")
         m_canvas.TextOut(m_area_x_size/2,m_area_y_size/2,m_default_text,::ColorToARGB(m_default_text_color),TA_CENTER|TA_VCENTER);
     }
  }

Чтобы отрисовать текстовый курсор, понадобятся методы для расчета его координат. Чтобы рассчитать X-координату, нужно указать индекс строки и индекс символа, на который нужно поместить курсор. Для этого используется метод CTextBox::LineWidth(). Так как ширина каждого символа сохраняется в динамическом массиве m_width[] структуры KeySymbolOptions, то здесь остаётся только суммировать ширину символов до указанной позиции

class CTextBox : public CElement
  {
private:
   //--- Возвращает ширину строки в пикселях
   uint              LineWidth(const uint line_index,const uint symbol_index);
  };
//+------------------------------------------------------------------+
//| Возвращает ширину строки от начала до указанной позиции          |
//+------------------------------------------------------------------+
uint CTextBox::LineWidth(const uint line_index,const uint symbol_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_width);
//--- Предотвращение выхода из диапазона
   uint s=(symbol_index<symbols_total)? symbol_index : symbols_total;
//--- Суммируем ширину всех символов
   uint width=0;
   for(uint i=0; i<s; i++)
      width+=m_lines[l].m_width[i];

//--- Вернуть ширину строки
   return(width);
  }

Методы для получения координат текстового курсора принимают совсем простой вид (см. листинг кода ниже). Координаты сохраняются в полях m_text_cursor_x и m_text_cursor_y. В расчётах координат используется и текущая позиция курсора, и индексы строки и символа, куда его нужно переместить. Для хранения этих значений предназначены поля m_text_cursor_x_pos и m_text_cursor_y_pos.

class CTextBox : public CElement
  {
private:
   //--- Текущие координаты текстового курсора
   int               m_text_cursor_x;
   int               m_text_cursor_y;
   //--- Текущая позиция текстового курсора
   uint              m_text_cursor_x_pos;
   uint              m_text_cursor_y_pos;
   //---
private:
   //--- Расчёт координат для текстового курсора
   void              CalculateTextCursorX(void);
   void              CalculateTextCursorY(void);
  };
//+------------------------------------------------------------------+
//| Расчёт X-координаты для текстового курсора                       |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextCursorX(void)
  {
//--- Получим ширину строки
   int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos);
//--- Рассчитать и сохранить X-координату курсора
   m_text_cursor_x=m_text_x_offset+line_width;
  }
//+------------------------------------------------------------------+
//| Расчёт Y-координаты для текстового курсора                       |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextCursorY(void)
  {
//--- Получим высоту строки
   int line_height=(int)LineHeight();
//--- Получим Y-координату курсора
   m_text_cursor_y=m_text_y_offset+int(line_height*m_text_cursor_y_pos);
  }

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

Для примера на скриншотах показаны увеличенные в графическом редакторе перекрытые и не перекрытые курсором символы 'd' и 'д'.

 Рис. 6. Пример перекрытия текстовым курсором пикселей символа 'd'.

Рис. 6. Пример перекрытия текстовым курсором пикселей символа 'd'.

 Рис. 7. Пример перекрытия текстовым курсором пикселей символа 'д'.

Рис. 7. Пример перекрытия текстовым курсором пикселей символа 'д'.


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

Теперь рассмотрим метод CTextBox::DrawCursor() для отрисовки текстового курсора. Ширина курсора будет равна одному пикселю, а высота совпадет с высотой строки. В самом начале получаем X-координату, на которой нарисовать курсор, и высоту строки. Y-координата будет рассчитываться в цикле, так как рисование будет попиксельным. Помним, что в базовом классе элементов CElementBase уже объявлен экземпляр класса CColors для работы с цветом. Поэтому сейчас на каждой итерации, рассчитав Y-координату, получаем цвет текущего пикселя на указанных координатах, а затем методом CColors::Negative() инвертируем его и устанавливаем на то же место

class CTextBox : public CElement
  {
private:
   //--- Рисует текстовый курсор
   void              DrawCursor(void);
  };
//+------------------------------------------------------------------+
//| Рисует текстовый курсор                                          |
//+------------------------------------------------------------------+
void CTextBox::DrawCursor(void)
  {
//--- Получим высоту строки
   int line_height=(int)LineHeight();
//--- Получим X-координату курсора
   CalculateTextCursorX();
//--- Нарисуем текстовый курсор
   for(int i=0; i<line_height; i++)
     {
      //--- Получим Y-координату для пикселя
      int y=m_text_y_offset+((int)m_text_cursor_y_pos*line_height)+i;
      //--- Получим текущий цвет пикселя
      uint pixel_color=m_canvas.PixelGet(m_text_cursor_x,y);
      //--- Инвертируем цвет для курсора
      pixel_color=m_clr.Negative((color)pixel_color);
      m_canvas.PixelSet(m_text_cursor_x,y,::ColorToARGB(pixel_color));
     }
  }

Реализованы два метода для отрисовки поля ввода с текстом: CTextBox::DrawText() и CTextBox::DrawTextAndCursor(). 

Метод CTextBox::DrawText() нужно использовать, когда нужно просто обновить текст в неактивном поле ввода. Здесь всё просто. Если элемент не скрыт, выводим текст, рисуем рамку и обновляем картинку.  

class CTextBox : public CElement
  {
private:
   //--- Рисует текст
   void              DrawText(void);
  };
//+------------------------------------------------------------------+
//| Рисует текст                                                     |
//+------------------------------------------------------------------+
void CTextBox::DrawText(void)
  {
//--- Выйти, если элемент скрыт
   if(!CElementBase::IsVisible())
      return;
//--- Выводим текст
   CTextBox::TextOut();
//--- Рисуем рамку
   DrawBorder();
//--- Обновить поле ввода
   m_canvas.Update();
  }

Если же поле ввода активировано, то, кроме текста нужно отобразить еще и мигающий текстовый курсор – метод CTextBox::DrawTextAndCursor(). Для мигания нужно определять состояние показать/скрыть для курсора. Каждый раз при вызове этого метода состояние будет меняться на противоположное. Также учтена возможность принудительного показа, когда в метод (аргумент show_state) передано значение true. Принудительный показ понадобится при перемещении курсора по полю ввода, когда оно активно. Фактически мигание курсора будет осуществляться в таймере элемента по установленному в конструкторе класса интервалу для временного счётчика, который здесь равен 200 миллисекунд. Каждый раз после вызова метода CTextBox::DrawTextAndCursor() счётчик нужно обнулять

class CTextBox : public CElement
  {
private:
   //--- Отображает текст и мигающий курсор
   void              DrawTextAndCursor(const bool show_state=false);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void)
  {
//--- Установка параметров для счётчика таймера
   m_counter.SetParameters(16,200);
  }
//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CTextBox::OnEventTimer(void)
  {
...
//--- Пауза между обновлением текстового курсора
   if(m_counter.CheckTimeCounter())
     {
      //--- Обновим текстовый курсор, если элемент виден и поле ввода активировано
      if(CElementBase::IsVisible() && m_text_edit_state)
         DrawTextAndCursor();
     }
  }
//+------------------------------------------------------------------+
//| Отображает текст и мигающий курсор                               |
//+------------------------------------------------------------------+
void CTextBox::DrawTextAndCursor(const bool show_state=false)
  {
//--- Определим состояние для текстового курсора (показать/скрыть)
   static bool state=false;
   state=(!show_state)? !state : show_state;

//--- Выводим текст
   CTextBox::TextOut();
//--- Нарисовать текстовый курсор
   if(state)
      DrawCursor();
//--- Рисуем рамку
   DrawBorder();
//--- Обновить поле ввода
   m_canvas.Update();
//--- Сброс счётчика
   m_counter.ZeroTimeCounter();
  }

Для создания элемента "Многострочное текстовое поле ввода" понадобится три приватных (private) метода, два из которых нужны для создания полос прокрутки, и один публичный (public) для внешнего вызова в пользовательском классе: 

class CTextBox : public CElement
  {
private:
   //--- Объекты для создания элемента
   CRectCanvas       m_canvas;
   CScrollV          m_scrollv;
   CScrollH          m_scrollh;
   //---
public:
   //--- Методы для создания элемента
   bool              CreateTextBox(const long chart_id,const int subwin,const int x_gap,const int y_gap);
   //---
private:
   bool              CreateCanvas(void);
   bool              CreateScrollV(void);
   bool              CreateScrollH(void);

   //---
public:
   //--- Возвращает указатели на полосы прокрутки
   CScrollV         *GetScrollVPointer(void)                   { return(::GetPointer(m_scrollv));  }
   CScrollH         *GetScrollHPointer(void)                   { return(::GetPointer(m_scrollh));  }
  };

Перед вызовом метода CTextBox::CreateCanvas() для создания поля ввода нужно сначала рассчитать его размеры. Здесь будет применён такой же метод, как это было реализовано в нарисованной таблице типа CCanvasTable. Пробежимся по нему вкратце. Есть общий размер картинки, а есть размер её видимой части. Размер элемента равен размеру видимой части картинки. При перемещении текстового курсора или полос прокрутки координаты картинка будет изменяться, а координаты видимой части (они же — координаты элемента) будут оставаться на месте.

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

class CTextBox : public CElement
  {
private:
   //--- Возвращает максимальную ширину строки
   uint              MaxLineWidth(void);
  };
//+------------------------------------------------------------------+
//| Возвращает максимальную ширину строки                            |
//+------------------------------------------------------------------+
uint CTextBox::MaxLineWidth(void)
  {
   uint max_line_width=0;
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
   for(uint i=0; i<lines_total; i++)
     {
      //--- Получим размер массива символов
      uint symbols_total=::ArraySize(m_lines[i].m_symbol);
      //--- Получим ширину строки
      uint line_width=LineWidth(symbols_total,i);
      //--- Сохраним максимальную ширину
      if(line_width>max_line_width)
         max_line_width=line_width;
     }
//--- Вернуть максимальную ширину строки
   return(max_line_width);
  }

Код метода CTextBox::CalculateTextBoxSize() для расчёта размеров элемента тогда будет выглядеть так, как показано в листинге ниже. Вызов этого метода также будет осуществляться в методах CTextBox::ChangeWidthByRightWindowSide() и CTextBox::ChangeHeightByBottomWindowSide(), предназначение которых сводится к тому, чтобы размеры элемента автоматически изменялись под размер формы, если такие свойства заданы разработчиком. 

class CTextBox : public CElement
  {
private:
   //--- Общий размер и размер видимой части элемента
   int               m_area_x_size;
   int               m_area_y_size;
   int               m_area_visible_x_size;
   int               m_area_visible_y_size;
   //---
private:
   //--- Рассчитывает ширину текстового поля ввода
   void              CalculateTextBoxSize(void);
  };
//+------------------------------------------------------------------+
//| Рассчитывает размеры текстового поля ввода                       |
//+------------------------------------------------------------------+
void CTextBox::CalculateTextBoxSize(void)
  {
//--- Получим максимальную ширину строки из текстового поля ввода
   int max_line_width=int((m_text_x_offset*2)+MaxLineWidth()+m_scrollv.ScrollWidth());
//--- Определим общую ширину
   m_area_x_size=(max_line_width>m_x_size)? max_line_width : m_x_size;
//--- Определим видимую ширину
   m_area_visible_x_size=m_x_size;
//--- Получим высоту строки
   int line_height=(int)LineHeight();
//--- Получим размер массива строк
   int lines_total=::ArraySize(m_lines);
//--- Рассчитаем общую высоту элемента
   int lines_height=int((m_text_y_offset*2)+(line_height*lines_total)+m_scrollh.ScrollWidth());
//--- Определим общую высоту
   m_area_y_size=(m_multi_line_mode && lines_height>m_y_size)? lines_height : m_y_size;
//--- Определим видимую высоту
   m_area_visible_y_size=m_y_size;
  }

Мы рассчитали размеры. Теперь их нужно применить. Для этого предназначен метод CTextBox::ChangeTextBoxSize(). Здесь можно указать через аргументы метода, нужно ли смещать область видимости в начало или оставить её на тех же позициях. Кроме этого, здесь ещё изменяются размеры полос прокрутки и осуществляется финальная корректировка области видимости относительно ползунков полос прокрутки. Не будем рассматривать здесь код этих методов, так как подобное уже было в предыдущих статьях. 

class CTextBox : public CElement
  {
private:
   //--- Изменить размеры поля ввода
   void              ChangeTextBoxSize(const bool x_offset=false,const bool y_offset=false);
  };
//+------------------------------------------------------------------+
//| Изменить размеры поля ввода                                      |
//+------------------------------------------------------------------+
void CTextBox::ChangeTextBoxSize(const bool is_x_offset=false,const bool is_y_offset=false)
  {
//--- Установить новый размер таблице
   m_canvas.XSize(m_area_x_size);
   m_canvas.YSize(m_area_y_size);
   m_canvas.Resize(m_area_x_size,m_area_y_size);
//--- Установим размеры видимой области
   m_canvas.SetInteger(OBJPROP_XSIZE,m_area_visible_x_size);
   m_canvas.SetInteger(OBJPROP_YSIZE,m_area_visible_y_size);
//--- Разница между общей шириной и видимой частью
   int x_different=m_area_x_size-m_area_visible_x_size;
   int y_different=m_area_y_size-m_area_visible_y_size;
//--- Зададим смещение фрейма внутри изображения по осям X и Y
   int x_offset=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int y_offset=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
   m_canvas.SetInteger(OBJPROP_XOFFSET,(!is_x_offset)? 0 : (x_offset<=x_different)? x_offset : x_different);
   m_canvas.SetInteger(OBJPROP_YOFFSET,(!is_y_offset)? 0 : (y_offset<=y_different)? y_offset : y_different);
//--- Изменить размеры полос прокрутки
   ChangeScrollsSize();
//--- Корректировка данных
   ShiftData();
  }

Для управления состоянием элемента и для получения его текущего состояния предназначены следующие поля и методы:

  • Метод CTextBox::TextEditState() получает состояние поля ввода. 
  • Вызов метода CTextBox::TextBoxState() блокирует/разблокирует элемент. Заблокированный элемент переводится в режим "Только для чтения". При этом для фона, рамки и текста будут установлены соответствующие цвета (это может сделать пользователь перед тем, как создать элемент). 
class CTextBox : public CElement
  {
private:
   //--- Режим "Только для чтения"
   bool              m_read_only_mode;
   //--- Состояние поля ввода
   bool              m_text_edit_state;
   //--- Состояние элемента
   bool              m_text_box_state;
   //---
public:
   //--- (1) Состояние поля ввода, (2) возвращение/установка состояния доступности элемента
   bool              TextEditState(void)                 const { return(m_text_edit_state);        }
   bool              TextBoxState(void)                  const { return(m_text_box_state);         }
   void              TextBoxState(const bool state);
  };
//+------------------------------------------------------------------+
//| Установка состояния доступности элемента                         |
//+------------------------------------------------------------------+
void CTextBox::TextBoxState(const bool state)
  {
   m_text_box_state=state;
//--- Настройка относительно текущего состояния
   if(!m_text_box_state)
     {
      //--- Приоритеты
      m_canvas.Z_Order(-1);
      //--- Поле ввода в режим "Только для чтения"
      m_read_only_mode=true;
     }
   else
     {
      //--- Приоритеты
      m_canvas.Z_Order(m_text_edit_zorder);
      //--- Поле ввода в режим редактирования
      m_read_only_mode=false;
     }
//--- Обновить поле ввода
   DrawText();
  }

 


Управление текстовым курсором

Текстовое поле ввода активируется по нажатию на него мышью. Сразу же определяются координаты места нажатия, и туда устанавливается текстовый курсор. За это отвечает метод CTextBox::OnClickTextBox(). Но перед тем, как перейти к его описанию, рассмотрим для начала несколько вспомогательных методов, которые будут вызываться в нём, а также во многих других методах класса CTextBox.

Метод CTextBox::SetTextCursor() для обновления значений позиции текстового курсора. В режиме однострочного поля ввода позиция по оси Y всегда равна 0.

class CTextBox : public CElement
  {
private:
   //--- Текущая позиция текстового курсора
   uint              m_text_cursor_x_pos;
   uint              m_text_cursor_y_pos;
   //---
private:
   //--- Устанавливает курсор по указанным позициям
   void              SetTextCursor(const uint x_pos,const uint y_pos);
  };
//+------------------------------------------------------------------+
//| Устанавливает курсор по указанным позициям                       |
//+------------------------------------------------------------------+
void CTextBox::SetTextCursor(const uint x_pos,const uint y_pos)
  {
   m_text_cursor_x_pos=x_pos;
   m_text_cursor_y_pos=(!m_multi_line_mode)? 0 : y_pos;
  }

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

class CTextBox : public CElement
  {
public:
   //--- Прокрутка таблицы: (1) вертикальная и (2) горизонтальная
   void              VerticalScrolling(const int pos=WRONG_VALUE);
   void              HorizontalScrolling(const int pos=WRONG_VALUE);
  };

Метод CTextBox::DeactivateTextBox() нужен для дезактивации поля ввода. Здесь следует упомянуть о новой возможности, предоставленной недавно разработчиками терминала. Добавлен ещё один идентификатор графика (CHART_KEYBOARD_CONTROL) из семейства перечислений ENUM_CHART_PROPERTY. Он включает или отключает управление графиком с клавиатуры клавишами 'Left', 'Right', 'Home', 'End', 'Page Up', 'Page Down', а также клавишами для масштабирования графика '+' и '-'. Таким образом, когда поле ввода активируется, то управление графиком нужно отключить, чтобы нажатия на перечисленные клавиши не перехватывались терминалом, и это, в свою очередь, не мешало работе поля ввода. В момент дезактивации поля ввода можно снова включать управление графиком с клавиатуры. 

Здесь же нужно перерисовать поле ввода, и если это не многострочный режим, то переместить текстовый курсор и ползунок полосы прокрутки в начало строки. 

class CTextBox : public CElement
  {
private:
   //--- Дезактивирует поле ввода
   void              DeactivateTextBox(void);
  };
//+------------------------------------------------------------------+
//| Дезактивация поля ввода                                          |
//+------------------------------------------------------------------+
void CTextBox::DeactivateTextBox(void)
  {
//--- Выйти, если уже дезактивировано
   if(!m_text_edit_state)
      return;
//--- Дезактивировать
   m_text_edit_state=false;
//--- Включим управление графиком
   m_chart.SetInteger(CHART_KEYBOARD_CONTROL,true);
//--- Нарисовать текст
   DrawText();
//--- Если многострочный режим отключен
   if(!m_multi_line_mode)
     {
      //--- Переместить курсор в начало строки
      SetTextCursor(0,0);
      //--- Сместить полосу прокрутки в начало строки
      HorizontalScrolling(0);
     }
  }

Управляя текстовым курсором, нужно отслеживать, не пересёк ли он границы области видимости. Если пересечение произошло, то нужно снова вернуть курсор в область видимости. Для этого понадобятся дополнительные многоразовые методы. Нужно рассчитывать допустимые границы поля ввода с учётом многострочного режима и наличия полос прокрутки. 

Чтобы рассчитать, насколько нужно сместить область видимости, нужно прежде всего узнать значения текущего смещения

class CTextBox : public CElement
  {
private:
   //--- Для расчёта границ видимой части поля ввода
   int               m_x_limit;
   int               m_y_limit;
   int               m_x2_limit;
   int               m_y2_limit;
   //---
private:
   //--- Расчёт границ поля ввода
   void              CalculateBoundaries(void);
   void              CalculateXBoundaries(void);
   void              CalculateYBoundaries(void);
  };
//+------------------------------------------------------------------+
//| Расчёт границ поля ввода по двум осям                            |
//+------------------------------------------------------------------+
void CTextBox::CalculateBoundaries(void)
  {
   CalculateXBoundaries();
   CalculateYBoundaries();
  }
//+------------------------------------------------------------------+
//| Расчёт границ поля ввода по оси X                                |
//+------------------------------------------------------------------+
void CTextBox::CalculateXBoundaries(void)
  {
//--- Получим X-координату и смещение по оси X
   int x       =(int)m_canvas.GetInteger(OBJPROP_XDISTANCE);
   int xoffset =(int)m_canvas.GetInteger(OBJPROP_XOFFSET);

//--- Рассчитаем границы видимой части поля ввода
   m_x_limit  =(x+xoffset)-x;
   m_x2_limit =(m_multi_line_mode)? (x+xoffset+m_x_size-m_scrollv.ScrollWidth()-m_text_x_offset)-x : (x+xoffset+m_x_size-m_text_x_offset)-x;
  }
//+------------------------------------------------------------------+
//| Расчёт границ поля ввода по оси Y                                |
//+------------------------------------------------------------------+
void CTextBox::CalculateYBoundaries(void)
  {
//--- Выйти, если отключен многострочный режим
   if(!m_multi_line_mode)
      return;
//--- Получим Y-координату и смещение по оси Y
   int y       =(int)m_canvas.GetInteger(OBJPROP_YDISTANCE);
   int yoffset =(int)m_canvas.GetInteger(OBJPROP_YOFFSET);

//--- Рассчитаем границы видимой части поля ввода
   m_y_limit  =(y+yoffset)-y;
   m_y2_limit =(y+yoffset+m_y_size-m_scrollh.ScrollWidth())-y;
  }

Чтобы точно спозиционировать полосы прокрутки относительно текущего местоположения курсора, будут использоваться следующие методы: 

class CTextBox : public CElement
  {
private:
   //--- Расчёт X-позиции ползунка полосы прокрутки в левой границе поля ввода
   int               CalculateScrollThumbX(void);
   //--- Расчёт X-позиции ползунка полосы прокрутки в правой границе поля ввода
   int               CalculateScrollThumbX2(void);
   //--- Расчёт Y-позиции ползунка полосы прокрутки в верхней границе поля ввода
   int               CalculateScrollThumbY(void);
   //--- Расчёт Y-позиции ползунка полосы прокрутки в нижней границе поля ввода
   int               CalculateScrollThumbY2(void);
  };
//+------------------------------------------------------------------+
//| Расчёт X-позиции полосы прокрутки в левой границе поля ввода     |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbX(void)
  {
   return(m_text_cursor_x-m_text_x_offset);
  }
//+------------------------------------------------------------------+
//| Расчёт X-позиции полосы прокрутки в правой границе поля ввода    |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbX2(void)
  {
   return((m_multi_line_mode)? m_text_cursor_x-m_x_size+m_scrollv.ScrollWidth()+m_text_x_offset : m_text_cursor_x-m_x_size+m_text_x_offset*2);
  }
//+------------------------------------------------------------------+
//| Расчёт Y-позиции полосы прокрутки в верхней границе поля ввода   |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbY(void)
  {
   return(m_text_cursor_y-m_text_y_offset);
  }
//+------------------------------------------------------------------+
//| Расчёт Y-позиции полосы прокрутки в нижней границе поля ввода    |
//+------------------------------------------------------------------+
int CTextBox::CalculateScrollThumbY2(void)
  {
//--- Установим шрифт для отображения на холсте (нужно, чтобы получить высоту строки)
   m_canvas.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//--- Получим высоту строки
   int line_height=m_canvas.TextHeight("|");
//--- Рассчитать и вернуть значение
   return(m_text_cursor_y-m_y_size+m_scrollh.ScrollWidth()+m_text_y_offset+line_height);
  }

Сделаем так, чтобы по нажатию на текстовое поле ввода генерировалось событие, четко дающее понять, что поле было активировано. Также нам нужно получать событие, соответствующее перемещению текстового курсора в поле ввода. В файл Defines.mqh добавим новые идентификаторы: 

  • ON_CLICK_TEXT_BOX для обозначения события активации текстового поля ввода.
  • ON_MOVE_TEXT_CURSOR для обозначения события перемещения текстового курсора. 
//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
...
#define ON_CLICK_TEXT_BOX          (31) // Активация текстового поля ввода
#define ON_MOVE_TEXT_CURSOR        (32) // Перемещение текстового курсора

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

Рис. 8. Позиция текстового курсора в редакторе кода MetaEditor. 

Рис. 8. Позиция текстового курсора в редакторе кода MetaEditor.

В листинге ниже показан код метода CTextBox::TextCursorInfo(), который возвращает строку в формате, как на скриншоте выше. Также показаны дополнительные методы, с помощью которых можно получить количество строк и символов в указанной строке и текущее положение текстового курсора. 

class CTextBox : public CElement
  {
private:
   //--- Возвращает индекс (1) строки, (2) символа, на котором находится текстовый курсор,
   //    (3) количество строк, (4) количество символов в указанной строке
   uint              TextCursorLine(void)                      { return(m_text_cursor_y_pos);      }
   uint              TextCursorColumn(void)                    { return(m_text_cursor_x_pos);      }
   uint              LinesTotal(void)                          { return(::ArraySize(m_lines));     }
   uint              ColumnsTotal(const uint line_index);
   //--- Информация текстового курсора (строка/количество строк, столбец/количество столбцов)
   string            TextCursorInfo(void);
  };
//+------------------------------------------------------------------+
//| Возвращает количество символов в указанной строке                |
//+------------------------------------------------------------------+
uint CTextBox::ColumnsTotal(const uint line_index)
  {
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Предотвращение выхода из диапазона
   uint check_index=(line_index<lines_total)? line_index : lines_total-1;
//--- Получим размер массива символов в строке
   uint symbols_total=::ArraySize(m_lines[check_index].m_symbol);
//--- Вернуть количество символов
   return(symbols_total);
  }
//+------------------------------------------------------------------+
//| Информация текстового курсора                                    |
//+------------------------------------------------------------------+
string CTextBox::TextCursorInfo(void)
  {
//--- Компоненты для строки
   string lines_total        =(string)LinesTotal();
   string columns_total      =(string)ColumnsTotal(TextCursorLine());
   string text_cursor_line   =string(TextCursorLine()+1);
   string text_cursor_column =string(TextCursorColumn()+1);
//--- Сформируем строку
   string text_box_info="Ln "+text_cursor_line+"/"+lines_total+", "+"Col "+text_cursor_column+"/"+columns_total;
//--- Вернуть строку
   return(text_box_info);
  }

Теперь всё готово к тому, чтобы представить описание метода CTextBox::OnClickTextBox(), о котором упоминалось в самом начале раздела (см. листинг кода ниже). Здесь в самом начале стоит проверка на имя объекта, на котором произошло нажатие левой кнопкой мыши. Если оказалось, что нажатие было не на поле ввода, отправим сообщение об окончании редактирования (идентификатор события ON_END_EDIT) в случае, когда поле ввода ещё активно. Затем дезактивируем поле ввода и выходим из метода.

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

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

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

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

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

И уже в самом конце метода CTextBox::OnClickTextBox() генерируем событие, сигнализирующее о том, что поле ввода было активировано (идентификатор события ON_CLICK_TEXT_BOX), отправив для однозначной идентификации также (1) идентификатор элемента, (2) индекс элемента и — дополнительно — (3) информацию о позиции курсора. 

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на элемент
   bool              OnClickTextBox(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на элемент                                     |
//+------------------------------------------------------------------+
bool CTextBox::OnClickTextBox(const string clicked_object)
  {
//--- Выйдем, если чужое имя объекта
   if(m_canvas.Name()!=clicked_object)
     {
      //--- Отправим сообщение об окончании ввода строки в поле ввода, если поле было активно
      if(m_text_edit_state)
         ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
      //--- Дезактивировать поле ввода
      DeactivateTextBox();
      return(false);
     }
//--- Выйти, если (1) включен режим "Только для чтения" или (2) элемент заблокирован
   if(m_read_only_mode || !m_text_box_state)
      return(true);
//--- Отключим управление графиком
   m_chart.SetInteger(CHART_KEYBOARD_CONTROL,false);
//--- Получим смещение по оси X и Y
   int xoffset=(int)m_canvas.GetInteger(OBJPROP_XOFFSET);
   int yoffset=(int)m_canvas.GetInteger(OBJPROP_YOFFSET);
//--- Определим координаты на поле ввода под курсором мыши
   int x =m_mouse.X()-m_canvas.X()+xoffset;
   int y =m_mouse.Y()-m_canvas.Y()+yoffset;

//--- Получим высоту строки
   int line_height=(int)LineHeight();
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Определим символ нажатия
   for(uint l=0; l<lines_total; l++)
     {
      //--- Зададим начальные координаты для проверки условия
      int x_offset=m_text_x_offset;
      int y_offset=m_text_y_offset+((int)l*line_height);
      //--- Проверка условия по оси Y
      bool y_pos_check=(l<lines_total-1)?(y>=y_offset && y<y_offset+line_height) : y>=y_offset;
      //--- Если нажатие было не на этой строке, перейти к следующей
      if(!y_pos_check)
         continue;
      //--- Получим размер массива символов
      uint symbols_total=::ArraySize(m_lines[l].m_width);
      //--- Если это пустая строка, переместить курсор на указанную позицию и выйти из цикла
      if(symbols_total<1)
        {
         SetTextCursor(0,l);
         HorizontalScrolling(0);
         break;
        }
      //--- Найдём символ, на который нажали
      for(uint s=0; s<symbols_total; s++)
        {
         //--- Если нашли символ, переместить курсор на указанную позицию и выйти из цикла
         if(x>=x_offset && x<x_offset+m_lines[l].m_width[s])
           {
            SetTextCursor(s,l);
            l=lines_total;
            break;
           }
         //--- Прибавить ширину текущего символа для следующей проверки
         x_offset+=m_lines[l].m_width[s];
         //--- Если это последний символ, переместим курсор в конец строки и выйдем из цикла
         if(s==symbols_total-1 && x>x_offset)
           {
            SetTextCursor(s+1,l);
            l=lines_total;
            break;
           }
        }
     }
//--- Если включен режим многострочного поля ввода
   if(m_multi_line_mode)
     {
      //--- Получим границы видимой части поля ввода
      CalculateYBoundaries();
      //--- Получим Y-координату курсора
      CalculateTextCursorY();
      //--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
      if(m_text_cursor_y<=m_y_limit)
         VerticalScrolling(CalculateScrollThumbY());
      else
        {
         if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
            VerticalScrolling(CalculateScrollThumbY2());
        }
     }
//--- Активировать поле ввода
   m_text_edit_state=true;
//--- Обновить текст и курсор
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_CLICK_TEXT_BOX,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }



Ввод символа

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

Для изменения размера массивов во многих методах класса CTextBox будет использоваться достаточно простой метод CTextBox::ArraysResize():

class CTextBox : public CElement
  {
private:
   //--- Устанавливает новый размер массивам свойств указанной строки
   void              ArraysResize(const uint line_index,const uint new_size);
  };
//+------------------------------------------------------------------+
//| Устанавливает новый размер массивам свойств указанной строки     |
//+------------------------------------------------------------------+
void CTextBox::ArraysResize(const uint line_index,const uint new_size)
  {
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Предотвращение выхода из диапазона
   uint l=(line_index<lines_total)? line_index : lines_total-1;
//--- Устанавливаем размер массивам структуры
   ::ArrayResize(m_lines[line_index].m_width,new_size);
   ::ArrayResize(m_lines[line_index].m_symbol,new_size);
  }

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

class CTextBox : public CElement
  {
private:
   //--- Добавляет символ и его свойства в массивы структуры
   void              AddSymbol(const string key_symbol);
  };
//+------------------------------------------------------------------+
//| Добавляет символ и его свойства в массивы структуры              |
//+------------------------------------------------------------------+
void CTextBox::AddSymbol(const string key_symbol)
  {
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Установим новый размер массивам
   ArraysResize(m_text_cursor_y_pos,symbols_total+1);
//--- Сместить все символы от конца массива к индексу добавляемого символа
   for(uint i=symbols_total; i>m_text_cursor_x_pos; i--)
     {
      m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i-1];
      m_lines[m_text_cursor_y_pos].m_width[i]  =m_lines[m_text_cursor_y_pos].m_width[i-1];
     }
//--- Получим ширину символа
   int width=m_canvas.TextWidth(key_symbol);
//--- Добавить символ в освободившийся элемент
   m_lines[m_text_cursor_y_pos].m_symbol[m_text_cursor_x_pos] =key_symbol;
   m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos]  =width;

//--- Увеличить счётчик позиции курсора
   m_text_cursor_x_pos++;
  }

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

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише
   bool              OnPressedKey(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише                                     |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKey(const long key_code)
  {
//--- Выйти, если поле ввода не активировано
   if(!m_text_edit_state)
      return(false);
//--- Получим символ клавиши
   string pressed_key=m_keys.KeySymbol(key_code);
//--- Выйти, если нет символа
   if(pressed_key=="")
      return(false);
//--- Добавим символ и его свойства
   AddSymbol(pressed_key);
//--- Рассчитать размеры поля ввода
   CalculateTextBoxSize();
//--- Установить новый размер полю ввода
   ChangeTextBoxSize(true,true);
//--- Получим границы видимой части поля ввода
   CalculateXBoundaries();
//--- Получим X-координату курсора
   CalculateTextCursorX();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


Обработка нажатия клавиши 'Backspace'

Теперь рассмотрим ситуацию, когда символ удаляется при нажатии на клавишу 'Backspace'. В этом случае в обработчике событий элемента "Многострочное поле ввода" будет вызываться метод CTextBox::OnPressedKeyBackspace(). Для его работы также понадобятся дополнительные методы, которые мы до этого ещё не рассматривали. Сначала будет представлен их код.

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

class CTextBox : public CElement
  {
private:
   //--- Удаляет символ
   void              DeleteSymbol(void);
  };
//+------------------------------------------------------------------+
//| Удаляет символ                                                   |
//+------------------------------------------------------------------+
void CTextBox::DeleteSymbol(void)
  {
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Если массив пустой
   if(symbols_total<1)
     {
      //--- Установим курсор на нулевую позицию текущей строки
      SetTextCursor(0,m_text_cursor_y_pos);
      return;
     }
//--- Получим позицию предыдущего символа
   int check_pos=(int)m_text_cursor_x_pos-1;
//--- Выйти, если выход из диапазона
   if(check_pos<0)
      return;
//--- Сместить все символы на один элемент влево от индекса удаляемого символа к концу массива
   for(uint i=check_pos; i<symbols_total-1; i++)
     {
      m_lines[m_text_cursor_y_pos].m_symbol[i] =m_lines[m_text_cursor_y_pos].m_symbol[i+1];
      m_lines[m_text_cursor_y_pos].m_width[i]  =m_lines[m_text_cursor_y_pos].m_width[i+1];
     }
//--- Уменьшить счётчик позиции курсора
   m_text_cursor_x_pos--;
//--- Установим новый размер массивам
   ArraysResize(m_text_cursor_y_pos,symbols_total-1);
  }

Если текстовый курсор находится в начале строки, и при этом это не первая строка, нужно удалить текущую строку и поднять все нижние строки на одну позицию. Если на удаляемой строке есть символы, то их нужно присоединить к строке, которая находится на одну позицию выше. Для этой операции будет задействован ещё один дополнительный метод — CTextBox::ShiftOnePositionUp(). Понадобится также вспомогательный метод CTextBox::LineCopy(), который немного упростит копирование строк. 

class CTextBox : public CElement
  {
private:
   //--- Делает копию указанной (source) строки в новое место (destination)
   void              LineCopy(const uint destination,const uint source);
  };
//+------------------------------------------------------------------+
//| Устанавливает новый размер массивам свойств указанной строки     |
//+------------------------------------------------------------------+
void CTextBox::LineCopy(const uint destination,const uint source)
  {
   ::ArrayCopy(m_lines[destination].m_width,m_lines[source].m_width);
   ::ArrayCopy(m_lines[destination].m_symbol,m_lines[source].m_symbol);
  }

Ниже представлен код метода CTextBox::ShiftOnePositionUp(). В первом цикле метода осуществляется поднятие на одну позицию вверх всех нижних строк от текущей позиции курсора. На первой итерации нужно проверить, есть ли в этой строке символы, и если есть, запомнить их для присоединения к предыдущей строке. После того, как строки смещены, массив строк уменьшается на один элемент. Текстовый курсор переносится в конец предыдущей строки. 

Последний блок кода метода CTextBox::ShiftOnePositionUp() предназначен для присоединения символов удалённой строки к предыдущей. Если есть строка для присоединения, то с помощью функции ::StringToCharArray() переносим её во временный массив типа uchar, как коды символов. Затем увеличиваем массив текущей строки на количество добавляемых символов. И в качестве завершающей операции поочерёдно в цикле добавляем символы и их свойства в массивы. Преобразование кодов символов из временного массива типа uchar осуществляем с помощью функции ::CharToString()

class CTextBox : public CElement
  {
private:
   //--- Смещает строки на одну позицию вверх
   void              ShiftOnePositionUp(void);
  };
//+------------------------------------------------------------------+
//| Смещает строки на одну позицию вверх                             |
//+------------------------------------------------------------------+
void CTextBox::ShiftOnePositionUp(void)
  {
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Сместим строки от следующего элемента на одну строку вверх
   for(uint i=m_text_cursor_y_pos; i<lines_total-1; i++)
     {
      //--- На первой итерации
      if(i==m_text_cursor_y_pos)
        {
         //--- Получим размер массива символов
         uint symbols_total=::ArraySize(m_lines[i].m_symbol);
         //--- Если есть символы в этой строке, запомним их, чтобы добавить к предыдущей строке
         m_temp_input_string=(symbols_total>0)? CollectString(i) : "";
        }
      //--- Индекс следующего элемента массива строк
      uint next_index=i+1;
      //--- Получим размер массива символов
      uint symbols_total=::ArraySize(m_lines[next_index].m_symbol);
      //--- Установим новый размер массивам
      ArraysResize(i,symbols_total);
      //--- Сделать копию строки
      LineCopy(i,next_index);
     }
//--- Установим новый размер массиву строк
   uint new_size=lines_total-1;
   ::ArrayResize(m_lines,new_size);

//--- Уменьшим счётчик строк
   m_text_cursor_y_pos--;
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Переместим курсор в конец
   m_text_cursor_x_pos=symbols_total;
//--- Получим X-координату курсора
   CalculateTextCursorX();
//--- Если есть строка, которую нужно добавить к предыдущей
   if(m_temp_input_string!="")
     {
      //--- Переносим строку в массив
      uchar array[];
      int total=::StringToCharArray(m_temp_input_string,array)-1;
      //--- Получим размер массива символов
      symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //--- Установим новый размер массивам
      new_size=symbols_total+total;
      ArraysResize(m_text_cursor_y_pos,new_size);
      //--- Добавить данные в массивы структуры
      for(uint i=m_text_cursor_x_pos; i<new_size; i++)
        {
         m_lines[m_text_cursor_y_pos].m_symbol[i] =::CharToString(array[i-m_text_cursor_x_pos]);
         m_lines[m_text_cursor_y_pos].m_width[i]  =m_canvas.TextWidth(m_lines[m_text_cursor_y_pos].m_symbol[i]);
        }
     }
  }

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

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

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише "Backspace"
   bool              OnPressedKeyBackspace(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише "Backspace"                         |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyBackspace(const long key_code)
  {
//--- Выйти, если это не клавиша "Backspace" или поле ввода не активировано
   if(key_code!=KEY_BACKSPACE || !m_text_edit_state)
      return(false);
//--- Удалить символ, если позиция больше нуля
   if(m_text_cursor_x_pos>0)
      DeleteSymbol();
//--- Удалить строку, если нулевая позиция и не первая строка
   else if(m_text_cursor_y_pos>0)
     {
      //--- Сместим строки на одну позицию вверх
      ShiftOnePositionUp();
     }
//--- Рассчитать размеры поля ввода
   CalculateTextBoxSize();
//--- Установить новый размер полю ввода
   ChangeTextBoxSize(true,true);
//--- Получим границы видимой части поля ввода
   CalculateBoundaries();
//--- Получим X- и Y-координату курсора
   CalculateTextCursorX();
   CalculateTextCursorY();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      if(m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
   else
      VerticalScrolling(m_scrollv.CurrentPos());
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


Обработка нажатия клавиши 'Enter'

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

class CTextBox : public CElement
  {
private:
   //--- Очищает указанную строку
   void              ClearLine(const uint line_index);
  };
//+------------------------------------------------------------------+
//| Очищает указанную строку                                         |
//+------------------------------------------------------------------+
void CTextBox::ClearLine(const uint line_index)
  {
   ::ArrayFree(m_lines[line_index].m_width);
   ::ArrayFree(m_lines[line_index].m_symbol);
  }

Теперь опишем подробнее алгоритм метода CTextBox::ShiftOnePositionDown(). В первую очередь, нужно запомнить количество символов той строки, на которой была нажата клавиша 'Enter'. От этого, а также от того, на какой позиции в строке был текстовый курсор, зависит, как будет отработан алгоритм метода CTextBox::ShiftOnePositionDown(). Далее перемещаем текстовый курсор на новую строку и увеличиваем размер массива строк на один элемент. Затем в цикле нужно сместить все строки, начиная с текущей, на одну позицию вниз, начиная с конца массива. На последней итерации, если в строке, на которой была нажата клавиша 'Enter', не было ни одного символа, то нужно очистить строку, на которой сейчас находится текстовый курсор. Очищаемая строка — копия той строки, содержание которой уже находится на следующей строке, как результат смещения на одну позицию вниз.

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

class CTextBox : public CElement
  {
private:
   //--- Смещает строки на одну позицию вниз
   void              ShiftOnePositionDown(void);
  };
//+------------------------------------------------------------------+
//| Смещает строки на одну позицию вниз                              |
//+------------------------------------------------------------------+
void CTextBox::ShiftOnePositionDown(void)
  {
//--- Получим размер массива символов из строки, на которой нажали клавишу "Enter"
   uint pressed_line_symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Увеличим счётчик строк
   m_text_cursor_y_pos++;
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Увеличим массив на один элемент
   uint new_size=lines_total+1;
   ::ArrayResize(m_lines,new_size);
//--- Сместим строки от текущей позиции на один пункт вниз (начиная с конца массива)
   for(uint i=lines_total; i>m_text_cursor_y_pos; 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==m_text_cursor_y_pos && pressed_line_symbols_total<1)
         ClearLine(prev_index);
     }
//--- Если нажатие клавиши "Enter" было не на пустой строке
   if(pressed_line_symbols_total>0)
     {
      //--- Индекс строки, на которой нажали клавишу "Enter"
      uint prev_line_index=m_text_cursor_y_pos-1;
      //--- Массив для копии символов от текущего положения курсора до конца строки
      string array[];
      //--- Установим размер массиву, как количество символов, которые нужно перенести на новую строку
      uint new_line_size=pressed_line_symbols_total-m_text_cursor_x_pos;
      ::ArrayResize(array,new_line_size);
      //--- Скопируем в массив символы, которые нужно перенести на новую строку
      for(uint i=0; i<new_line_size; i++)
         array[i]=m_lines[prev_line_index].m_symbol[m_text_cursor_x_pos+i];

      //--- Установим новый размер массивам структуры в строке, на которой нажали клавишу "Enter"
      ArraysResize(prev_line_index,pressed_line_symbols_total-new_line_size);
      //--- Установим новый размер массивам структуры в новой строке
      ArraysResize(m_text_cursor_y_pos,new_line_size);
      //--- Добавить данные в массивы структуры новой строки
      for(uint k=0; k<new_line_size; k++)
        {
         m_lines[m_text_cursor_y_pos].m_symbol[k] =array[k];
         m_lines[m_text_cursor_y_pos].m_width[k]  =m_canvas.TextWidth(array[k]);
        }

     }
  }

Для обработки нажатия на клавишу 'Enter' теперь есть всё необходимое. Переходим к рассмотрению метода CTextBox::OnPressedKeyEnter(). В самом начале проверяем, нажата ли клавиша 'Enter' и активировано ли поле ввода. Затем, если эти проверки пройдены, то если это однострочное поле ввода, нужно просто закончить работу с ним. Для этого дезактивируем его, отправим событие с идентификатором ON_END_EDIT и выйдем из метода.

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

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише "Enter"
   bool              OnPressedKeyEnter(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише "Enter"                             |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyEnter(const long key_code)
  {
//--- Выйти, если это не клавиша "Enter" или поле ввода не активировано
   if(key_code!=KEY_ENTER || !m_text_edit_state)
      return(false);
//--- Если отключен многострочный режим
   if(!m_multi_line_mode)
     {
      //--- Дезактивировать поле ввода
      DeactivateTextBox();
      //--- Отправим сообщение об этом
      ::EventChartCustom(m_chart_id,ON_END_EDIT,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
      return(false);
     }
//--- Сместим строки на одну позицию вниз
   ShiftOnePositionDown();
//--- Рассчитать размеры поля ввода
   CalculateTextBoxSize();
//--- Установить новый размер полю ввода
   ChangeTextBoxSize();
//--- Получим границы видимой части поля ввода
   CalculateYBoundaries();
//--- Получим Y-координату курсора
   CalculateTextCursorY();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
      VerticalScrolling(CalculateScrollThumbY2());

//--- Переместить курсор в начало строки
   SetTextCursor(0,m_text_cursor_y_pos);
//--- Переместить полосу прокрутки в начало
   HorizontalScrolling(0);
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


Обработка нажатия клавиш 'Left' и 'Right'

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

class CTextBox : public CElement
  {
private:
   //--- Корректировка текстового курсора по оси X
   void              CorrectingTextCursorXPos(const int x_pos=WRONG_VALUE);
  };
//+------------------------------------------------------------------+
//| Корректировка текстового курсора по оси X                        |
//+------------------------------------------------------------------+
void CTextBox::CorrectingTextCursorXPos(const int x_pos=WRONG_VALUE)
  {
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width);
//--- Опеределим позицию курсора
   uint text_cursor_x_pos=0;
//--- Если позиция указана
   if(x_pos!=WRONG_VALUE)
      text_cursor_x_pos=(x_pos>(int)symbols_total-1)? symbols_total : x_pos;
//--- Если позиция не указана, то установим курсор в конец строки
   else
      text_cursor_x_pos=symbols_total;
//--- Нулевая позиция, если в строке нет символов
   m_text_cursor_x_pos=(symbols_total<1)? 0 : text_cursor_x_pos;
//--- Получим X-координату курсора
   CalculateTextCursorX();
  }

В листинге ниже представлен код метода CTextBox::OnPressedKeyLeft() для обработки нажатия клавиши 'Влево'. Программа выйдет из метода, если была нажата какая-то другая клавиша или поле ввода не активировано, а также, если сейчас нажата клавиша 'Ctrl'. Обработку одновременного нажатия с клавишей 'Ctrl' будем рассматривать в другом разделе статьи. 

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

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише "Left"
   bool              OnPressedKeyLeft(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише "Left"                              |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyLeft(const long key_code)
  {
//--- Выйти, если это не клавиша "Left" или нажата клавиша "Ctrl" или поле ввода не активировано
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- Если позиция текстового курсора больше нуля
   if(m_text_cursor_x_pos>0)
     {
      //--- Сместим его на предыдущий символ
      m_text_cursor_x-=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos-1];
      //--- Уменьшим счётчик символов
      m_text_cursor_x_pos--;
     }
   else
     {
      //--- Если это не первая строка
      if(m_text_cursor_y_pos>0)
        {
         //--- Перейдём в конец предыдущей строки
         m_text_cursor_y_pos--;
         CorrectingTextCursorXPos();
        }
     }
//--- Получим границы видимой части поля ввода
   CalculateBoundaries();
//--- Получим Y-координату курсора
   CalculateTextCursorY();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      //--- Получим размер массива символов
      uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //---
      if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

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

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

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише "Right"
   bool              OnPressedKeyRight(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише "Right"                             |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyRight(const long key_code)
  {
//--- Выйти, если это не клавиша "Right" или нажата клавиша "Ctrl" или поле ввода не активировано
   if(key_code!=KEY_RIGHT || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_width);
//--- Если это не конец строки
   if(m_text_cursor_x_pos<symbols_total)
     {
      //--- Сместим позицию текстового курсора на следующий символ
      m_text_cursor_x+=m_lines[m_text_cursor_y_pos].m_width[m_text_cursor_x_pos];
      //--- Увеличим счётчик символов
      m_text_cursor_x_pos++;
     }
   else
     {
      //--- Получим размер массива строк
      uint lines_total=::ArraySize(m_lines);
      //--- Если это не последняя строка
      if(m_text_cursor_y_pos<lines_total-1)
        {
         //--- Переместить курсор в начало следующей строки
         m_text_cursor_x=m_text_x_offset;
         SetTextCursor(0,++m_text_cursor_y_pos);
        }
     }
//--- Получим границы видимой части поля ввода
   CalculateBoundaries();
//--- Получим Y-координату курсора
   CalculateTextCursorY();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
   else
     {
      if(m_text_cursor_x_pos==0)
         HorizontalScrolling(0);
     }
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_y+(int)LineHeight()>=m_y2_limit)
      VerticalScrolling(CalculateScrollThumbY2());
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


Обработка нажатия клавиш 'Up' и 'Down'

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

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

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

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише "Up"
   bool              OnPressedKeyUp(const long key_code);
   //--- Обработка нажатия на клавише "Down"
   bool              OnPressedKeyDown(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише "Up"                                |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyUp(const long key_code)
  {
//--- Выйти, если отключен многострочный режим
   if(!m_multi_line_mode)
      return(false);
//--- Выйти, если это не клавиша "Up" или поле ввода не активировано
   if(key_code!=KEY_UP || !m_text_edit_state)
      return(false);
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Если не выходим за пределы массива
   if(m_text_cursor_y_pos-1<lines_total)
     {
      //--- Переход на предыдущую строку
      m_text_cursor_y_pos--;
      //--- Корректировка текстового курсора по оси X
      CorrectingTextCursorXPos(m_text_cursor_x_pos);
     }
//--- Получим границы видимой части поля ввода
   CalculateBoundaries();
//--- Получим Y-координату курсора
   CalculateTextCursorY();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }



Обработка нажатия клавиш 'Home' и 'End'

Нажатие на клавишах 'Home' и 'End' перемещает текстовый курсор в начало и в конец строки соответственно. Для обработки этих событий предназначены методы CTextBox::OnPressedKeyHome() и CTextBox::OnPressedKeyEnd(). Их код представлен в листинге ниже и он не требует никаких дополнительных пояснений, так как довольно прост и снабжен подробными комментариями. 

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише "Home"
   bool              OnPressedKeyHome(const long key_code);
   //--- Обработка нажатия на клавише "End"
   bool              OnPressedKeyEnd(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише "Home"                              |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyHome(const long key_code)
  {
//--- Выйти, если это не клавиша "Home" или клавиша "Ctrl" нажата или поле ввода не активировано
   if(key_code!=KEY_HOME || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- Переместить курсор в начало текущей строки
   SetTextCursor(0,m_text_cursor_y_pos);
//--- Переместить полосу прокрутки на первую позицию
   HorizontalScrolling(0);
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }
//+------------------------------------------------------------------+
//| Обработка нажатия на клавише "End"                               |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyEnd(const long key_code)
  {
//--- Выйти, если (1) это не клавиша "End" или (2) клавиша "Ctrl" нажата или (3) поле ввода не активировано
   if(key_code!=KEY_END || m_keys.KeyCtrlState() || !m_text_edit_state)
      return(false);
//--- Получим количество символов в текущей строке
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Переместить курсор в конец текущей строки
   SetTextCursor(symbols_total,m_text_cursor_y_pos);
//--- Получим X-координату курсора
   CalculateTextCursorX();
//--- Получим границы видимой части поля ввода
   CalculateXBoundaries();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_x>=m_x2_limit)
      HorizontalScrolling(CalculateScrollThumbX2());
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

 


Обработка одновременного нажатия клавиш в сочетании с клавишей 'Ctrl'

Теперь рассмотрим методы, которые обрабатывают такие одновременные нажатия клавиш:

  • 'Ctrl' + 'Left' – перемещение текстового курсора от слова к слову влево.
  • 'Ctrl' + 'Right' – перемещение текстового курсора от слова к слову вправо.
  • 'Ctrl' + 'Home' – перемещение текстового курсора в начало первой строки. 
  • 'Ctrl' + 'End' – перемещение текстового курсора в конец последней строки.

В качестве примера рассмотрим только один из этих методов — CTextBox::OnPressedKeyCtrlAndLeft(), для перемещения текстового курсора от слова к слову влево. В начале метода стоят проверки на одновременное нажатие клавиш 'Ctrl' и 'Left'. Если какая-то из этих клавиш не нажата, то программа выйдет из метода. Кроме этого, поле ввода должно быть активировано.

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

Затем, как и во всех других методах, проверяется выход текстового курсора за границы видимой области поля ввода и при необходимости корректируются ползунки полос прокрутки. В самом конце поле ввода перерисовывается и отправляется сообщение о том, что текстовый курсор был перемещён.

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише Ctrl + Left
   bool              OnPressedKeyCtrlAndLeft(const long key_code);
   //--- Обработка нажатия на клавише Ctrl + Right
   bool              OnPressedKeyCtrlAndRight(const long key_code);
   //--- Обработка одновременного нажатия клавиш Ctrl + Home
   bool              OnPressedKeyCtrlAndHome(const long key_code);
   //--- Обработка одновременного нажатия клавиш Ctrl + End
   bool              OnPressedKeyCtrlAndEnd(const long key_code);
  };
//+------------------------------------------------------------------+
//| Обработка одновременного нажатия клавиш Ctrl + Left              |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyCtrlAndLeft(const long key_code)
  {
//--- Выйти, если (1) это не клавиша "Left" и (2) клавиша "Ctrl" не нажата или (3) поле ввода не активировано
   if(!(key_code==KEY_LEFT && m_keys.KeyCtrlState()) || !m_text_edit_state)
      return(false);
//--- Символ пробела
   string SPACE=" ";
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Получим количество символов в текущей строке
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Если курсор в начале текущей строки и это не первая строка,
//    переместим курсор в конец предыдущей строки
   if(m_text_cursor_x_pos==0 && m_text_cursor_y_pos>0)
     {
      //--- Получим индекс предыдущей строки
      uint prev_line_index=m_text_cursor_y_pos-1;
      //--- Получим количество символов предыдущей строки
      symbols_total=::ArraySize(m_lines[prev_line_index].m_symbol);
      //--- Переместим курсор в конец предыдущей строки
      SetTextCursor(symbols_total,prev_line_index);
     }
//--- Если курсор не в начале текущей строки или курсор на первой строке
   else
     {
      //--- Найдём начало непрерывной последовательности символов (справа налево)
      for(uint i=m_text_cursor_x_pos; i<=symbols_total; i--)
        {
         //--- Перейти к следующему, если курсор в конце строки
         if(i==symbols_total)
            continue;
         //--- Если это первый символ строки
         if(i==0)
           {
            //--- Установим курсор в начало строки
            SetTextCursor(0,m_text_cursor_y_pos);
            break;
           }
         //--- Если это не первый символ строки
         else
           {
            //--- Если нашли начало непрерывной последовательности, на которой впервые.
            //    Началом считается пробел на следующем индексе.
            if(i!=m_text_cursor_x_pos &&
               m_lines[m_text_cursor_y_pos].m_symbol[i]!=SPACE &&
               m_lines[m_text_cursor_y_pos].m_symbol[i-1]==SPACE)

              {
               //--- Установим курсор в начало новой непрерывной последовательности
               SetTextCursor(i,m_text_cursor_y_pos);
               break;
              }
           }
        }
     }
//--- Получим границы видимой части поля ввода
   CalculateBoundaries();
//--- Получим X-координату курсора
   CalculateTextCursorX();
//--- Получим Y-координату курсора
   CalculateTextCursorY();
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_x<=m_x_limit)
      HorizontalScrolling(CalculateScrollThumbX());
   else
     {
      //--- Получим размер массива символов
      symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
      //---
      if(m_text_cursor_x_pos==symbols_total && m_text_cursor_x>=m_x2_limit)
         HorizontalScrolling(CalculateScrollThumbX2());
     }
//--- Переместить полосу прокрутки, если текстовый курсор вышел из поля видимости
   if(m_text_cursor_y<=m_y_limit)
      VerticalScrolling(CalculateScrollThumbY());
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

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

 


Интеграция элемента в движок библиотеки

Для правильной работы элемента "Многострочное поле ввода" понадобится персональный массив в структуре WindowElements класса CWndContainer. Подключаем файл с классом CTextBox к файлу WndContainer.mqh:

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Controls\TextBox.mqh"

Добавляем в структуру WindowElements персональный массив для нового элемента

//+------------------------------------------------------------------+
//| Класс для хранения всех объектов интерфейса                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Структура массивов элементов
   struct WindowElements
     {
      //--- Многострочные поля ввода
      CTextBox         *m_text_boxes[];
     };
   //--- Массив массивов элементов для каждого окна
   WindowElements    m_wnd[];
  };

Так как элемент типа CTextBox составной и содержит в себе элементы другого типа (в данном случае это полосы прокрутки), то нужен метод, в котором указатели на эти элементы будут распределяться в по своим персональным массивам. В листинге ниже показан код метода CWndContainer::AddTextBoxElements(), который предназначен для этих целей. Вызов этого метода осуществляется там же, где и все подобные методы, то есть в CWndContainer::AddToElementsArray(). 

class CWndContainer
  {
private:
   //--- Сохраняет указатели на объекты многострочного поля ввода
   bool              AddTextBoxElements(const int window_index,CElementBase &object);
  };
//+------------------------------------------------------------------+
//| Сохраняет указатели на объекты многострочного поля ввода         |
//+------------------------------------------------------------------+
bool CWndContainer::AddTextBoxElements(const int window_index,CElementBase &object)
  {
//--- Выйдем, если это не многострочное поле ввода
   if(dynamic_cast<CTextBox *>(&object)==NULL)
      return(false);
//--- Получим указатель на элемент
   CTextBox *tb=::GetPointer(object);
   for(int i=0; i<2; i++)
     {
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      if(i==0)
        {
         //--- Получим указатель полосы прокрутки
         CScrollV *sv=tb.GetScrollVPointer();
         m_wnd[window_index].m_elements[size]=sv;
         AddToObjectsArray(window_index,sv);
         //--- Добавим указатель в персональный массив
         AddToRefArray(sv,m_wnd[window_index].m_scrolls);
        }
      else if(i==1)
        {
         CScrollH *sh=tb.GetScrollHPointer();
         m_wnd[window_index].m_elements[size]=sh;
         AddToObjectsArray(window_index,sh);
         //--- Добавим указатель в персональный массив
         AddToRefArray(sh,m_wnd[window_index].m_scrolls);
        }
     }
//--- Добавим указатель в персональный массив
   AddToRefArray(tb,m_wnd[window_index].m_text_boxes);
   return(true);
  }

Теперь нужно внести некоторое дополнение в метод CWndEvents::OnTimerEvent(). Помним, что перерисовка графического интерфейса производится только когда курсор мыши находится в движении и приостанавливается через некоторое заданное время сразу после прекращения перемещения курсора. Для элемента типа CTextBox нужно сделать исключение. Иначе, когда поле ввода активировано, текстовый курсор не будет мигать. 

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CWndEvents::OnTimerEvent(void)
  {
//--- Выйти, если курсор мыши в состоянии покоя (разница между вызовами >300 ms) и левая кнопка мыши отжата
   if(m_mouse.GapBetweenCalls()>300 && !m_mouse.LeftButtonState())
     {
      int text_boxes_total=CWndContainer::TextBoxesTotal(m_active_window_index);
      for(int e=0; e<text_boxes_total; e++)
         m_wnd[m_active_window_index].m_text_boxes[e].OnEventTimer();

      //---
      return;
     }
//--- Если массив пуст, выйдем  
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- Проверка событий всех элементов по таймеру
   CheckElementsEventsTimer();
//--- Перерисуем график
   m_chart.Redraw();
  }

Далее создадим тестовое MQL-приложение, в котором можно будет протестировать элемент "Многострочное поле ввода". 

 


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

Для теста создадим MQL-приложение с графическим интерфейсом, где будет два текстовых поля ввода. Одно будет однострочным, а второе — многострочным. Кроме этих полей ввода, в графическом интерфейсе приложения для примера будет главное меню с контекстными и статусная строка. Во второй пункт статусной строки будет транслироваться положение текстового курсора многострочного поля ввода.

Создаём два экземпляра класса типа CTextBox и объявляем два метода для создания поля ввода:

class CProgram : public CWndEvents
  {
protected:
   //--- Поля ввода
   CTextBox          m_text_box1;
   CTextBox          m_text_box2;
   //---
protected:
   //--- Поля ввода
   bool              CreateTextBox1(const int x_gap,const int y_gap);
   bool              CreateTextBox2(const int x_gap,const int y_gap);
  };

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

//+------------------------------------------------------------------+
//| Создаёт многострочное поле ввода                                 |
//+------------------------------------------------------------------+
bool CProgram::CreateTextBox2(const int x_gap,const int y_gap)
  {
//--- Сохраним указатель на окно
   m_text_box2.WindowPointer(m_window);
//--- Установим свойства перед созданием
   m_text_box2.FontSize(8);
   m_text_box2.Font("Calibri"); // Consolas|Calibri|Tahoma
   m_text_box2.AreaColor(clrWhite);
   m_text_box2.TextColor(clrBlack);
   m_text_box2.MultiLineMode(true);
   m_text_box2.AutoXResizeMode(true);
   m_text_box2.AutoXResizeRightOffset(2);
   m_text_box2.AutoYResizeMode(true);
   m_text_box2.AutoYResizeBottomOffset(24);

//--- Массив строк
   string lines_array[]=
     {
      "Введение",
      "Группы клавиш и клавиатурные раскладки",
      "Обработка события нажатия клавиш",
      "ASCII-коды символов и управляющих клавиш",
      "Скан-коды клавиш",
      "Вспомогательный класс для работы с клавиатурой",
      "Элемент «Многострочное текстовое поле ввода»",
      "Разработка класса CTextBox для создания элемента",
      "Свойства и внешний вид",
      "Управление текстовым курсором",
      "Ввод символа",
      "Обработка нажатия клавиши «Backspace»",
      "Обработка нажатия клавиши «Enter»",
      "Обработка нажатия клавиш «Left» и «Right»",
      "Обработка нажатия клавиш «Up» и «Down»",
      "Обработка нажатия клавиш «Home» и «End»",
      "Обработка одновременного нажатия клавиш в сочетании с клавишей «Ctrl»",
      "Интеграция элемента в движок библиотеки",
      "Приложение для теста элемента",
      "Заключение"
     };
//--- Добавим текст в текстовое поле
   int lines_total=::ArraySize(lines_array);
   for(int i=0; i<lines_total; i++)
     {
      //--- Добавим текст в первую строку
      if(i==0)
         m_text_box2.AddText(0,lines_array[i]);
      //--- Добавим строку в текстовое поле
      else
         m_text_box2.AddLine(lines_array[i]);
     }
//--- Создадим элемент управления
   if(!m_text_box2.CreateTextBox(m_chart_id,m_subwin,x_gap,y_gap))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_text_box2);
//--- Установка текста в пункты статусной строки
   m_status_bar.ValueToItem(1,m_text_box2.TextCursorInfo());
   return(true);
  }

Для принятия сообщений от полей ввода в обработчик событий MQL-приложения добавим код:

//+------------------------------------------------------------------+
//| Обработчик событий графика                                       |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие (1) ввода значения или (2) активации текстого поля ввода или (3) перемещения текстового курсора
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT ||
      id==CHARTEVENT_CUSTOM+ON_CLICK_TEXT_BOX ||
      id==CHARTEVENT_CUSTOM+ON_MOVE_TEXT_CURSOR)
     {
      ::Print(__FUNCTION__," > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);

      //--- Если идентификаторы совпадают (сообщение от многострочного поля ввода)
      if(lparam==m_text_box2.Id())
        {
         //--- Обновление второго пункта статусной строки
         m_status_bar.ValueToItem(1,sparam);
        }
      //--- Перерисовать график
      m_chart.Redraw();
      return;
     }
  }

После компиляции приложения и загрузки его на график вы увидите следующее:

 Рис. 9. Графический интерфейс с демонстрацией элемента «Текстовое поле ввода»

Рис. 9. Графический интерфейс с демонстрацией элемента «Текстовое поле ввода»

 

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

 


Заключение

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

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

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

 

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

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