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

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

MetaTrader 5Примеры | 19 апреля 2017, 09:34
5 106 152
Anatoli Kazharski
Anatoli Kazharski

Содержание


Введение

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

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

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

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

Отслеживание нажатия клавиши Shift

В первую очередь, дополним рассмотренный ранее класс CKeys, предназначенный для работы с клавиатурой, методом CKeys::KeyShiftState() для определения текущего состояния клавиши Shift. Эта клавиша будет использоваться в различных комбинациях для выделения текста. В листинге ниже показан код этого простого метода. Клавиша Shift считается нажатой, если функция ::TerminalInfoInteger() с идентификатором TERMINAL_KEYSTATE_SHIFT возвращает значение меньше нуля.

//+------------------------------------------------------------------+
//| Класс для работы с клавиатурой                                   |
//+------------------------------------------------------------------+
class CKeys
  {
public:
   //--- Возвращает состояние клавиши Shift
   bool              KeyShiftState(void);
  };
//+------------------------------------------------------------------+
//| Возвращает состояние клавиши Shift                               |
//+------------------------------------------------------------------+
bool CKeys::KeyShiftState(void)
  {
   return(::TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0);
  }



Комбинации клавиш для выделения текста

Рассмотрим все комбинации клавиш для выделения текста, которые будут реализованы в нашем поле ввода. Начнем с сочетаний двух клавиш.

  • Сочетания 'Shift + Left' и 'Shift + Right' смещают текстовый курсор влево и вправо соответственно на один символ. Текст при этом выделяется другим цветом фона и символа (их пользователь может настраивать):

 Рис. 1. Выделение текста со смещением на один символ влево и вправо.

Рис. 1. Выделение текста со смещением на один символ влево и вправо.


  • Сочетания 'Shift + Home' и 'Shift + End' смещают текстовый курсор в начало и конец строки с выделением всех символов от начального положения курсора.

 Рис. 2. Выделение текста со смещением от начального положения курсора до начала и конца строки.

Рис. 2. Выделение текста со смещением от начального положения курсора до начала и конца строки.


  • Сочетания 'Shift + Up' и 'Shift + Down' смещают текстовый курсор вверх и вниз соответственно на одну строку. Для сочетания 'Shift + Up'  текст выделяется на начальной строке от курсора до начала этой строки и до курсора от конца конечной строки. Для 'Shift + Down' поведение симметрично. Если между начальной и конечной выделенными строками есть ещё строки, то текст в них выделяется полностью.

 Рис. 3. Выделение текста со смещением на одну строку вверх и вниз.

Рис. 3. Выделение текста со смещением на одну строку вверх и вниз.


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

В сочетаниях из трёх клавиш, кроме клавиши Shift, используется Ctrl. Рассмотрим все такие комбинации, которые будут реализованы в этой статье:

  • Комбинации 'Ctrl + Shift + Left' и 'Ctrl + Shift + Right' предназначены для выделения текста целыми словами влево и вправо от текущего положения текстового курсора соответственно:

 Рис. 4. Выделение текста со смещением на одно слово влево и вправо.

Рис. 4. Выделение текста со смещением на одно слово влево и вправо.


  • Комбинации клавиш 'Ctrl + Shift + Home' и 'Ctrl + Shift + End' позволяют выделять весь текст до начала первой и конца последней строк от текущего положения текстового курсора:

 Рис. 5. Выделение текста со смещением текстового курсора в начало и конец документа.

Рис. 5. Выделение текста со смещением текстового курсора в начало и конец документа.


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


Методы для выделения текста

По умолчанию выделенный текст отображается белыми символами на синем фоне. При необходимости цвета можно изменить с помощью методов CTextBox:: SelectedBackColor() и CTextBox:: SelectedTextColor(). 

class CTextBox : public CElement
  {
private:
   //--- Цвет фона и символов выделенного текста
   color             m_selected_back_color;
   color             m_selected_text_color;
   //---
private:
   //--- Цвет фона и символов выделенного текста
   void              SelectedBackColor(const color clr)        { m_selected_back_color=clr;       }
   void              SelectedTextColor(const color clr)        { m_selected_text_color=clr;       }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTextBox::CTextBox(void) : m_selected_text_color(clrWhite),
                           m_selected_back_color(C'51,153,255')
  {
//...
  }

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

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

class CTextBox : public CElement
  {
private:
   //--- Начальные и конечные индексы строк и символов (выделенного текста)
   int               m_selected_line_from;
   int               m_selected_line_to;
   int               m_selected_symbol_from;
   int               m_selected_symbol_to;
   //---
private:
   //--- Устанавливает (1) начальные и (2) конечные индексы для выделения текста
   void              SetStartSelectedTextIndexes(void);
   void              SetEndSelectedTextIndexes(void);
   //--- Сбросить выделенный текст
   void              ResetSelectedText(void);
  };
//+------------------------------------------------------------------+
//| Установить начальные индексы для выделения текста                |
//+------------------------------------------------------------------+
void CTextBox::SetStartSelectedTextIndexes(void)
  {
//--- Если начальные индексы для выделения текста ещё не установлены
   if(m_selected_line_from==WRONG_VALUE)
     {
      m_selected_line_from   =(int)m_text_cursor_y_pos;
      m_selected_symbol_from =(int)m_text_cursor_x_pos;
     }
  }
//+------------------------------------------------------------------+
//| Установить конечные индексы для выделения текста                 |
//+------------------------------------------------------------------+
void CTextBox::SetEndSelectedTextIndexes(void)
  {
//--- Установить конечные индексы для выделения текста
   m_selected_line_to   =(int)m_text_cursor_y_pos;
   m_selected_symbol_to =(int)m_text_cursor_x_pos;
//--- Если все индексы равны, то сбросить выделение
   if(m_selected_line_from==m_selected_line_to && m_selected_symbol_from==m_selected_symbol_to)
      ResetSelectedText();
  }
//+------------------------------------------------------------------+
//| Сбросить выделенный текст                                        |
//+------------------------------------------------------------------+
void CTextBox::ResetSelectedText(void)
  {
   m_selected_line_from   =WRONG_VALUE;
   m_selected_line_to     =WRONG_VALUE;
   m_selected_symbol_from =WRONG_VALUE;
   m_selected_symbol_to   =WRONG_VALUE;
  }

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

class CTextBox : public CElement
  {
private:
   //--- Перемещение текстового курсора на один символ влево
   void              MoveTextCursorToLeft(void);
   //--- Перемещение текстового курсора на один символ вправо
   void              MoveTextCursorToRight(void);
   //--- Перемещение текстового курсора на один символ вверх
   void              MoveTextCursorToUp(void);
   //--- Перемещение текстового курсора на один символ вниз
   void              MoveTextCursorToDown(void);

   //--- Корректировка горизонтальной полосы прокрутки
   void              CorrectingHorizontalScrollThumb(void);
   //--- Корректировка вертикальной полосы прокрутки
   void              CorrectingVerticalScrollThumb(void);
  };

Практически весь код в методах обработки нажатия сочетаний клавиш, одна из которых Shift, идентичен, кроме вызова методов для перемещения текстового курсора. Поэтому имеет смысл создать дополнительный метод, в который можно просто передать направление перемещения текстового курсора. В файл Enums.mqh было добавлено перечисление ENUM_MOVE_TEXT_CURSOR с несколькими идентификаторами (см. листинг кода ниже), с помощью которых можно указать, куда нужно переместить текстовый курсор:

  • TO_NEXT_LEFT_SYMBOL — на один символ влево.
  • TO_NEXT_RIGHT_SYMBOL — на один символ вправо.
  • TO_NEXT_LEFT_WORD — на одно слово влево.
  • TO_NEXT_RIGHT_WORD — на одно слово вправо.
  • TO_NEXT_UP_LINE — на одну строку вверх.
  • TO_NEXT_DOWN_LINE — на одну строку вниз.
  • TO_BEGIN_LINE — в начало текущей строки.
  • TO_END_LINE — в конец текущей строки.
  • TO_BEGIN_FIRST_LINE — в начало первой строки.
  • TO_END_LAST_LINE — в конец последней строки.

//+------------------------------------------------------------------+
//| Перечисление по направлению перемещения текстового курсора       |
//+------------------------------------------------------------------+
enum ENUM_MOVE_TEXT_CURSOR
  {
   TO_NEXT_LEFT_SYMBOL  =0,
   TO_NEXT_RIGHT_SYMBOL =1,
   TO_NEXT_LEFT_WORD    =2,
   TO_NEXT_RIGHT_WORD   =3,
   TO_NEXT_UP_LINE      =4,
   TO_NEXT_DOWN_LINE    =5,
   TO_BEGIN_LINE        =6,
   TO_END_LINE          =7,
   TO_BEGIN_FIRST_LINE  =8,
   TO_END_LAST_LINE     =9
  };

Теперь можно создать общий метод для перемещения текстового курсора — CTextBox::MoveTextCursor(), в который достаточно передать один из идентификаторов вышеприведенного списка. Этот же метод теперь будет использоваться практически во всех методах-обработчиках событий нажатия клавиш в элементе управления CTextBox.

class CTextBox : public CElement
  {
private:
   //--- Перемещение текстового курсора в указанном направлении
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction);
  };
//+------------------------------------------------------------------+
//| Перемещение текстового курсора в указанном направлении           |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction)
  {
   switch(direction)
     {
      //--- Переместить курсор на один символ влево
      case TO_NEXT_LEFT_SYMBOL  : MoveTextCursorToLeft();        break;
      //--- Переместить курсор на один символ вправо
      case TO_NEXT_RIGHT_SYMBOL : MoveTextCursorToRight();       break;
      //--- Переместить курсор на одно слово влево
      case TO_NEXT_LEFT_WORD    : MoveTextCursorToLeft(true);    break;
      //--- Переместить курсор на одно слово вправо
      case TO_NEXT_RIGHT_WORD   : MoveTextCursorToRight(true);   break;
      //--- Переместить курсор на одну строку вверх
      case TO_NEXT_UP_LINE      : MoveTextCursorToUp();          break;
      //--- Переместить курсор на одну строку вниз
      case TO_NEXT_DOWN_LINE    : MoveTextCursorToDown();        break;
      //--- Переместить курсор в начало текущей строки
      case TO_BEGIN_LINE : SetTextCursor(0,m_text_cursor_y_pos); break;
      //--- Переместить курсор в конец текущей строки
      case TO_END_LINE :
        {
         //--- Получим количество символов в текущей строке
         uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
         //--- Переместить курсор
         SetTextCursor(symbols_total,m_text_cursor_y_pos);
         break;
        }
      //--- Переместить курсор в начало первой строки
      case TO_BEGIN_FIRST_LINE : SetTextCursor(0,0); break;
      //--- Переместить курсор в конец последней строки
      case TO_END_LAST_LINE :
        {
         //--- Получим количество строк и символов в последней строке
         uint lines_total   =::ArraySize(m_lines);
         uint symbols_total =::ArraySize(m_lines[lines_total-1].m_symbol);
         //--- Переместить курсор
         SetTextCursor(symbols_total,lines_total-1);
         break;
        }
     }
  }

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

Пример с повторяющимся блоком кода в методах для перемещения текстового курсора:

//+------------------------------------------------------------------+
//| Обработка нажатия на клавише 'Left'                              |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyLeft(const long key_code)
  {
//--- Выйти, если (1) это не клавиша 'Left' или (2) нажата клавиша 'Ctrl' или (3) клавиша 'Shift' нажата
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || m_keys.KeyShiftState())
      return(false);
//--- Сброс выделения
   ResetSelectedText();
//--- Сместить текстовый курсор влево на один символ
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- Корректируем полосы прокрутки
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

Пример с повторяющимся блоком кода в методах для выделения текста:

//+------------------------------------------------------------------+
//| Обработка нажатия на клавише Shift + Left                        |
//+------------------------------------------------------------------+
bool CTextBox::OnPressedKeyShiftAndLeft(const long key_code)
  {
//--- Выйти, если (1) это не клавиша 'Left' или (2) клавиша 'Ctrl' нажата или (3) клавиша 'Shift' не нажата
   if(key_code!=KEY_LEFT || m_keys.KeyCtrlState() || !m_keys.KeyShiftState())
      return(false);
//--- Установить начальные индексы для выделения текста
   SetStartSelectedTextIndexes();
//--- Сместить текстовый курсор влево на один символ
   MoveTextCursor(TO_NEXT_LEFT_SYMBOL);
//--- Установить конечные индексы для выделения текста
   SetEndSelectedTextIndexes();
//--- Корректируем полосы прокрутки
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

Реализуем ещё один дополнительный (перегруженный) метод CTextBox::MoveTextCursor(), в который тоже нужно будет передавать идентификатор с направлением перемещения, а также флаг, указывающий, (1) перемещение ли это текстового курсора или (2) выделение текста.

class CTextBox : public CElement
  {
private:
   //--- Перемещение текстового курсора в указанном направлении
   void              MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text);
  };
//+------------------------------------------------------------------+
//| Перемещение текстового курсора в указанном направлении и         |
//| с условием                                                       |
//+------------------------------------------------------------------+
void CTextBox::MoveTextCursor(const ENUM_MOVE_TEXT_CURSOR direction,const bool with_highlighted_text)
  {
//--- Если только перемещение текстового курсора
   if(!with_highlighted_text)
     {
      //--- Сброс выделения
      ResetSelectedText();
      //--- Переместить курсор в начало первой строки
      MoveTextCursor(direction);
     }
//--- Если с выделением текста
   else
     {
      //--- Установить начальные индексы для выделенния текста
      SetStartSelectedTextIndexes();
      //--- Сместить текстовый курсор влево на один символ
      MoveTextCursor(direction);
      //--- Установить конечные индексы для выделения текста
      SetEndSelectedTextIndexes();
     }
//--- Корректируем полосы прокрутки
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
  }

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

class CTextBox : public CElement
  {
private:
   //--- Обработка нажатия на клавише Shift + Left
   bool              OnPressedKeyShiftAndLeft(const long key_code);
   //--- Обработка нажатия на клавише Shift + Right
   bool              OnPressedKeyShiftAndRight(const long key_code);
   //--- Обработка нажатия на клавише Shift + Up
   bool              OnPressedKeyShiftAndUp(const long key_code);
   //--- Обработка нажатия на клавише Shift + Down
   bool              OnPressedKeyShiftAndDown(const long key_code);
   //--- Обработка нажатия на клавише Shift + Home
   bool              OnPressedKeyShiftAndHome(const long key_code);
   //--- Обработка нажатия на клавише Shift + End
   bool              OnPressedKeyShiftAndEnd(const long key_code);

   //--- Обработка нажатия на клавише Ctrl + Shift + Left
   bool              OnPressedKeyCtrlShiftAndLeft(const long key_code);
   //--- Обработка нажатия на клавише Ctrl + Shift + Right
   bool              OnPressedKeyCtrlShiftAndRight(const long key_code);
   //--- Обработка нажатия на клавише Ctrl + Shift + Home
   bool              OnPressedKeyCtrlShiftAndHome(const long key_code);
   //--- Обработка нажатия на клавише Ctrl + Shift + End
   bool              OnPressedKeyCtrlShiftAndEnd(const long key_code);
  };

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

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

  1. Если начальный индекс строки ниже, чем конечный, то символ выделен:
    • Если это конечная строка и символ справа от конечного выделенного
    • Если это начальная строка и символ слева от начального выделенного
    • На промежуточных строках выделены все символы
  2. Если начальный индекс строки выше, чем конечный, то символ выделен:
    • Если это конечная строка и символ слева от конечного выделенного
    • Если начальная строка и символ справа от начального выделенного
    • На промежуточных строках выделены все символы
  3. Если текст выделен только на одной строке, то символ выделен, если находится в обозначенном диапазоне между начальным и конечным индексами символов.

class CTextBox : public CElement
  {
private:
   //--- Проверка наличия выделенного текста
   bool              CheckSelectedText(const uint line_index,const uint symbol_index);
  };
//+------------------------------------------------------------------+
//| Проверка наличия выделенного текста                              |
//+------------------------------------------------------------------+
bool CTextBox::CheckSelectedText(const uint line_index,const uint symbol_index)
  {
   bool is_selected_text=false;
//--- Выйти, если нет выделенного текста
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- Если начальный индекс на строке ниже
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- Конечная строка и символ справа от конечного выделенного
      if((int)line_index==m_selected_line_to && (int)symbol_index>=m_selected_symbol_to)
        { is_selected_text=true; }
      //--- Начальная строка и символ слева от начального выделенного
      else if((int)line_index==m_selected_line_from && (int)symbol_index<m_selected_symbol_from)
        { is_selected_text=true; }
      //--- Промежуточная строка (выделяются все символы)
      else if((int)line_index>m_selected_line_to && (int)line_index<m_selected_line_from)
        { is_selected_text=true; }
     }
//--- Если начальный индекс на строке выше
   else if(m_selected_line_from<m_selected_line_to)
     {
      //--- Конечная строка и символ слева от конечного выделенного
      if((int)line_index==m_selected_line_to && (int)symbol_index<m_selected_symbol_to)
        { is_selected_text=true; }
      //--- Начальная строка и символ справа от начального выделенного
      else if((int)line_index==m_selected_line_from && (int)symbol_index>=m_selected_symbol_from)
        { is_selected_text=true; }
      //--- Промежуточная строка (выделяются все символы)
      else if((int)line_index<m_selected_line_to && (int)line_index>m_selected_line_from)
        { is_selected_text=true; }
     }
//--- Если начальный и конечный индекс на одной строке
   else
     {
      //--- Нашли проверяемую строку
      if((int)line_index>=m_selected_line_to && (int)line_index<=m_selected_line_from)
        {
         //--- Если смещение курсора вправо и символ в выделенном диапазоне
         if(m_selected_symbol_from>m_selected_symbol_to)
           {
            if((int)symbol_index>=m_selected_symbol_to && (int)symbol_index<m_selected_symbol_from)
               is_selected_text=true;
           }
         //--- Если смещение курсора влево и символ в выделенном диапазоне
         else
           {
            if((int)symbol_index>=m_selected_symbol_from && (int)symbol_index<m_selected_symbol_to)
               is_selected_text=true;
           }
        }
     }
//--- Вернуть результат
   return(is_selected_text);
  }

В методе CTextBox::TextOut(), который предназначен для вывода текста, нужно вместо вывода целой строки добавить внутренний цикл с перебором символов строки. В нём определяется, выделен ли проверяемый символ. Если символ выделен, то определяется его цвет, и под ним рисуется закрашенный прямоугольник. Только после этого выводится сам символ

class CTextBox : public CElement
  {
private:
   //--- Вывод текста на холст
   void              TextOut(void);
  };
//+------------------------------------------------------------------+
//| Вывод текста на холст                                            |
//+------------------------------------------------------------------+
void CTextBox::TextOut(void)
  {
//--- Очистить холст
   m_canvas.Erase(AreaColorCurrent());
//--- Получим размер массива строк
   uint lines_total=::ArraySize(m_lines);
//--- Корректировка в случае выхода из диапазона
   m_text_cursor_y_pos=(m_text_cursor_y_pos>=lines_total)? lines_total-1 : m_text_cursor_y_pos;
//--- Получим размер массива символов
   uint symbols_total=::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
//--- Если включен многострочный режим или количество символов больше нуля
   if(m_multi_line_mode || symbols_total>0)
     {
      //--- Получим ширину строки
      int line_width=(int)LineWidth(m_text_cursor_x_pos,m_text_cursor_y_pos);
      //--- Получим высоту строки и пройдёмся по всем строкам в цикле
      int line_height=(int)LineHeight();
      for(uint i=0; i<lines_total; i++)
        {
         //--- Получим координаты для текста
         int x=m_text_x_offset;
         int y=m_text_y_offset+((int)i*line_height);
         //--- Получим размер строки
         uint string_length=::ArraySize(m_lines[i].m_symbol);
         //--- Рисуем текст
         for(uint s=0; s<string_length; s++)
           {
            uint text_color=TextColorCurrent();
            //--- Если есть выделенный текст, определим его цвет, а также цвет фона текущего символа
            if(CheckSelectedText(i,s))
              {
               //--- Цвет выделенного текста
               text_color=::ColorToARGB(m_selected_text_color);
               //--- Рассчитаем координаты для рисования фона
               int x2=x+m_lines[i].m_width[s];
               int y2=y+line_height-1;
               //--- Рисуем цвет фона символа
               m_canvas.FillRectangle(x,y,x2,y2,::ColorToARGB(m_selected_back_color));
              }
            //--- Нарисовать символ
            m_canvas.TextOut(x,y,m_lines[i].m_symbol[s],text_color,TA_LEFT);
            //--- X-координата для следующего символа
            x+=m_lines[i].m_width[s];
           }
        }
     }
//--- Если же многострочный режим отключен и нет ни одного символа, то будет отображаться текст по умолчанию
   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);
     }
  }

Методы для выделения текста реализованы, и вот как это выглядит в готовом приложении:

 Рис. 6. Демонстрация выделения текста в реализованном текстовом поле MQL-приложения.

Рис. 6. Демонстрация выделения текста в реализованном текстовом поле MQL-приложения.


Методы для удаления выделенного текста

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

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

В случаях, когда начальный индекс символа выделенного текста находится слева, то текстовый курсор тоже нужно сместить вправо на количество удаляемых символов.

сlass CTextBox : public CElement
  {
private:
   //--- Удаляет выделенный на одной строке текст
   void              DeleteTextOnOneLine(void);
  };
//+------------------------------------------------------------------+
//| Удаляет выделенный на одной строке текст                         |
//+------------------------------------------------------------------+
void CTextBox::DeleteTextOnOneLine(void)
  {
   int symbols_total     =::ArraySize(m_lines[m_text_cursor_y_pos].m_symbol);
   int symbols_to_delete =::fabs(m_selected_symbol_from-m_selected_symbol_to);
//--- Если начальный индекс символа справа
   if(m_selected_symbol_to<m_selected_symbol_from)
     {
      //--- Сместим символы на освободившееся место в текущей строке
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_from,m_selected_symbol_to);
     }
//--- Если начальный индекс символа слева
   else
     {
      //--- Сместим текстовый курсор влево на количество удаляемых символов
      m_text_cursor_x_pos-=symbols_to_delete;
      //--- Сместим символы на освободившееся место в текущей строке
      MoveSymbols(m_text_cursor_y_pos,m_selected_symbol_to,m_selected_symbol_from);
     }
//--- Уменьшим размер массива текущей строки на извлечённое из неё количество символов
   ArraysResize(m_text_cursor_y_pos,symbols_total-symbols_to_delete);
  }

Для удаления нескольких строк выделенного текста будет использоваться метод CTextBox::DeleteTextOnMultipleLines(). Здесь алгоритм сложнее. Вначале нужно определить:

  • Общее количество символов на начальной и конечной строках
  • Количество промежуточных строк выделенного текста (кроме начальной и конечной строк)
  • Количество символов для удаления на начальной и конечной строках.

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

  • Во временный динамический массив копируются символы для переноса, которые останутся после удаления, с одной строки на другую.
  • Устанавливается новый размер массиву-приёмнику (строке).
  • Добавляются данные в массивы структуры строки-приёмника.
  • Смещаются строки на количество удаляемых строк.
  • Массиву строк устанавливается новый размер (уменьшается на количество удаляемых строк).
  • В случае, если начальная строка выше конечной (выделение текста вниз), то текстовый курсор перемещается на начальные индексы (строки и символа) выделенного текста.

class CTextBox : public CElement
  {
private:
   //--- Удаляет выделенный текст на нескольких строках
   void              DeleteTextOnMultipleLines(void);
  };
//+------------------------------------------------------------------+
//| Удаляет выделенный текст на нескольких строках                   |
//+------------------------------------------------------------------+
void CTextBox::DeleteTextOnMultipleLines(void)
  {
//--- Общее количество символов на начальной и конечной строках
   uint symbols_total_line_from =::ArraySize(m_lines[m_selected_line_from].m_symbol);
   uint symbols_total_line_to   =::ArraySize(m_lines[m_selected_line_to].m_symbol);
//--- Количество промежуточных строк для удаления
   uint lines_to_delete =::fabs(m_selected_line_from-m_selected_line_to);
//--- Количество символов для удаления на начальной и конечной строках
   uint symbols_to_delete_in_line_from =::fabs(symbols_total_line_from-m_selected_symbol_from);
   uint symbols_to_delete_in_line_to   =::fabs(symbols_total_line_to-m_selected_symbol_to);
//--- Если начальная строка ниже конечной
   if(m_selected_line_from>m_selected_line_to)
     {
      //--- Скопируем в массив символы, которые нужно перенести
      string array[];
      CopyWrapSymbols(m_selected_line_from,m_selected_symbol_from,symbols_to_delete_in_line_from,array);
      //--- Установим новый размер строке-приёмнику
      uint new_size=m_selected_symbol_to+symbols_to_delete_in_line_from;
      ArraysResize(m_selected_line_to,new_size);
      //--- Добавить данные в массивы структуры строки-приёмника
      PasteWrapSymbols(m_selected_line_to,m_selected_symbol_to,array);
      //--- Получим размер массива строк
      uint lines_total=::ArraySize(m_lines);
      //--- Сместим строки вверх на количество удаляемых строк
      MoveLines(m_selected_line_to+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- Установим новый размер массиву строк
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
     }
//--- Если начальная строка выше конечной
   else
     {
      //--- Скопируем в массив символы, которые нужно перенести
      string array[];
      CopyWrapSymbols(m_selected_line_to,m_selected_symbol_to,symbols_to_delete_in_line_to,array);
      //--- Установим новый размер строке-приёмнику
      uint new_size=m_selected_symbol_from+symbols_to_delete_in_line_to;
      ArraysResize(m_selected_line_from,new_size);
      //--- Добавить данные в массивы структуры строки-приёмника
      PasteWrapSymbols(m_selected_line_from,m_selected_symbol_from,array);
      //--- Получим размер массива строк
      uint lines_total=::ArraySize(m_lines);
      //--- Сместим строки вверх на количество удаляемых строк
      MoveLines(m_selected_line_from+1,lines_total-lines_to_delete,lines_to_delete,false);
      //--- Установим новый размер массиву строк
      ::ArrayResize(m_lines,lines_total-lines_to_delete);
      //--- Переместить курсор на начальную позицию в выделении
      SetTextCursor(m_selected_symbol_from,m_selected_line_from);
     }
  }

Какой из вышеописанных методов вызвать, определяется в основном методе для удаления текста — CTextBox::DeleteSelectedText(). После того, как выделенный текст будет удалён, сбрасываются значения начальных и конечных индексов. Затем нужно рассчитать заново размеры текстового поля, так как, возможно, изменилось количество строк. Также могла измениться максимальная ширина строки, по которой рассчитывается ширина текстового поля. В конце метода отправляется сообщение о том, что текстовый курсор переместился. Метод возвращает true, если текст был выделен и удалён. Если же при вызове метода оказывается, что выделенного текста нет, то метод возвращает false

class CTextBox : public CElement
  {
private:
   //--- Удаляет выделенный текст
   void              DeleteSelectedText(void);
  };
//+------------------------------------------------------------------+
//| Удаляет выделенный текст                                         |
//+------------------------------------------------------------------+
bool CTextBox::DeleteSelectedText(void)
  {
//--- Выйти, если текст не выделен
   if(m_selected_line_from==WRONG_VALUE)
      return(false);
//--- Если удаляются символы на одной строке
   if(m_selected_line_from==m_selected_line_to)
      DeleteTextOnOneLine();
//--- Если удаляются символы с нескольких строк
   else
      DeleteTextOnMultipleLines();
//--- Сброс выделенного текста
   ResetSelectedText();
//--- Рассчитать размеры поля ввода
   CalculateTextBoxSize();
//--- Установить новый размер полю ввода
   ChangeTextBoxSize();
//--- Корректируем полосы прокрутки
   CorrectingHorizontalScrollThumb();
   CorrectingVerticalScrollThumb();
//--- Обновить текст в поле ввода
   DrawTextAndCursor(true);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_MOVE_TEXT_CURSOR,CElementBase::Id(),CElementBase::Index(),TextCursorInfo());
   return(true);
  }

Метод CTextBox::DeleteSelectedText() вызывается не только при нажатии клавиши Backspace, но также: (1) при вводе нового символа и (2) при нажатии клавиши Enter. В этих случаях текст сначала удаляется, а затем осуществляется действие, соответствующее нажатой клавише.

Вот как это выглядит в готовом приложении:

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

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


Класс для работы с данными изображения

В качестве дополнения в этой статье рассмотрим новый класс (CImage) для работы с данными изображения. Он многократно будет использоваться во многих классах элементов управления библиотеки, где нужно нарисовать картинку. Класс содержится в файле Objects.mqh

Свойства класса:
  • массив пикселей изображения;
  • ширина изображения;
  • высота изображения;
  • путь к файлу изображения.

//+------------------------------------------------------------------+
//| Класс для хранения данных изображения                            |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // Массив пикселей картинки
   uint              m_image_width;  // Ширина изображения
   uint              m_image_height; // Высота изображения
   string            m_bmp_path;     // Путь к файлу изображения
public:
   //--- (1) Размер массива данных, (2) установить/вернуть данные (цвет пикселя)
   uint              DataTotal(void)                              { return(::ArraySize(m_image_data)); }
   uint              Data(const uint data_index)                  { return(m_image_data[data_index]);  }
   void              Data(const uint data_index,const uint data)  { m_image_data[data_index]=data;     }
   //--- Установить/вернуть ширину изображения
   void              Width(const uint width)                      { m_image_width=width;               }
   uint              Width(void)                                  { return(m_image_width);             }
   //--- Установить/вернуть высоту изображения
   void              Height(const uint height)                    { m_image_height=height;             }
   uint              Height(void)                                 { return(m_image_height);            }
   //--- Установить/вернуть путь к изображению
   void              BmpPath(const string bmp_file_path)          { m_bmp_path=bmp_file_path;          }
   string            BmpPath(void)                                { return(m_bmp_path);                }

  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CImage::CImage(void) : m_image_width(0),
                       m_image_height(0),
                       m_bmp_path("")
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CImage::~CImage(void)
  {
  }

Для сохранения изображения и его свойств предназначен метод CImage::ReadImageData(). Этот метод читает изображение по указанному пути и сохраняет его данные.

class CImage
  {
public:
   //--- Читает и сохраняет данные переданного изображения
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| Сохраняет переданную картинку в массив                           |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- Сбросить последнюю ошибку
   ::ResetLastError();
//--- Сохраним путь к изображению
   m_bmp_file_path=bmp_file_path;
//--- Прочитать и сохранить данные изображения
   if(!::ResourceReadImage(m_bmp_file_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > error: ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

Иногда может понадобиться сделать копию изображения такого же типа (CImage). Для этих целей реализован метод CImage::CopyImageData(). В начале метода массиву-приёмнику устанавливается размер массива-источника. Затем в цикле копируются данные из массива-источника в массив-приёмник.

class CImage
  {
public:
   //--- Копирует данные переданного изображения
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| Копирует данные переданного изображения                          |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- Получим размеры массива-приёмника и массива-источника
   uint data_total        =DataTotal();
   uint source_data_total =::GetPointer(array_source).DataTotal();
//--- Изменить размер массива-приёмника
   ::ArrayResize(m_image_data,source_data_total);
//--- Копируем данные
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=::GetPointer(array_source).Data(i);
  }

До этого обновления в классе CCanvasTable использовалась структура для хранения данных изображения. Теперь с наличием класса CImage внесены соответствующие изменения. 


Заключение

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

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

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

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


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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (152)
Vladimir Karputov
Vladimir Karputov | 26 июн. 2017 в 06:48
Konstantin:

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


А если поверх объекта-графика (по периметру) нарисовать прямоугольник и отслеживать изменение его размеров? То есть изменили размер прямоугольника - значит можно изменить размер объекта графика (всё только на уровне предположения, изменение размера объекта-графика не проверял).

Konstantin
Konstantin | 26 июн. 2017 в 07:46
Vladimir Karputov:

А если поверх объекта-графика (по периметру) нарисовать прямоугольник и отслеживать изменение его размеров? То есть изменили размер прямоугольника - значит можно изменить размер объекта графика (всё только на уровне предположения, изменение размера объекта-графика не проверял).

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

Anatoli Kazharski
Anatoli Kazharski | 26 июн. 2017 в 08:30

Чтобы реализовать вертикальное масштабирование в объектах-графиках, нужно использовать вот эти свойства:

Реализуется точно также, как сейчас реализована навигация (горизонтальная прокрутка) в объектах-графиках.

Konstantin
Konstantin | 26 июн. 2017 в 08:38
Anatoli Kazharski:

Чтобы реализовать вертикальное масштабирование в объектах-графиках, нужно использовать вот эти свойства:

Реализуется точно также, как сейчас реализована навигация (горизонтальная прокрутка) в объектах-графиках.


а разве ограничения указанные в справке уже сняли?

Anatoli Kazharski
Anatoli Kazharski | 26 июн. 2017 в 08:40
Konstantin:

а разве ограничения указанные в справке уже сняли?

Те ограничения вообще к другому относятся. 

//---

Пример:

      //--- Режим фиксированного масштаба
      ::ChartSetInteger(m_sub_chart_id[i],CHART_SCALEFIX,true);
      //--- Максимум и минимум
      ::ChartSetDouble(m_sub_chart_id[i],CHART_FIXED_MAX,2.0);
      ::ChartSetDouble(m_sub_chart_id[i],CHART_FIXED_MIN,1.0);
Создание документации на основе исходных кодов MQL5 Создание документации на основе исходных кодов MQL5
В статье рассматривается создание документации к коду на MQL5, начиная с автоматизации простановки необходимых тэгов. Далее описана работа с программой Doxygen, её правильная настройка и получение результатов в различных форматах: в html, в HtmlHelp и в PDF.
Анализ графиков Баланса/Средств по символам и ORDER_MAGIC советников Анализ графиков Баланса/Средств по символам и ORDER_MAGIC советников
С введением хеджинга в MetaTrader 5 появилась отличная возможность одновременной торговли несколькими советниками на одном торговом счёте. При этом возможна ситуация, когда одна стратегия прибыльна, вторая убыточна, а в итоге график прибыли болтается около нуля. В таком случае полезно построить графики Баланса и Средств для каждой торговой стратегии по отдельности.
Прогнозирование рыночных движений с помощью байес-классификации и индикаторов на основе сингулярного спектрального анализа Прогнозирование рыночных движений с помощью байес-классификации и индикаторов на основе сингулярного спектрального анализа
В статье рассматривается идеология и методика построения рекомендательной системы для оперативной торговли на основе объединения возможностей прогнозирования с помощью сингулярного спектрального анализа (ССА) и важного метода машинного обучения, основанного на теореме Байеса.
Волны Вульфа Волны Вульфа
Графический метод, предложенный Биллом Вульфом, позволяет не только выявить фигуру и тем самым определить момент и направление входа, но и спрогнозировать цель, которую должна достигнуть цена, и время ее достижения. В статье описано, как на основе индикатора Зигзаг создать индикатор для поиска волн Вульфа и простой советник, торгующий по его сигналам.