Графические интерфейсы XI: Поля ввода и комбо-боксы в ячейках таблицы (build 15)

Anatoli Kazharski | 20 июля, 2017

Содержание


Введение

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

В очередном обновлении мы займемся элементом "Таблица" (класс CTable). Ранее появилась возможность добавлять в ячейки таблицы чекбоксы и кнопки. Расширим линейку этих элементов полями ввода и комбобоксами. Также добавим в новый билд возможность управлять размерами окна во время работы приложения.


Управление размерами окна

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

Рассмотрим, как это все реализовано в библиотеке. 

Для создания кнопки полноэкранного режима объявлен отдельный экземпляр класса CButton. Для получения указателя на кнопку предназначен публичный метод CButton::GetFullscreenButtonPointer(). По умолчанию эта кнопка отключена на форме. Чтобы её включить, воспользуйтесь методом CButton::FullscreenButtonIsUsed(). 

//+------------------------------------------------------------------+
//| Класс формы для элементов управления                             |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
private:
   //--- Объекты для создания формы
   CButton           m_button_fullscreen;
   //--- Наличие кнопки для разворачивания в полноэкранный режим
   bool              m_fullscreen_button;
   //---
public:
   //--- Возвращает указатели на кнопки формы
   CButton          *GetFullscreenButtonPointer(void)                { return(::GetPointer(m_button_fullscreen)); }
   //--- Использовать кнопку полноэкранного режима
   void              FullscreenButtonIsUsed(const bool state)        { m_fullscreen_button=state;                 }
   bool              FullscreenButtonIsUsed(void)              const { return(m_fullscreen_button);               }
  };

Кнопка полноэкранного режима создается в общем методе CWindow::CreateButtons() (см. листинг кода ниже), где создаются все включенные кнопки формы. Подобно кнопкам для показа всплывающих подсказок и сворачивания формы, кнопка полноэкранного режима может быть использована только в главном окне. 

//+------------------------------------------------------------------+
//| Создаёт кнопки на форме                                          |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Controls\\full_screen.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp"
//---
bool CWindow::CreateButtons(void)
  {
//--- Если тип программы "скрипт", выйдем
   if(CElementBase::ProgramType()==PROGRAM_SCRIPT)
      return(true);
//--- Счётчик, размер, количество
   int i=0,x_size=20;
   int buttons_total=4;
//--- Путь к файлу
   string icon_file="";
//--- Исключение в области захвата
   m_right_limit=0;
//---
   CButton *button_obj=NULL;
//---
   for(int b=0; b<buttons_total; b++)
     {
      ...
      else if(b==1)
        {
         m_button_fullscreen.MainPointer(this);
         //--- Выйти, если (1) кнопка не включена или (2) это диалоговое окно
         if(!m_fullscreen_button || m_window_type==W_DIALOG)
            continue;
         //---
         button_obj=::GetPointer(m_button_fullscreen);
         icon_file="Images\\EasyAndFastGUI\\Controls\\full_screen.bmp";
        }
      ...
     }
//---
   return(true);
  }

Минимальные размеры окна автоматически устанавливаются равными размерам, указанным при создании элемента. Но эти значения можно переопределить. В текущей версии нельзя установить размеры окна меньше, чем 200x200 пикселей. Это контролируется в методе инициализации свойств элемента — CWindow::InitializeProperties(). 

class CWindow : public CElement
  {
private:
   //--- Минимальные размеры окна
   int               m_minimum_x_size;
   int               m_minimum_y_size;
   //---
public:
   //--- Установка минимальных размеров окна
   void              MinimumXSize(const int x_size)                  { m_minimum_x_size=x_size;                   }
   void              MinimumYSize(const int y_size)                  { m_minimum_y_size=y_size;                   }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWindow::CWindow(void) : m_minimum_x_size(0),
                         m_minimum_y_size(0)
  {
...
  }
//+------------------------------------------------------------------+
//| Инициализация свойств                                            |
//+------------------------------------------------------------------+
void CWindow::InitializeProperties(const long chart_id,const int subwin,const string caption_text,const int x_gap,const int y_gap)
  {
...
   m_x_size         =(m_x_size<1)? 200 : m_x_size;
   m_y_size         =(m_y_size<1)? 200 : m_y_size;
...
   m_minimum_x_size =(m_minimum_x_size<200)? m_x_size : m_minimum_x_size;
   m_minimum_y_size =(m_minimum_y_size<200)? m_y_size : m_minimum_y_size;
...
  }

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

class CWindow : public CElement
  {
private:
   //--- Последние координаты и размеры окна перед переводом в полноэкранный размер
   int               m_last_x;
   int               m_last_y;
   int               m_last_x_size;
   int               m_last_y_size;
   bool              m_last_auto_xresize;
   bool              m_last_auto_yresize;
  };

Для обработки нажатия на кнопку полноэкранного режима используется метод CWindow::OnClickFullScreenButton(). В нем сначала идет проверка на идентификатор и индекс элемента, а потом код разделяется на два блока:

В конце метода CWindow::OnClickFullScreenButton() форма перерисовывается:

class CWindow : public CElement
  {
private:
   //--- Статус окна в полноэкранном режиме
   bool              m_is_fullscreen;
   //---
public:
   //--- В полноэкранный размер или в предыдущий размер окна
   bool              OnClickFullScreenButton(const int id=WRONG_VALUE,const int index=WRONG_VALUE);
  };
//+------------------------------------------------------------------+
//| В полноэкранный размер или в предыдущий размер формы             |
//+------------------------------------------------------------------+
bool CWindow::OnClickFullScreenButton(const int id=WRONG_VALUE,const int index=WRONG_VALUE)
  {
//--- Проверить идентификатор и индекс элемента, если был внешний вызов
   int check_id    =(id!=WRONG_VALUE)? id : CElementBase::Id();
   int check_index =(index!=WRONG_VALUE)? index : CElementBase::Index();
//--- Выйти, если индексы не совпадают
   if(check_id!=m_button_fullscreen.Id() || check_index!=m_button_fullscreen.Index())
      return(false);
//--- Если окно не в полноэкранном размере
   if(!m_is_fullscreen)
     {
      //--- Перевести в полноэкранный размер
      m_is_fullscreen=true;
      //--- Получим текущие размеры окна графика
      SetWindowProperties();
      //--- Запомним текущие координаты и размеры формы
      m_last_x            =m_x;
      m_last_y            =m_y;
      m_last_x_size       =m_x_size;
      m_last_y_size       =m_full_height;
      m_last_auto_xresize =m_auto_xresize_mode;
      m_last_auto_yresize =m_auto_yresize_mode;
      //--- Включить автоизменение размеров формы
      m_auto_xresize_mode=true;
      m_auto_yresize_mode=true;
      //--- Развернуть форму на весь график
      ChangeWindowWidth(m_chart.WidthInPixels()-2);
      ChangeWindowHeight(m_chart.HeightInPixels(m_subwin)-3);
      //--- Обновить местоположение
      m_x=m_y=1;
      Moving(m_x,m_y);
      //--- Заменить изображение в кнопке
      m_button_fullscreen.IconFile("Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp");
      m_button_fullscreen.IconFileLocked("Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp");
     }
//--- Если окно в полноэкранном размере
   else
     {
      //--- Перевести в предыдущий размер окна
      m_is_fullscreen=false;
      //--- Отключить автоизменение размеров
      m_auto_xresize_mode=m_last_auto_xresize;
      m_auto_yresize_mode=m_last_auto_yresize;
      //--- Если режим отключен, то установить прежний размер
      if(!m_auto_xresize_mode)
         ChangeWindowWidth(m_last_x_size);
      if(!m_auto_yresize_mode)
         ChangeWindowHeight(m_last_y_size);
      //--- Обновить местоположение
      m_x=m_last_x;
      m_y=m_last_y;
      Moving(m_x,m_y);
      //--- Заменить изображение в кнопке
      m_button_fullscreen.IconFile("Images\\EasyAndFastGUI\\Controls\\full_screen.bmp");
      m_button_fullscreen.IconFileLocked("Images\\EasyAndFastGUI\\Controls\\full_screen.bmp");
     }
//--- Снять фокус с кнопки
   m_button_fullscreen.MouseFocus(false);
   m_button_fullscreen.Update(true);
   return(true);
  }

Метод CWindow::OnClickFullScreenButton() вызывается в обработчике элемента по приходу пользовательского события ON_CLICK_BUTTON

//+------------------------------------------------------------------+
//| Обработчик событий графика                                       |
//+------------------------------------------------------------------+
void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события нажатия на кнопках формы
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      ...
      //--- Проверка полноэкранного режима
      if(OnClickFullScreenButton((uint)lparam,(uint)dparam))
         return;
      ...
      //---
      return;
     }
  }

Результат будет такой:

 Рис. 1. Демонстрация перехода в полноэкранный режим и обратно.


Рис. 1. Демонстрация перехода в полноэкранный режим и обратно.


Чтобы переключаться в полноэкранный вид и обратно двойным нажатием на заголовке окна, теперь достаточно принимать в обработчике элемента это событие двойного нажатия (ON_DOUBLE_CLICK) над заголовком окна.

void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события двойного нажатия на объекте
   if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK)
     {
      //--- Если событие генерировалось на заголовке окна
      if(CursorInsideCaption(m_mouse.X(),m_mouse.Y()))
         OnClickFullScreenButton(m_button_fullscreen.Id(),m_button_fullscreen.Index());
      //---
      return;
     }
  }

Это работает так:

 Рис. 2. Демонстрация перехода в полноэкранный режим двойным нажатием на заголовке.


Рис. 2. Демонстрация перехода в полноэкранный режим двойным нажатием на заголовке.


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

class CWindow : public CElement
  {
private:
   //--- Режим изменения размеров окна
   bool              m_xy_resize_mode;
   //---
public:
   //--- Возможность изменения размеров окна
   bool              ResizeMode(void)                          const { return(m_xy_resize_mode);                  }
   void              ResizeMode(const bool state)                    { m_xy_resize_mode=state;                    }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWindow::CWindow(void) : m_xy_resize_mode(false)
  {
...
  }

Для отслеживания нажатия левой кнопки мыши на границах окна нужен ещё один идентификатор (PRESSED_INSIDE_BORDER) в перечислении ENUM_MOUSE_STATE, которое находится в файле Enums.mqh.

//+------------------------------------------------------------------+
//|                                                        Enums.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Перечисление областей зажатия левой кнопкой мыши                 |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_STATE
  {
   NOT_PRESSED           =0,
   PRESSED_INSIDE        =1,
   PRESSED_OUTSIDE       =2,
   PRESSED_INSIDE_HEADER =3,
   PRESSED_INSIDE_BORDER =4
  };

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

//+------------------------------------------------------------------+
//| Перечисление типов указателей                                    |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_POINTER
  {
   MP_CUSTOM            =0,
   MP_X_RESIZE          =1,
   MP_Y_RESIZE          =2,
   MP_XY1_RESIZE        =3,
   MP_XY2_RESIZE        =4,
   MP_WINDOW_RESIZE     =5,
   MP_X_RESIZE_RELATIVE =6,
   MP_Y_RESIZE_RELATIVE =7,
   MP_X_SCROLL          =8,
   MP_Y_SCROLL          =9,
   MP_TEXT_SELECT       =10
  };

Для создания графического объекта указателя курсора мыши в класс CWindow добавлен метод CreateResizePointer():

class CWindow : public CElement
  {
private:
   bool              CreateResizePointer(void);
  };
//+------------------------------------------------------------------+
//| Создаёт указатель курсора изменения размеров                     |
//+------------------------------------------------------------------+
bool CWindow::CreateResizePointer(void)
  {
//--- Выйти, если режим изменения размеров выключен
   if(!m_xy_resize_mode)
      return(true);
//--- Свойства
   m_xy_resize.XGap(13);
   m_xy_resize.YGap(11);
   m_xy_resize.XSize(23);
   m_xy_resize.YSize(23);
   m_xy_resize.Id(CElementBase::Id());
   m_xy_resize.Type(MP_WINDOW_RESIZE);
//--- Создание элемента
   if(!m_xy_resize.CreatePointer(m_chart_id,m_subwin))
      return(false);
//---
   return(true);
  }

Для изменения размеров окна понадобилось реализовать несколько методов. Рассмотрим их по порядку.

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

class CWindow : public CElement
  {
private:
   //--- Индекс границы для изменения размеров окна
   int               m_resize_mode_index;
   //---
private:
   //--- Возвращает индекс режима для изменения размеров окна
   int               ResizeModeIndex(const int x,const int y);
  };
//+------------------------------------------------------------------+
//| Возвращает индекс режима для изменения размеров окна             |
//+------------------------------------------------------------------+
int CWindow::ResizeModeIndex(const int x,const int y)
  {
//--- Вернуть индекс границы, если уже есть захват
   if(m_resize_mode_index!=WRONG_VALUE && m_mouse.LeftButtonState())
      return(m_resize_mode_index);
//--- Толщина, отступ и индекс границы
   int width  =5;
   int offset =15;
   int index  =WRONG_VALUE;
//--- Проверка фокуса на левой границе
   if(x>0 && x<width && y>m_caption_height+offset && y<m_y_size-offset)
      index=0;
//--- Проверка фокуса на правой границе
   else if(x>m_x_size-width && x<m_x_size && y>m_caption_height+offset && y<m_y_size-offset)
      index=1;
//--- Проверка фокуса на нижней границе
   else if(y>m_y_size-width && y<m_y_size && x>offset && x<m_x_size-offset)
      index=2;
//--- Если индекс получен, отметим область нажатия
   if(index!=WRONG_VALUE)
      m_clamping_area_mouse=PRESSED_INSIDE_BORDER;
//--- Вернуть индекс области
   return(index);
  }

Понадобятся вспомогательные поля класса: для определения точек захвата, сохранения изначальных размеров и последующих расчётов. Когда процесс изменения размеров уже начался, нужно будет генерировать сообщение на формирование списка доступных элементов. Поэтому понадобится и метод для генерации сообщения на восстановление элементов и обнуления служебных полей: CWindow::ZeroResizeVariables().

class CWindow : public CElement
  {
private:
   //--- Переменные связанные с изменением размеров окна
   int               m_x_fixed;
   int               m_size_fixed;
   int               m_point_fixed;
   //---
private:
   //--- Обнуление переменных
   void              ZeroResizeVariables(void);
  };
//+------------------------------------------------------------------+
//| Обнуление переменных связанных с изменением размеров окна        |
//+------------------------------------------------------------------+
void CWindow::ZeroResizeVariables(void)
  {
//--- Выйти, если обнуление уже было
   if(m_point_fixed<1)
      return;
//--- Обнулить
   m_x_fixed     =0;
   m_size_fixed  =0;
   m_point_fixed =0;
//--- Отправим сообщение на восстановление доступных элементов
   ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),1,"");
//--- Отправим сообщение об изменении в графическом интерфейсе
   ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,"");
  }

Чтобы определить, что режим, показ или скрытие указателя курсора мыши готовы для изменения размеров окна, реализован метод CWindow::CheckResizePointer(). Здесь сначала определяем индекс границы методом CWindow::ResizeModeIndex()

Если указатель курсора ещё не показан, то при определённом индексе границы нужно установить соответствующую картинку, скорректировать положение и вывести указатель.

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

Метод CWindow::CheckResizePointer() возвращает true, если граница окна для изменения размеров определена, и false в противном случае.

class CWindow : public CElement
  {
private:
   //--- Проверка готовности для изменения размеров окна
   bool              CheckResizePointer(const int x,const int y);
  };
//+------------------------------------------------------------------+
//| Проверка готовности для изменения размеров окна                  |
//+------------------------------------------------------------------+
bool CWindow::CheckResizePointer(const int x,const int y)
  {
//--- Определим текущий индекс границы
   m_resize_mode_index=ResizeModeIndex(x,y);
//--- Если курсор скрыт
   if(!m_xy_resize.IsVisible())
     {
      //--- Если граница определена
      if(m_resize_mode_index!=WRONG_VALUE)
        {
         //--- Для определения индекса отображаемой картинки указателя курсора мыши
         int index=WRONG_VALUE;
         //--- Если на вертикальных границах
         if(m_resize_mode_index==0 || m_resize_mode_index==1)
            index=0;
         //--- Если на горизонтальных границах
         else if(m_resize_mode_index==2)
            index=1;
         //--- Изменить картинку
         m_xy_resize.ChangeImage(0,index);
         //--- Переместить, перерисовать и показать
         m_xy_resize.Moving(m_mouse.X(),m_mouse.Y());
         m_xy_resize.Update(true);
         m_xy_resize.Reset();
         return(true);
        }
     }
   else
     {
      //--- Переместить указатель
      if(m_resize_mode_index!=WRONG_VALUE)
         m_xy_resize.Moving(m_mouse.X(),m_mouse.Y());
      //--- Скрыть указатель
      else if(!m_mouse.LeftButtonState())
        {
         //--- Скрыть указатель и обнулить переменные
         m_xy_resize.Hide();
         ZeroResizeVariables();
        }
      //--- Обновить график
      m_chart.Redraw();
      return(true);
     }
//---
   return(false);
  }

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

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

class CWindow : public CElement
  {
private:
   //--- Проверка перетаскивания границы окна
   int               CheckDragWindowBorder(const int x,const int y);
  };
//+------------------------------------------------------------------+
//| Проверка перетаскивания границы окна                             |
//+------------------------------------------------------------------+
int CWindow::CheckDragWindowBorder(const int x,const int y)
  {
//--- Для определения расстояния перемещения
   int distance=0;
//--- Если захват границы ещё не осуществлён
   if(m_point_fixed<1)
     {
      //--- Если изменение размера по оси X
      if(m_resize_mode_index==0 || m_resize_mode_index==1)
        {
         m_x_fixed     =m_x;
         m_size_fixed  =m_x_size;
         m_point_fixed =x;
        }
      //--- Если изменение размера по оси Y
      else if(m_resize_mode_index==2)
        {
         m_size_fixed  =m_y_size;
         m_point_fixed =y;
        }
      //--- Отправим сообщение на определение доступных элементов
      ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),0,"");
      //--- Отправим сообщение об изменении в графическом интерфейсе
      ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,"");
      return(0);
     }
//--- Если это левая граница
   if(m_resize_mode_index==0)
      distance=m_mouse.X()-m_x_fixed;
//--- Если это правая граница
   else if(m_resize_mode_index==1)
      distance=x-m_point_fixed;
//--- Если это нижняя граница
   else if(m_resize_mode_index==2)
      distance=y-m_point_fixed;
//--- Вернуть расстояние перемещения
   return(distance);
  }

Полученный результат, возвращенный методом CWindow::CheckDragWindowBorder() передаётся в метод CWindow::CalculateAndResizeWindow(), где рассчитываются координаты и размеры окна относительно его границы

class CWindow : public CElement
  {
private:
   //--- Расчёт и изменение размеров окна
   void              CalculateAndResizeWindow(const int distance);
  };
//+------------------------------------------------------------------+
//| Расчёт и изменение размеров окна                                 |
//+------------------------------------------------------------------+
void CWindow::CalculateAndResizeWindow(const int distance)
  {
//--- Левая граница
   if(m_resize_mode_index==0)
     {
      int new_x      =m_x_fixed+distance-m_point_fixed;
      int new_x_size =m_size_fixed-distance+m_point_fixed;
      //--- Выйти, если превышаем ограничения
      if(new_x<1 || new_x_size<=m_minimum_x_size)
         return;
      //--- Координаты
      CElementBase::X(new_x);
      m_canvas.X_Distance(new_x);
      //--- Установить и запомнить размер
      CElementBase::XSize(new_x_size);
      m_canvas.XSize(new_x_size);
      m_canvas.Resize(new_x_size,m_canvas.YSize());
     }
//--- Правая граница
   else if(m_resize_mode_index==1)
     {
      int gap_x2     =m_chart_width-m_mouse.X()-(m_size_fixed-m_point_fixed);
      int new_x_size =m_size_fixed+distance;
      //--- Выйти, если превышаем ограничения
      if(gap_x2<1 || new_x_size<=m_minimum_x_size)
         return;
      //--- Установить и запомнить размер
      CElementBase::XSize(new_x_size);
      m_canvas.XSize(new_x_size);
      m_canvas.Resize(new_x_size,m_canvas.YSize());
     }
//--- Нижняя граница
   else if(m_resize_mode_index==2)
     {
      int gap_y2=m_chart_height-m_mouse.Y()-(m_size_fixed-m_point_fixed);
      int new_y_size=m_size_fixed+distance;
      //--- Выйти, если превышаем ограничения
      if(gap_y2<2 || new_y_size<=m_minimum_y_size)
         return;
      //--- Установить и запомнить размер
      m_full_height=new_y_size;
      CElementBase::YSize(new_y_size);
      m_canvas.YSize(new_y_size);
      m_canvas.Resize(m_canvas.XSize(),new_y_size);
     }
  }

Методы CWindow::CheckDragWindowBorder() и CWindow::CheckDragWindowBorder() вызываются в методе CWindow::UpdateSize(). Здесь в начале метода стоит проверка, зажата ли сейчас левая кнопка мыши. Если кнопка отжата, то все значения переменных, связанных с изменением размеров окна, сбрасываются, и программа выходит из метода.

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

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

class CWindow : public CElement
  {
private:
   //--- Обновление размеров окна
   void              UpdateSize(const int x,const int y);
  };
//+------------------------------------------------------------------+
//| Обновление размеров окна                                         |
//+------------------------------------------------------------------+
void CWindow::UpdateSize(const int x,const int y)
  {
//--- Если закончили и левая кнопка мыши отжата, сбросим значения
   if(!m_mouse.LeftButtonState())
     {
      ZeroResizeVariables();
      return;
     }
//--- Выйти, если захват и перемещение границы ещё не началось
   int distance=0;
   if((distance=CheckDragWindowBorder(x,y))==0)
      return;
//--- Расчёт и изменение размеров окна
   CalculateAndResizeWindow(distance);
//--- Перерисовать окно
   Update(true);
//--- Обновить положение объектов
   Moving(m_x,m_y);
//--- Сообщение о том, что размеры окна были изменены
   if(m_resize_mode_index==2)
      ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_YSIZE,(long)CElementBase::Id(),0,"");
   else
      ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_XSIZE,(long)CElementBase::Id(),0,"");
  }

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

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

class CWindow : public CElement
  {
private:
   //--- Управляет размерами окна
   void              ResizeWindow(void);
  };
//+------------------------------------------------------------------+
//| Управляет размерами окна                                         |
//+------------------------------------------------------------------+
void CWindow::ResizeWindow(void)
  {
//--- Выйти, если окно недоступно
   if(!IsAvailable())
      return;
//--- Выйти, если кнопка мыши была нажата не над границей формы
   if(m_clamping_area_mouse!=PRESSED_INSIDE_BORDER && m_clamping_area_mouse!=NOT_PRESSED)
      return;
//--- Выйти, если (1) режим изменения размеров окна отключен или 
//    (2) окно в полноэкранном размере или (3) окно минимизировано
   if(!m_xy_resize_mode || m_is_fullscreen || m_is_minimized)
      return;
//--- Координаты
   int x =m_mouse.RelativeX(m_canvas);
   int y =m_mouse.RelativeY(m_canvas);
//--- Проверка готовности для изменения ширины списков
   if(!CheckResizePointer(x,y))
      return;
//--- Обновление размеров окна
   UpdateSize(x,y);
  }

Метод CWindow::ResizeWindow() вызывается в обработчике событий по приходу события перемещения курсора мыши (CHARTEVENT_MOUSE_MOVE). 

Это работает так:

 Рис. 3. Демонстрация изменения размеров окна методом перемещения его границ.


Рис. 3. Демонстрация изменения размеров окна методом перемещения его границ.



Поля ввода и комбобоксы в ячейках таблицы

Если в ячейках таблицы есть разные элементы управления, она становится очень гибким инструментом для управления содержащимися в ней данными. Ближайший пример можно увидеть прямо в торговых терминалах MetaTrader в окнах настроек MQL-приложений на вкладке «Входные параметры» или в окне «Тестер стратегий» на вкладке «Параметры». Графические интерфейсы с такими возможностями выведут MQL-приложения на новый уровень.

 Рис. 4. Окно настроек в MQL-программе.


Рис. 4. Окно настроек в MQL-программе.


 Рис. 5. Настройки MQL-приложения в тестере стратегий.


Рис. 5. Настройки MQL-приложения в тестере стратегий.


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

Прежде всего в перечисление ENUM_TYPE_CELL в файле Enums.mqh добавлены два новых идентификатора для обозначения типов ячейки таблицы:

//+------------------------------------------------------------------+
//| Перечисление типов ячейки таблицы                                |
//+------------------------------------------------------------------+
enum ENUM_TYPE_CELL
  {
   CELL_SIMPLE   =0,
   CELL_BUTTON   =1,
   CELL_CHECKBOX =2,
   CELL_COMBOBOX =3,
   CELL_EDIT     =4
  };

Для реализации задуманного нам достаточно создать в таблице только один элемент поля ввода (CTextEdit) и/или один элемент комбобокса (CComboBox), как составные части элемента CTable. Появляться они будут при двойном щелчке по ячейке, когда нужно изменить значение в ней. 

При установке типа ячейки с помощью метода CTable::CellType() надо один раз установить флаг в специальных полях класса, если указывается тип CELL_EDIT или CELL_COMBOBOX.

//+------------------------------------------------------------------+
//| Класс для создания нарисованной таблицы                          |
//+------------------------------------------------------------------+
class CTable : public CElement
  {
private:
   //--- Наличие ячеек с полями ввода и комбо-боксами
   bool              m_edit_state;
   bool              m_combobox_state;
   //---
public:
   //--- Установка/получение типа ячейки
   void              CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type);
  };
//+------------------------------------------------------------------+
//| Устанавливает тип ячейки                                         |
//+------------------------------------------------------------------+
void CTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type)
  {
//--- Проверка на выход из диапазона
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- Установить тип ячейки
   m_columns[column_index].m_rows[row_index].m_type=type;
//--- Признак наличия поля ввода
   if(type==CELL_EDIT && !m_edit_state)
      m_edit_state=true;
//--- Признак наличия комбо-бокса
   else if(type==CELL_COMBOBOX && !m_combobox_state)
      m_combobox_state=true;
  }

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

class CTable : public CElement
  {
private:
   //--- Объекты для создания таблицы
   CTextEdit         m_edit;
   CComboBox         m_combobox;
   //---
private:
   bool              CreateEdit(void);
   bool              CreateCombobox(void);
   //---
public:
   //--- Возвращает указатели на элементы
   CTextEdit        *GetTextEditPointer(void)                { return(::GetPointer(m_edit));     }
   CComboBox        *GetComboboxPointer(void)                { return(::GetPointer(m_combobox)); }
  };

При обработке двойного нажатия левой кнопкой мыши на таблице вызывается метод CTable::CheckCellElement(). В него внесены соответствующие дополнения для ячеек типа CELL_EDIT и CELL_COMBOBOX. В листинге ниже показана сокращённая версия этого метода. Методы для обработки разных типов ячеек будут подробнее рассмотрены ниже.

//+------------------------------------------------------------------+
//| Проверяет, был ли при нажатии задействован элемент в ячейке      |
//+------------------------------------------------------------------+
bool CTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false)
  {
//--- Выйти, если в ячейке нет элемента управления
   if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE)
      return(false);
//---
   switch(m_columns[column_index].m_rows[row_index].m_type)
     {
      ...
      //--- Если это ячейка с полем ввода
      case CELL_EDIT :
        {
         if(!CheckPressedEdit(column_index,row_index,double_click))
            return(false);
         //---
         break;
        }
      //--- Если это ячейка с комбо-боксом
      case CELL_COMBOBOX :
        {
         if(!CheckPressedCombobox(column_index,row_index,double_click))
            return(false);
         //---
         break;
        }
     }
//---
   return(true);
  }

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

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

//+------------------------------------------------------------------+
//| Класс для создания многострочного текстового поля                |
//+------------------------------------------------------------------+
class CTextBox : public CElement
  {
private:
   //--- Режим авто выделения текста
   bool              m_auto_selection_mode;
   //---
public:
   //--- Режим для автоматического выделения текста
   void              AutoSelectionMode(const bool state)       { m_auto_selection_mode=state;     }
  };

Чтобы полностью выделить текст в поле ввода, реализован приватный метод CTextBox::SelectAllText(). Здесь сначала получаем количество символов первой строки и устанавливаем индексы для выделения текста. Далее видимую текстовую область нужно сместить до конца вправо. Последним действием нужно переместить текстовый курсор в конец строки.

class CTextBox : public CElement
  {
private:
   //--- Выделить весь текст
   void              SelectAllText(void);
  };
//+------------------------------------------------------------------+
//| Выделить весь текст                                              |
//+------------------------------------------------------------------+
void CTextBox::SelectAllText(void)
  {
//--- Получим размер массива символов
   int symbols_total=::ArraySize(m_lines[0].m_symbol);
//--- Установить индексы для выделения текста
   m_selected_line_from   =0;
   m_selected_line_to     =0;
   m_selected_symbol_from =0;
   m_selected_symbol_to   =symbols_total;
//--- Переместить ползунок горизонтальной полосы прокрутки на последнюю позицию
   HorizontalScrolling();
//--- Переместить курсор в конец строки
   SetTextCursor(symbols_total,0);
  }

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

class CTextBox : public CElement
  {
public:
   //--- Активация поля ввода
   void              ActivateTextBox(void);
  };
//+------------------------------------------------------------------+
//| Активация поля ввода                                             |
//+------------------------------------------------------------------+
void CTextBox::ActivateTextBox(void)
  {
   OnClickTextBox(m_textbox.Name());
  }

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

class CTextBox : public CElement
  {
public:
   //--- Изменение размеров
   void              ChangeSize(const uint x_size,const uint y_size);
  };
//+------------------------------------------------------------------+
//| Изменение размеров                                               |
//+------------------------------------------------------------------+
void CTextBox::ChangeSize(const uint x_size,const uint y_size)
  {
//--- Установить новый размер
   ChangeMainSize(x_size,y_size);
//--- Рассчитать размеры поля ввода
   CalculateTextBoxSize();
//--- Установить новый размер полю ввода
   ChangeTextBoxSize();
  }

Двойным нажатием на ячейке с полем ввода вызывается метод CTable::CheckPressedEdit(). Чтобы потом обработать событие окончания ввода значения (ON_END_EDIT) здесь также понадобятся поля класса для сохранения индексов последней редактируемой ячейки

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

class CTable : public CElement
  {
private:
   //--- Индексы столбца и ряда последней редактированной ячейки
   int               m_last_edit_row_index;
   int               m_last_edit_column_index;
   //---
private:
   //--- Проверяет, было ли нажатие на ячейке с полем ввода
   bool              CheckPressedEdit(const int column_index,const int row_index,const bool double_click=false);
  };
//+------------------------------------------------------------------+
//| Проверяет, было ли нажатие на поле ввода в ячейке                |
//+------------------------------------------------------------------+
bool CTable::CheckPressedEdit(const int column_index,const int row_index,const bool double_click=false)
  {
//--- Выйти, если это не двойной клик
   if(!double_click)
      return(false);
//--- Сохранить индексы
   m_last_edit_row_index    =row_index;
   m_last_edit_column_index =column_index;
//--- Сдвиг по двум осям
   int x_offset=(int)m_table.GetInteger(OBJPROP_XOFFSET);
   int y_offset=(int)m_table.GetInteger(OBJPROP_YOFFSET);
//--- Установить новые координаты
   m_edit.XGap(m_columns[column_index].m_x-x_offset);
   m_edit.YGap(m_rows[row_index].m_y+((m_show_headers)? m_header_y_size : 0)-y_offset);
//--- Размеры
   int x_size =m_columns[column_index].m_x2-m_columns[column_index].m_x+1;
   int y_size =m_cell_y_size+1;
//--- Установить размер
   m_edit.GetTextBoxPointer().ChangeSize(x_size,y_size);
//--- Установить значение из ячейки таблицы
   m_edit.SetValue(m_columns[column_index].m_rows[row_index].m_full_text);
//--- Активировать поле ввода
   m_edit.GetTextBoxPointer().ActivateTextBox();
//--- Установить фокус
   m_edit.GetTextBoxPointer().MouseFocus(true);
//--- Показать поле ввода
   m_edit.Reset();
//--- Перерисовать график
   m_chart.Redraw();
   return(true);
  }

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

class CTable : public CElement
  {
private:
   //--- Обработка окончания ввода значения в ячейку
   bool              OnEndEditCell(const int id);
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   ...
//--- Обработка события окончания ввода
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      if(OnEndEditCell((int)lparam))
         return;
      //---
      return;
     }
   ...
  }
//+------------------------------------------------------------------+
//| Обработка окончания ввода значения в ячейку                      |
//+------------------------------------------------------------------+
bool CTable::OnEndEditCell(const int id)
  {
//--- Выйти, если (1) идентификаторы не совпадают или (2) ячеек с полями ввода нет
   if(id!=CElementBase::Id() || !m_edit_state)
      return(false);
//--- Установить новое значение в ячейку таблицы
   SetValue(m_last_edit_column_index,m_last_edit_row_index,m_edit.GetValue(),0,true);
   Update();
//--- Деактивировать и скрыть поле ввода
   m_edit.GetTextBoxPointer().DeactivateTextBox();
   m_edit.Hide();
   m_chart.Redraw();
   return(true);
  }

При клике мышью вне активированного поля ввода это поле должно скрываться. Для этого понадобится метод CTable::OnEndEditCell(). Кроме того, поле ввода нужно деактивировать, чтобы при следующем вызове оно отобразилось корректно. Метод CTable::OnEndEditCell() вызывается в обработчике таблицы по событию изменения состояния левой кнопки мыши (ON_CHANGE_MOUSE_LEFT_BUTTON). По такому же принципу работает и метод CTable::CheckAndHideCombobox() для проверки комбобокса в ячейках. Не будем приводить здесь код этого метода, так как он практически идентичен рассмотренному.

class CTable : public CElement
  {
private:
   //--- Проверка элементов в ячейках на скрытие
   void              CheckAndHideEdit(void);
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   ...
//--- Изменение состояния левой кнопки мыши
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON)
     {
      ...
      //--- Проверка поля ввода  в ячейках на скрытие
      CheckAndHideEdit();
      //--- Проверка комбо-бокса в ячейках на скрытие
      CheckAndHideCombobox();
      return;
     }
   ...
  }
//+------------------------------------------------------------------+
//| Проверка поля ввода в ячейках на скрытие                         |
//+------------------------------------------------------------------+
void CTable::CheckAndHideEdit(void)
  {
//--- Выйти, если (1) поля ввода нет или (2) оно скрыто
   if(!m_edit_state || !m_edit.IsVisible())
      return;
//--- Проверим фокус
   m_edit.GetTextBoxPointer().CheckMouseFocus();
//--- Деактивировать и скрыть поле ввода, если оно (1) вне фокуса и (2) кнопка мыши нажата
   if(!m_edit.GetTextBoxPointer().MouseFocus() && m_mouse.LeftButtonState())
     {
      m_edit.GetTextBoxPointer().DeactivateTextBox();
      m_edit.Hide();
      m_chart.Redraw();
     }
  }

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

class CTable : public CElement
  {
private:
   //--- Свойства ячеек таблицы
   struct CTCell
     {
      ...
      string            m_value_list[];   // Массив значений (для ячеек с комбобоксами)
      int               m_selected_item;  // Выбранный пункт в списке комбобокса
      ...
     };
  };

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

Для этого служит метод CTable::AddValueList(). Также в этот метод передаются индексы ячейки и индекс пункта, который нужно выбрать в списке комбобокса. По умолчанию выбран первый пункт (индекс 0). 

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

class CTable : public CElement
  {
public:
   //--- Добавить список значений в комбобокс
   void              AddValueList(const uint column_index,const uint row_index,const string &array[],const uint selected_item=0);
  };
//+------------------------------------------------------------------+
//| Добавить список значений в комбобокс                            |
//+------------------------------------------------------------------+
void CTable::AddValueList(const uint column_index,const uint row_index,const string &array[],const uint selected_item=0)
  {
//--- Проверка на выход из диапазона
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- Установим размер списку указанной ячейки
   uint total=::ArraySize(array);
   ::ArrayResize(m_columns[column_index].m_rows[row_index].m_value_list,total);
//--- Сохраним переданные значения
   ::ArrayCopy(m_columns[column_index].m_rows[row_index].m_value_list,array); 
//--- Проверка индекса выбранного пункта в списке
   uint check_item_index=(selected_item>=total)? total-1 : selected_item;
//--- Сохранить выбранный пункт в списке
   m_columns[column_index].m_rows[row_index].m_selected_item=(int)check_item_index;
//--- Сохранить текст выбранного пункта в ячейке
   m_columns[column_index].m_rows[row_index].m_full_text=array[check_item_index];
  }

В методе CTable::CheckPressedCombobox() обрабатывается двойное нажатие на ячейке с комбобоксом. Здесь сначала сохраняются индексы ячейки для последующей обработки в случае выбора пункта в списке. Затем для комбобокса устанавливаются координаты по верхнему левому углу ячейки. После этого его элементам устанавливаются такие же размеры, как у ячейки. Для изменения размеров кнопки (CButton) и списка (CListView) во время выполнения программы в их классы, как и для поля ввода, был добавлен метод ChangeSize(). Так как размер списка в каждой ячейке может быть разным, то каждый раз нужно перестраивать и заполнять список заново. Далее элементы комбобокса перерисовываются, и он делается видимым. В самом конце метода генерируется событие об изменении в графическом интерфейсе

class CTable : public CElement
  {
private:
   //--- Проверяет, было ли нажатие на ячейке с комбобоксом
   bool              CheckPressedCombobox(const int column_index,const int row_index,const bool double_click=false);
  };
//+------------------------------------------------------------------+
//| Проверяет, было ли нажатие на комбобоксе в ячейке               |
//+------------------------------------------------------------------+
bool CTable::CheckPressedCombobox(const int column_index,const int row_index,const bool double_click=false)
  {
//--- Выйти, если это не двойной клик
   if(!double_click)
      return(false);
//--- Сохранить индексы
   m_last_edit_row_index    =row_index;
   m_last_edit_column_index =column_index;
//--- Сдвиг по двум осям
   int x_offset=(int)m_table.GetInteger(OBJPROP_XOFFSET);
   int y_offset=(int)m_table.GetInteger(OBJPROP_YOFFSET);
//--- Установить новые координаты
   m_combobox.XGap(m_columns[column_index].m_x-x_offset);
   m_combobox.YGap(m_rows[row_index].m_y+((m_show_headers)? m_header_y_size : 0)-y_offset);
//--- Установить размер кнопке
   int x_size =m_columns[column_index].m_x2-m_columns[column_index].m_x+1;
   int y_size =m_cell_y_size+1;
   m_combobox.GetButtonPointer().ChangeSize(x_size,y_size);
//--- Установить размер списку
   y_size=m_combobox.GetListViewPointer().YSize();
   m_combobox.GetListViewPointer().ChangeSize(x_size,y_size);
//--- Установить размер списка ячейки
   int total=::ArraySize(m_columns[column_index].m_rows[row_index].m_value_list);
   m_combobox.GetListViewPointer().Rebuilding(total);
//--- Установить список из ячейки
   for(int i=0; i<total; i++)
      m_combobox.GetListViewPointer().SetValue(i,m_columns[column_index].m_rows[row_index].m_value_list[i]);
//--- Установить пункт из ячейки
   int index=m_columns[column_index].m_rows[row_index].m_selected_item;
   m_combobox.SelectItem(index);
//--- Обновить элемент
   m_combobox.GetButtonPointer().MouseFocus(true);
   m_combobox.GetButtonPointer().Update(true);
   m_combobox.GetListViewPointer().Update(true);
//--- Показать поле ввода
   m_combobox.Reset();
//--- Перерисовать график
   m_chart.Redraw();
//--- Отправим сообщение об изменении в графическом интерфейсе
   ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,"");
   return(true);
  }

Событие выбора пункта в списке комбобокса (ON_CLICK_COMBOBOX_ITEM) обрабатывается методом CTable::OnClickComboboxItem(). Здесь сначала проверяется соответствие идентификаторов и наличие комбобокса в таблице. Если эти проверки пройдены, то в ячейке по ранее сохранённым индексам устанавливается индекс выбранного пункта и значение из пункта.

class CTable : public CElement
  {
private:
   //--- Обработка выбора пункта в выпадающем списке ячейки
   bool              OnClickComboboxItem(const int id);
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   ...
//--- Обработка события выбора пункта в списке
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
     {
      if(OnClickComboboxItem((int)lparam))
         return;
      //---
      return;
     }
   ...
  }
//+------------------------------------------------------------------+
//| Обработка выбора пункта в комбобоксе ячейки                     |
//+------------------------------------------------------------------+
bool CTable::OnClickComboboxItem(const int id)
  {
//--- Выйти, если (1) идентификаторы не совпадают или (2) ячеек с комбобоксом нет
   if(id!=CElementBase::Id() || !m_combobox_state)
      return(false);
//--- Индексы последней редактируемой ячейки
   int c=m_last_edit_column_index;
   int r=m_last_edit_row_index;
//--- Запомним в ячейке индекс выбранного пункта
   m_columns[c].m_rows[r].m_selected_item=m_combobox.GetListViewPointer().SelectedItemIndex();
//--- Установить новое значение в ячейку таблицы
   SetValue(c,r,m_combobox.GetValue(),0,true);
   Update();
   return(true);
  }

Работать это все в итоге будет так:

Рис. 6. Демонстрация работы с полями ввода и комбо-боксами в ячейках таблицы. 

Рис. 6. Демонстрация работы с полями ввода и комбобоксами в ячейках таблицы.



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

Для тестов создано MQL-приложение с графическим интерфейсом, в котором есть элементы таблица (CTable) и многострочное поле ввода (CTextBox). В первом столбце таблицы во всех ячейках есть элемент чекбокс (CELL_CHECKBOX). Во втором столбце ячейки имеют тип «поле ввода» (CELL_EDIT). В третьем столбце ячейкам поочерёдно установлены типы «Комбобокс» (CELL_COMBOBOX) и «Поле ввода»  (CELL_EDIT). В пятом столбце ячейки имеют тип «Кнопка» (CELL_BUTTON). В обработчике событий пользовательского класса MQL-приложения события таблицы будут обрабатываться и выводиться в многострочное поле ввода. 

Работает это всё так:

 Рис. 7. MQL-приложение для тестов проделанной работы.


Рис. 7. MQL-приложение для тестов проделанной работы.


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


Заключение

Мы добавили в таблицу возможность создавать ячейки типа «Поле ввода» и «Комбобокс». Форму для элементов управления можно развёртывать на весь экран или вручную уточнять её размеры перетаскиванием границ.

Общая схема библиотеки на текущем этапе разработки:

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


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


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

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