Графические интерфейсы V: Элемент "Комбинированный список" (Глава 3)

Anatoli Kazharski | 16 мая, 2016

Содержание


 

Введение

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

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

 


Элемент «Комбинированный список»

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

В списке ниже перечислены все объекты-примитивы, из которых будет собираться элемент «Комбо-бокс».

  1. Фон элемента
  2. Надпись (описание элемента)
  3. Кнопка
  4. Признак выпадающего списка

Рис. 1. Составные части элемента «Комбо-бокс».

Рис. 1. Составные части элемента «Комбо-бокс».


Далее рассмотрим разработку класса для создания этого элемента.

 

 

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

Последовательно разберём все этапы разработки элемента управления «комбо-бокс», чтобы в последующем эту статью можно было использовать как пример для создания своих подобных классов. Сначала создайте mqh-файл (ComboBox.mqh) и подключите к нему все необходимые файлы с классами, которые понадобятся для создания комбинированного списка. В данном случае это будут три файла с классами:

//+------------------------------------------------------------------+
//|                                                     ComboBox.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "ListView.mqh"

Далее в файле ComboBox.mqh нужно создать класс CComboBox и стандартные для каждого элемента библиотеки методы:

//+------------------------------------------------------------------+
//| Класс для создания комбинированного списка                       |
//+------------------------------------------------------------------+
class CComboBox : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //---
public:
                     CComboBox(void);
                    ~CComboBox(void);
   //--- Сохраняет указатель формы
   void              WindowPointer(CWindow &object)                   { m_wnd=::GetPointer(object);                      }
   //---
public:
   //--- Обработчик событий графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Таймер
   virtual void      OnEventTimer(void);
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y);
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Установка, (2) сброс приоритетов на нажатие левой кнопки мыши
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
   //--- Сбросить цвет
   virtual void      ResetColors(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CComboBox::CComboBox(void)
  {
//--- Сохраним имя класса элемента в базовом классе
   CElement::ClassName(CLASS_NAME);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CComboBox::~CComboBox(void)
  {
  }

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

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

class CComboBox : public CElement
  {
private:
   //--- Свойства комбо-бокса:
   //    Цвет общего фона
   color             m_area_color;
   //--- Текст и отступы текстовой метки
   string            m_label_text;
   int               m_label_x_gap;
   int               m_label_y_gap;
   //--- Цвета текстовой метки в разных состояниях
   color             m_label_color;
   color             m_label_color_off;
   color             m_label_color_hover;
   color             m_label_color_array[];
   //--- (1) Текст кнопки и (2) её размеры
   string            m_button_text;
   int               m_button_x_size;
   int               m_button_y_size;
   //--- Цвета кнопки в разных состояниях
   color             m_button_color;
   color             m_button_color_off;
   color             m_button_color_hover;
   color             m_button_color_pressed;
   color             m_button_color_array[];
   //--- Цвета рамки кнопки в разных состояних
   color             m_button_border_color;
   color             m_button_border_color_off;
   //--- Цвет текста кнопки в разных состояних
   color             m_button_text_color;
   color             m_button_text_color_off;
   //--- Отступы ярлыка
   int               m_drop_arrow_x_gap;
   int               m_drop_arrow_y_gap;
   //--- Ярлыки кнопки с выпадающим меню в активном и заблокированном состоянии
   string            m_drop_arrow_file_on;
   string            m_drop_arrow_file_off;
   //--- Приоритеты на нажатие левой кнопкой мыши
   int               m_area_zorder;
   int               m_button_zorder;
   int               m_zorder;
   //---
public:
   //--- (1) Цвет фона, (2) устанавливает и (3) возвращает значение текстовой метки
   void              AreaColor(const color clr)                       { m_area_color=clr;                                }
   void              LabelText(const string label_text)               { m_label_text=label_text;                         }
   string            LabelText(void)                            const { return(m_label_text);                            }
   //--- Отступы текстовой метки
   void              LabelXGap(const int x_gap)                       { m_label_x_gap=x_gap;                             }
   void              LabelYGap(const int y_gap)                       { m_label_y_gap=y_gap;                             }
   //--- (1) Возвращает текст кнопки, (2) установка размеров кнопки
   string            ButtonText(void)                           const { return(m_button_text);                           }
   void              ButtonXSize(const int x_size)                    { m_button_x_size=x_size;                          }
   void              ButtonYSize(const int y_size)                    { m_button_y_size=y_size;                          }
   //--- (1) Цвет фона, (2) цвета текстовой метки
   void              LabelColor(const color clr)                      { m_label_color=clr;                               }
   void              LabelColorOff(const color clr)                   { m_label_color_off=clr;                           }
   void              LabelColorHover(const color clr)                 { m_label_color_hover=clr;                         }
   //--- Цвета кнопки
   void              ButtonBackColor(const color clr)                 { m_button_color=clr;                              }
   void              ButtonBackColorOff(const color clr)              { m_button_color_off=clr;                          }
   void              ButtonBackColorHover(const color clr)            { m_button_color_hover=clr;                        }
   void              ButtonBackColorPressed(const color clr)          { m_button_color_pressed=clr;                      }
   //--- Цвета рамки кнопки
   void              ButtonBorderColor(const color clr)               { m_button_border_color=clr;                       }
   void              ButtonBorderColorOff(const color clr)            { m_button_border_color_off=clr;                   }
   //--- Цвета текста кнопки
   void              ButtonTextColor(const color clr)                 { m_button_text_color=clr;                         }
   void              ButtonTextColorOff(const color clr)              { m_button_text_color_off=clr;                     }
   //--- Установка ярлыков для кнопки с выпадающим меню в активном и заблокированном состояниях
   void              DropArrowFileOn(const string file_path)          { m_drop_arrow_file_on=file_path;                  }
   void              DropArrowFileOff(const string file_path)         { m_drop_arrow_file_off=file_path;                 }
   //--- Отступы ярлыка
   void              DropArrowXGap(const int x_gap)                   { m_drop_arrow_x_gap=x_gap;                        }
   void              DropArrowYGap(const int y_gap)                   { m_drop_arrow_y_gap=y_gap;                        }
  };

Инициализация всех перечисленных выше свойств значениями по умолчанию размещена в конструкторе класса: 

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CComboBox::CComboBox(void) : m_area_color(C'15,15,15'),
                             m_label_text("combobox: "),
                             m_label_x_gap(0),
                             m_label_y_gap(2),
                             m_label_color(clrWhite),
                             m_label_color_off(clrGray),
                             m_label_color_hover(C'85,170,255'),
                             m_button_text(""),
                             m_button_y_size(18),
                             m_button_text_color(clrBlack),
                             m_button_text_color_off(clrDarkGray),
                             m_button_color(clrGainsboro),
                             m_button_color_off(clrLightGray),
                             m_button_color_hover(C'193,218,255'),
                             m_button_color_pressed(C'153,178,215'),
                             m_button_border_color(clrWhite),
                             m_button_border_color_off(clrWhite),
                             m_drop_arrow_x_gap(16),
                             m_drop_arrow_y_gap(1),
                             m_drop_arrow_file_on(""),
                             m_drop_arrow_file_off("")
  {
//--- Установим приоритеты на нажатие левой кнопки мыши
   m_zorder        =0;
   m_area_zorder   =1;
   m_button_zorder =2;
  }

Комбо-бокс создается с помощью пяти приватных методов, которые будут вызываться в главном публичном методе CComboBox::CreateComboBox(). Для получения доступа к настройке свойств списка и его полосы прокрутки создадим методы для получения указателей на эти элементы: 

class CComboBox : public CElement
  {
private:
   //--- Объекты для создания комбо-бокса
   CRectLabel        m_area;
   CLabel            m_label;
   CEdit             m_button;
   CBmpLabel         m_drop_arrow;
   CListView         m_listview;
   //---
public:
   //--- Методы для создания комбо-бокса
   bool              CreateComboBox(const long chart_id,const int subwin,const int x,const int y);
   //---
private:
   bool              CreateArea(void);
   bool              CreateLabel(void);
   bool              CreateButton(void);
   bool              CreateDropArrow(void);
   bool              CreateList(void);
   //---
public:
   //--- Возвращает указатели на (1) список и (2) полосу прокрутки
   CListView        *GetListViewPointer(void)                         { return(::GetPointer(m_listview));                }
   CScrollV         *GetScrollVPointer(void)                          { return(m_listview.GetScrollVPointer());          }
  };

Из методов, которые представлены в листинге кода выше, рассмотрим подробнее только метод для создания списка CComboBox::CreateList(). Во всех остальных нет ничего особенного, что бы не рассматривалось ранее в предыдущих статьях этой серии. Но перед этим нужно внести некоторые дополнения в класс списка CListView

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

class CListView : public CElement
  {
private:
   //--- Указатель на элемент, управляющий видимостью списка
   CElement         *m_combobox;
   //---
public:
   //--- Сохраняет указатель на комбо-бокс
   void              ComboBoxPointer(CElement &object)                   { m_combobox=::GetPointer(object); }
  };

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

Ниже показана сокращённая версия метода CListView::CreateListView(): 

//+------------------------------------------------------------------+
//| Создаёт список                                                   |
//+------------------------------------------------------------------+
bool CListView::CreateListView(const long chart_id,const int window,const int x,const int y)
  {
//--- Выйти, если нет указателя на форму

//--- Если список выпадающий — значит, нужен указатель на комбо-бокс, к которому он будет привязан
   if(CElement::IsDropdown())
     {
      //--- Выйти, если нет указателя на комбо-бокс
      if(::CheckPointer(m_combobox)==POINTER_INVALID)
        {
         ::Print(__FUNCTION__," > Перед созданием выпадающего списка классу нужно передать "
                 "указатель на комбо-бокс: CListView::ComboBoxPointer(CElement &object)");
         return(false);
        }
     }
//--- Инициализация переменных
//--- Отступы от крайней точки
//--- Создание кнопки
//--- Скрыть элемент, если окно диалоговое или оно минимизировано
//---
   return(true);
  }

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

CComboBox::CComboBox(void)
  {
//--- Режим выпадающего списка
   m_listview.IsDropdown(true);
  }

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

//+------------------------------------------------------------------+
//| Создаёт список                                                   |
//+------------------------------------------------------------------+
bool CComboBox::CreateList(void)
  {
//--- Сохраним указатели на форму и комбо-бокс
   m_listview.WindowPointer(m_wnd);
   m_listview.ComboBoxPointer(this);
//--- Координаты
   int x=CElement::X2()-m_button_x_size;
   int y=CElement::Y()+m_button_y_size;
//--- Установим свойства
   m_listview.Id(CElement::Id());
   m_listview.XSize(m_button_x_size);
//--- Создадим элемент управления
   if(!m_listview.CreateListView(m_chart_id,m_subwin,x,y))
      return(false);
//--- Скрыть список
   m_listview.Hide();
   return(true);
  }

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

class CListView : public CElement
  {
public:
   //--- Установка (1) размера списка (количество пунктов) и (2) видимой его части
   void              ItemsTotal(const int items_total)                { m_listview.ListSize(items_total);                }
   void              VisibleItemsTotal(const int visible_items_total) { m_listview.VisibleListSize(visible_items_total); }
   
   //--- Сохраняет переданное значение в списке по указанному индексу
   void              ValueToList(const int item_index,const string item_text);
  };
//+------------------------------------------------------------------+
//| Сохраняет переданное значение в списке по указанному индексу     |
//+------------------------------------------------------------------+
void CComboBox::ValueToList(const int item_index,const string item_text)
  {
   m_listview.ValueToList(item_index,item_text);
  }

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

class CListView : public CElement
  {
public:
   //--- Выделение пункта по указанному индексу
   void              SelectedItemByIndex(const int index);
  };
//+------------------------------------------------------------------+
//| Выделение пункта по указанному индексу                           |
//+------------------------------------------------------------------+
void CComboBox::SelectedItemByIndex(const int index)
  {
//--- Выбрать пункт в списке
   m_listview.SelectedItemByIndex(index);
//--- Запомнить и установить текст в кнопке
   m_button_text=m_listview.SelectedItemText();
   m_button.Description(m_listview.SelectedItemText());
  }

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

class CListView : public CElement
  {
public:
   //--- Получение и установка состояния элемента
   bool              ComboBoxState(void)                        const { return(m_combobox_state);                        }
   void              ComboBoxState(const bool state);
  };
//+------------------------------------------------------------------+
//| Изменение состояния комбо-бокса                                  |
//+------------------------------------------------------------------+
void CComboBox::ComboBoxState(const bool state)
  {
   m_combobox_state=state;
//--- Установить цвета объектам согласно текущему состоянию
   m_label.Color((state)? m_label_color : m_label_color_off);
   m_button.Color((state)? m_button_text_color : m_button_text_color_off);
   m_button.BackColor((state)? m_button_color : m_button_color_off);
   m_button.BorderColor((state)? m_button_border_color : m_button_border_color_off);
   m_drop_arrow.State(state);
  }

При наведении курсора мыши изменение цвета объектов элемента, с помощью метода CComboBox::ChangeObjectsColor(), будет осуществляться только тогда, когда элемент доступен

class CListView : public CElement
  {
public:
   //--- Изменение цвета объекта при наведении курсора
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| Изменение цвета объекта при наведении курсора                    |
//+------------------------------------------------------------------+
void CComboBox::ChangeObjectsColor(void)
  {
//--- Выйти, если элемент заблокирован
   if(!m_combobox_state)
      return;
//--- Изменить цвет объектов
   CElement::ChangeObjectColor(m_label.Name(),CElement::MouseFocus(),OBJPROP_COLOR,m_label_color,m_label_color_hover,m_label_color_array);
   CElement::ChangeObjectColor(m_button.Name(),CElement::MouseFocus(),OBJPROP_BGCOLOR,m_button_color,m_button_color_hover,m_button_color_array);
  }

Вызывать метод CComboBox::ChangeObjectsColor() нужно в таймере элемента CComboBox::OnEventTimer(). Забегая немного вперёд, замечу, что комбо-бокс может быть составной частью более сложного и при этом выпадающего элемента. В качестве такого примера в одной следующих статей будет «Выпадающий календарь», составной частью которого является комбо-бокс. Поэтому условия для изменения цвета в таймере для этого элемента будут формироваться следующим образом (см. листинг кода ниже):

  1. Если элемент выпадающий и список скрыт.
  2. Если первое условие не исполняется, то проверяем доступность формы и самого элемента. 

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CComboBox::OnEventTimer(void)
  {
//--- Если элемент выпадающий и список скрыт
   if(CElement::IsDropdown() && !m_listview.IsVisible())
      ChangeObjectsColor();
   else
     {
      //--- Если форма и элемент не заблокированы
      if(!m_wnd.IsLocked() && m_combobox_state)
         ChangeObjectsColor();
     }
  }

Для управления видимостью списка комбо-бокса создадим метод CComboBox::ChangeComboboxListState(), который изменяет текущее состояние комбо-бокса на противоположное. В самом начале будет стоять проверка на доступность элемента. Если комбо-бокс заблокирован, то программа выйдет из метода. Далее код разделяется на две ветки:

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

class CListView : public CElement
  {
public:
   //--- Изменяет текущее состояние комбо-бокса на противоположное
   void              ChangeComboBoxListState(void);
  };
//+------------------------------------------------------------------+
//| Изменяет текущее состояние комбо-бокса на противоположное        |
//+------------------------------------------------------------------+
void CComboBox::ChangeComboBoxListState(void)
  {
//--- Выйти, если элемент заблокирован
   if(!m_combobox_state)
      return;
//--- Если список виден
   if(m_listview.IsVisible())
     {
      //--- Скрыть список
      m_listview.Hide();
      //--- Установить цвета
      m_label.Color(m_label_color_hover);
      m_button.BackColor(m_button_color_hover);
      //--- Если элемент не выпадающий
      if(!CElement::IsDropdown())
        {
         //--- Разблокировать форму
         m_wnd.IsLocked(false);
         m_wnd.IdActivatedElement(WRONG_VALUE);
        }
     }
//--- Если список скрыт
   else
     {
      //--- Показать список
      m_listview.Show();
      //--- Установить цвета
      m_label.Color(m_label_color_hover);
      m_button.BackColor(m_button_color_pressed);
      //--- Заблокировать форму
      m_wnd.IsLocked(true);
      m_wnd.IdActivatedElement(CElement::Id());
     }
  }


 


Методы для обработки событий элемента

Теперь можно перейти к настройке обработчика событий элемента CComboBox::OnEvent(). Здесь понадобятся два вспомогательных приватных (private) метода:

class CListView : public CElement
  {
private:
   //--- Обработка нажатия на кнопку
   bool              OnClickButton(const string clicked_object);
   //--- Проверка нажатой левой кнопки мыши над кнопкой комбо-бокса
   void              CheckPressedOverButton(void);
  };

Код метода CComboBox::OnClickButton() совсем прост. В нём будет только проверка на имя объекта, на котором произведено нажатие, и вызов ранее рассмотренного метода CComboBox::ChangeComboBoxListState(), который должен управлять видимостью списка комбо-бокса (см. листинг кода ниже). 

//+------------------------------------------------------------------+
//| Нажатие на кнопку комбо-бокса                                    |
//+------------------------------------------------------------------+
bool CComboBox::OnClickButton(const string clicked_object)
  {
//--- Выйдем, если чужое имя объекта  
   if(clicked_object!=m_button.Name())
      return(false);
//--- Изменить состояние списка
   ChangeComboboxListState();
   return(true);
  }

В листинге ниже представлен код метода CComboBox::CheckPressedOverButton(). В начале метода стоит проверка на доступность формы и идентификатор элемента активатора. Программа выходит из метода, если форма заблокирована и идентификаторы не совпадают. 

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

(1) список скрывается,

(2) восстанавливаются цвета  объектов элемента,

и в конце этого блока кода, если идентификаторы совпадают и элемент не выпадающий, (3) нужно разблокировать форму. То есть, напомню, разблокировать форму может только тот элемент, который его заблокировал.

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

//+------------------------------------------------------------------+
//| Проверка нажатой левой кнопки мыши над кнопкой                   |
//+------------------------------------------------------------------+
void CComboBox::CheckPressedOverButton(void)
  {
//--- Выйти, если форма заблокирована и идентификаторы не совпадают
   if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      return;
//--- Если фокуса нет
   if(!CElement::MouseFocus())
     {
      //--- Выйти, если фокус не на списке или полоса прокрутки списка в действии
      if(m_listview.MouseFocus() || m_listview.ScrollState())
         return;
      //--- Скрыть список
      m_listview.Hide();
      //--- Восстановить цвета
      ResetColors();
      //--- Если идентификаторы совпадают и элемент не выпадающий
      if(m_wnd.IdActivatedElement()==CElement::Id() && !CElement::IsDropdown())
         //--- Разблокировать форму
         m_wnd.IsLocked(false);
     }
//--- Если фокус есть
   else
     {
      //--- Выйти, если список виден
      if(m_listview.IsVisible())
         return;
      //--- Установить цвет с учётом фокуса
      if(m_button.MouseFocus())
         m_button.BackColor(m_button_color_pressed);
      else
         m_button.BackColor(m_button_color_hover);
     }
  }

Вызов метода CComboBox::OnClickButton() нужно разместить в блоке обработки события нажатия на графическом объекте, которое можно определить по идентификатору CHARTEVENT_OBJECT_CLICK

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CComboBox::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события нажатия левой кнопки мыши на объекте
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Нажатие на кнопке комбо-бокса
      if(OnClickButton(sparam))
         return;
     }
  }

В блоке обработки события перемещения курсора мыши (CHARTEVENT_MOUSE_MOVE) перед вызовом метода CComboBox::CheckPressedOverButton() нужно пройти проверки на:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CComboBox::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события перемещения курсора
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      if(!CElement::IsVisible())
         return;
      //--- Координаты
      int  x=(int)lparam;
      int  y=(int)dparam;
      //--- Проверка фокуса над элементами
      CElement::MouseFocus(x>CElement::X() && x<CElement::X2() && 
                           y>CElement::Y() && y<CElement::Y2());
      m_button.MouseFocus(x>m_button.X() && x<m_button.X2() && 
                          y>m_button.Y() && y<m_button.Y2());
      //--- Выйти, если элемент заблокирован
      if(!m_combobox_state)
         return;
      //--- Выйти, если левая кнопка мыши отжата
      if(sparam=="0")
         return;
      //--- Проверка нажатой левой кнопки мыши над сдвоенной кнопкой
      CheckPressedOverButton();
      return;
     }
  }

В момент нажатия на одном из пунктов списка генерируется пользовательское событие ON_CLICK_LIST_ITEM (см. листинг кода ниже). В обработчике событий комбо-бокса нужно получить это сообщение и, если идентификаторы элементов совпадают (то есть, сообщение пришло от списка закреплённого за этим комбо-боксом), то в кнопку комбо-бокса сохраняем текст выделенного пункта списка и скрываем список с помощью метода CComboBox::ChangeComboBoxListState().

Для связи с разрабатываемым приложением это же сообщение с идентификатором ON_CLICK_LIST_ITEM можно принять и в пользовательском классе CProgram. Но можно вдогонку отправить сообщение и от комбо-бокса с уникальным для него (1) идентификатором события, (2) идентификатором элемента и (3) описанием комбо-бокса. Сделаем и такой вариант для расширения возможностей идентификации событий элементов управления. В файл Defines.mqh добавим уникальный идентификатор для элемента управления «Комбинированный список». 

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_CLICK_COMBOBOX_ITEM    (17) // Выбор пункта в списке комбобокса

В этом случае в обработчике событий элемента нужно добавить строку, выделенную в коде ниже синим маркером

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CComboBox::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события нажатия на пункте списка
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      //--- Если идентификаторы совпадают
      if(lparam==CElement::Id())
        {
         //--- Запомнить и установить текст в кнопку
         m_button_text=m_listview.SelectedItemText();
         m_button.Description(m_listview.SelectedItemText());
         //--- Изменить состояние списка
         ChangeComboBoxListState();
         //--- Отправим сообщение об этом
         ::EventChartCustom(m_chart_id,ON_CLICK_COMBOBOX_ITEM,CElement::Id(),0,m_label_text);
        }
      //---
      return;
     }
  }

Можно ещё настроить скрытие списка при изменении свойств графика. Для этого нужно обрабатывать событие с идентификатором CHARTEVENT_CHART_CHANGE (см. листинг кода ниже): 

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CComboBox::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события изменения свойств графика
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- Выйти, если элемент заблокирован
      if(!m_combobox_state)
         return;
      //--- Скрыть список
      m_listview.Hide();
      //--- Восстановить цвета
      ResetColors();
      //--- Разблокировать форму
      m_wnd.IsLocked(false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
      return;
     }
  }

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

 

 


Связывание класса элемента с движком библиотеки

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

1. Подключить файл с классом элемента к корневому файлу библиотеки WndContainer.mqh.

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "ComboBox.mqh"

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

class CWndContainer
  {
protected:
   //--- Структура массивов элементов
   struct WindowElements
     {
      //--- Общий массив всех объектов
      //--- Общий массив всех элементов

      //--- Персональные массивы элементов:
      //    Массив контекстных меню
      //--- Массив главных меню
      //--- Всплывающие подсказки
      //--- Массив выпадающих списков разных типов
      CElement         *m_drop_lists[];
     };
   //--- Массив массивов элементов для каждого окна
   WindowElements    m_wnd[];
   //---
public:
   //--- Количество выпадающих списков
   int               DropListsTotal(const int window_index);
  };
//+------------------------------------------------------------------+
//| Возвращает кол-во выпадающих списков по указанному индексу окна  |
//+------------------------------------------------------------------+
int CWndContainer::DropListsTotal(const int window_index)
  {
   if(window_index>=::ArraySize(m_wnd))
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//---
   return(::ArraySize(m_wnd[window_index].m_drop_lists));
  }

3. Создать приватный метод для добавления указателей в персональный массив. В данном случае нужно из элемента «Комбо-бокс» получить указатель на список и добавить его в персональный массив, и кроме этого, добавить указатели на объекты списка и полосы прокрутки этого списка в общий массив объектов. Подробнее смотрите код метода CWndContainer::AddComboBoxElements() в листинге кода ниже. 

class CWndContainer
  {
private:
   //--- Сохраняет указатели на элементы контекстного меню в базу
   //--- Сохраняет указатели на элементы главного меню в базу
   //--- Сохраняет указатели на элементы сдвоенной кнопки в базу
   //--- Сохраняет указатели на элементы всплывающих подсказок в базу
   //--- Сохраняет указатели на объекты списка в базу

   //--- Сохраняет указатели на элементы выпадающих списков в базу
   bool              AddComboBoxElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Сохраняет указатель на выпадающий список в персональный массив   |
//+------------------------------------------------------------------+
bool CWndContainer::AddComboBoxElements(const int window_index,CElement &object)
  {
//--- Выйдем, если это не всплывающая подсказка
   if(object.ClassName()!="CComboBox")
      return(false);
//--- Получим указатель на комбо-бокс
   CComboBox *cb=::GetPointer(object);
//---
   for(int i=0; i<2; i++)
     {
      //--- Увеличение массива элементов
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      //--- Добавим список в базу
      if(i==0)
        {
         CListView *lv=cb.GetListViewPointer();
         m_wnd[window_index].m_elements[size]=lv;
         AddToObjectsArray(window_index,lv);
         //--- Добавим указатель в персональный массив
         AddToRefArray(lv,m_wnd[window_index].m_drop_lists);
        }
      //--- Добавим полосу прокрутки в базу
      else if(i==1)
        {
         CScrollV *sv=cb.GetScrollVPointer();
         m_wnd[window_index].m_elements[size]=sv;
         AddToObjectsArray(window_index,sv);
        }
     }
//---
   return(true);
  }

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

//+------------------------------------------------------------------+
//| Добавляет указатель в массив элементов                           |
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElement &object)
  {
//--- Если в базе нет форм для элементов управления
//--- Если запрос на несуществующую форму
//--- Добавим в общий массив элементов
//--- Добавим объекты элемента в общий массив объектов
//--- Запомним во всех формах id последнего элемента
//--- Увеличим счётчик идентификаторов элементов

//--- Сохраняет указатели на объекты контекстного меню в базу
//--- Сохраняет указатели на объекты главного меню в базу
//--- Сохраняет указатели на объекты сдвоенной кнопки в базу
//--- Сохраняет указатели на объекты всплывающей подсказки в базу
//--- Сохраняет указатели на объекты списка в базу

//--- Сохраняет указатели на объекты элемента комбо-бокса в базу
   if(AddComboBoxElements(window_index,object))
      return;
  }

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

//+------------------------------------------------------------------+
//| Устанавливает состояние графика                                  |
//+------------------------------------------------------------------+
void CWndEvents::SetChartState(void)
  {
   int awi=m_active_window_index;
//--- Для определения события, когда нужно отключить управление
   bool condition=false;
//--- Проверим окна
   int windows_total=CWndContainer::WindowsTotal();
   for(int i=0; i<windows_total; i++)
     {
      //--- Перейти к следующей, если эта форма скрыта
      if(!m_windows[i].IsVisible())
         continue;
      //--- Проверить условия во внутреннем обработчике формы
      m_windows[i].OnEvent(m_id,m_lparam,m_dparam,m_sparam);
      //--- Если есть фокус, отметим это
      if(m_windows[i].MouseFocus())
        {
         condition=true;
         break;
        }
     }
//--- Проверим выпадающие списки
   if(!condition)
     {
      //--- Получим общее количество выпадающих списков
      int drop_lists_total=CWndContainer::DropListsTotal(awi);
      for(int i=0; i<drop_lists_total; i++)
        {
         //--- Получим указатель на выпадающий список
         CListView *lv=m_wnd[awi].m_drop_lists[i];
         //--- Если список активирован (открыт)
         if(lv.IsVisible())
           {
            //--- Проверим фокус над списком и состояние его полосы прокрутки
            if(m_wnd[awi].m_drop_lists[i].MouseFocus() || lv.ScrollState())
              {
               condition=true;
               break;
              }
           }
        }
     }
//--- Проверим фокус контекстных меню
   if(!condition)
     {
      //--- Получим общее количество выпадающих контекстных меню
      int context_menus_total=CWndContainer::ContextMenusTotal(awi);
      for(int i=0; i<context_menus_total; i++)
        {
         //--- Если фокус над контекстным меню
         if(m_wnd[awi].m_context_menus[i].MouseFocus())
           {
            condition=true;
            break;
           }
        }
     }
//--- Устанавливаем состояние графика во всех формах
   for(int i=0; i<windows_total; i++)
      m_windows[i].CustomEventChartState(condition);
  }

Всё готово для того, чтобы протестировать элемент «Комбинированный список». 

 

 

Тест элемента в графическом интерфейсе пользовательского приложения

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

В пользовательском классе (CProgram) тестового приложения класс комбинированного списка уже доступен через базовые классы. Создаём четыре экземпляра класса типа CComboBox и объявляем четыре метода для каждого из них с указанием отступов от верхней левой точки формы (см. листинг кода ниже).

//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Комбо-боксы
   CComboBox         m_combobox1;
   CComboBox         m_combobox2;
   CComboBox         m_combobox3;
   CComboBox         m_combobox4;
   //---
private:
   //--- Комбо-бокс 1
#define COMBOBOX1_GAP_X       (7)
#define COMBOBOX1_GAP_Y       (50)
   bool              CreateComboBox1(const string text);
   //--- Комбо-бокс 2
#define COMBOBOX2_GAP_X       (160)
#define COMBOBOX2_GAP_Y       (50)
   bool              CreateComboBox2(const string text);
   //--- Комбо-бокс 3
#define COMBOBOX3_GAP_X       (7)
#define COMBOBOX3_GAP_Y       (202)
   bool              CreateComboBox3(const string text);
   //--- Комбо-бокс 4
#define COMBOBOX4_GAP_X       (160)
#define COMBOBOX4_GAP_Y       (202)
   bool              CreateComboBox4(const string text);
  };

Рассмотрим только один из этих методов, так как можно сказать, что они почти во всём идентичны, кроме некоторых настраиваемых пользователем свойств. Например, четвёртый комбо-бокс сделаем заблокированным сразу после его создания. Перечислим основные действия создания элемента управления «Комбо-бокс».

  • Сохраняем указатель на форму в классе элемента.
  • Рассчитываем координаты.
  • Объявляем и сразу инициализируем массив текста для пунктов списка.
  • Устанавливаем свойства элемента. Большинство из них инициализированы значениями по умолчанию, но при необходимости значения можно переопределить перед созданием элемента.
  • Сохраняем значения пунктов в списке комбо-бокса.
  • При необходимости устанавливаем свойства для списка и полосы прокрутки.
  • Выделяем пункт в списке. По умолчанию будет выделен первый (0) пункт.
  • Создаём элемент.
  • При необходимости можно заблокировать элемент. Для примера в нашем тестовом приложении заблокируем четвёртый комбо-бокс.
  • В самом конце нужно передать объект в базовый класс для сохранения указателя.

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

  1. Добавление указателя формы в классе элемента. Иначе создание графического интерфейса будет прервано. О причине неудачи можно узнать по сообщениям в журнале.
  2. Создание элемента.
  3. Сохранение указателя элемента в базу объектов.
//+------------------------------------------------------------------+
//| Создаёт комбо-бокс 1                                             |
//+------------------------------------------------------------------+
bool CProgram::CreateComboBox1(const string text)
  {
//--- Общее количество пунктов в списке
#define ITEMS_TOTAL1 8
//--- Передать объект формы
   m_combobox1.WindowPointer(m_window1);
//--- Координаты
   int x=m_window1.X()+COMBOBOX1_GAP_X;
   int y=m_window1.Y()+COMBOBOX1_GAP_Y;
//--- Массив значений пунктов в списке
   string items_text[ITEMS_TOTAL1]={"FALSE","item 1","item 2","item 3","item 4","item 5","item 6","item 7"};
//--- Установим свойства перед созданием
   m_combobox1.XSize(140);
   m_combobox1.YSize(18);
   m_combobox1.LabelText(text);
   m_combobox1.ButtonXSize(70);
   m_combobox1.AreaColor(clrWhiteSmoke);
   m_combobox1.LabelColor(clrBlack);
   m_combobox1.LabelColorHover(clrCornflowerBlue);
   m_combobox1.ButtonBackColor(C'206,206,206');
   m_combobox1.ButtonBackColorHover(C'193,218,255');
   m_combobox1.ButtonBorderColor(C'150,170,180');
   m_combobox1.ButtonBorderColorOff(C'178,195,207');
   m_combobox1.ItemsTotal(ITEMS_TOTAL1);
   m_combobox1.VisibleItemsTotal(5);
//--- Сохраним значения пунктов в список комбо-бокса
   for(int i=0; i<ITEMS_TOTAL1; i++)
      m_combobox1.ValueToList(i,items_text[i]);
//--- Получим указатель списка
   CListView *lv=m_combobox1.GetListViewPointer();
//--- Установим свойства списка
   lv.LightsHover(true);
   lv.SelectedItemByIndex(lv.SelectedItemIndex()==WRONG_VALUE ? 2 : lv.SelectedItemIndex());
//--- Создадим элемент управления
   if(!m_combobox1.CreateComboBox(m_chart_id,m_subwin,x,y))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_combobox1);
   return(true);
  }

Вызов методов создания элементов нужно размещать в главном методе создания графического интерфейса. В нашем случае это CProgram::CreateTradePanel(). Ниже показана сокращённая версия метода: 

//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы 1 для элементов управления

//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Создание статусной строки

//--- Комбо-боксы
   if(!CreateComboBox1("Combobox 1:"))
      return(false);
   if(!CreateComboBox2("Combobox 2:"))
      return(false);
   if(!CreateComboBox3("Combobox 3:"))
      return(false);
   if(!CreateComboBox4("Combobox 4:"))
      return(false);

//--- Списки

//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

В обработчике событий пользовательского класса (CProgram) добавим блок для определения сообщений от комбо-боксов с идентификатором ON_CLICK_COMBOBOX_ITEM. Сделаем так, что если придёт сообщение от третьего комбо-бокса, то в зависимости от того, какой был выбран пункт в его списке, будет меняться состояние четвёртого комбо-бокса. В данном случае, выбор любого кроме первого (0) пункта в списке будет делать четвёртый комбо-бокс доступным. Выбор же первого пункта будет его блокировать. 

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие выбора пункта в комбо-боксе
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
     {
      if(sparam==m_combobox1.LabelText())
         ::Print(__FUNCTION__," > Это сообщение от первого комбо-бокса > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      else if(sparam==m_combobox2.LabelText())
         ::Print(__FUNCTION__," > Это сообщение от второго комбо-бокса > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //--- Обрабатываем сообщение от третьего комбо-бокса
      else if(sparam==m_combobox3.LabelText())
        {
         ::Print(__FUNCTION__," > Это сообщение от третьего комбо-бокса > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
         //--- Если выбрали указанное значение, то отключим четвёртый комбо-бокс
         if(m_combobox3.ButtonText()=="FALSE")
            m_combobox4.ComboBoxState(false);
         //--- Если другое значение, то включим четвёртый комбо-бокс
         else
            m_combobox4.ComboBoxState(true);
        }
      else if(sparam==m_combobox4.LabelText())
         ::Print(__FUNCTION__," > Это сообщение от четвёрого комбо-бокса > id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
     }
  }

Если сейчас скомпилировать программу и загрузить её на график, то получим результат, как на скриншоте ниже:

 Рис. 2. Тест элемента «Комбо-бокс».

Рис. 2. Тест элемента «Комбо-бокс».

 

Разработка класса CComboBox для создания комбинированных списков завершена.

 


Заключение

В этой статье мы рассмотрели составной элемент управления «Комбинированный список» или «Комбо-бокс». На текущем этапе разработки библиотеки для создания графических интерфейсов её схема выглядит так, как показано на рисунке ниже:

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

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

 

Следующей статьёй будет открыта шестая часть серии о разработке библиотеки для создания графических интерфейсов. В главах шестой части создадим классы для создания таких элементов управления, как «чек-боксы», «поля ввода» и их различные смешанные типы.

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


Список статей (глав) пятой части: