English 中文 Español Deutsch 日本語 Português
Графические интерфейсы III: Простые и многофункциональные кнопки (Глава 1)

Графические интерфейсы III: Простые и многофункциональные кнопки (Глава 1)

MetaTrader 5Примеры | 17 марта 2016, 15:27
6 991 32
Anatoli Kazharski
Anatoli Kazharski

Содержание



Введение

Ранее, в предыдущих двух частях серии, были рассмотрены множество тем, которые относятся к разработке структуры библиотеки для создания графических интерфейсов, а также основные механизмы, предназначенные для управления объектами.

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

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

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

  • Обычная кнопка (simple button). Класс CSimpleButton.
  • Кнопка с картинкой (icon button). Класс CIconButton.
  • Сдвоенная кнопка с несколькими функциями (split button). Класс CSplitButton.

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

  • Группа из простых кнопок. Класс CButtonsGroup.
  • Группа из кнопок с картинками. Класс CIconButtonsGroup.
  • Группа радио-кнопок. Класс CRadioButtons.

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

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

 


Разработка класса для создания простой кнопки

Начнём с простой кнопки. В файле Objects.mqh мы ранее подготовили класс для создания примитива элемента управления типа CButton. Его базовым классом является CChartObjectButton, с помощью которого можно создать графический объект типа OBJ_BUTTON. В свойствах у этого объекта в свойствах уже предусмотрено два состояния – нажат (on) и отжат (off). Графически это может быть представлено тоже в двух разных вариантах, в зависимости от того, включено ли отображение рамки у объекта. В обоих режимах цвет нажатой кнопки становится чуть темнее, чем в отжатом состоянии.

Вручную этот объект можно установить на график из главного меню: Вставка -> Объекты -> Графические объекты -> Кнопка. Также для ручного изменения доступны параметры из окна настроек графического объекта:

Рис. 1. Окно настроек графического объекта «Кнопка».

Рис. 1. Окно настроек графического объекта «Кнопка».

 

Далее, в папке Controls, где сейчас расположены другие файлы с классами элементов управления разрабатываемой библиотеки, создадим файл SimpleButton.mqh, а в нём — класс CSimpleButton со стандартными для всех элементов полями и методами, которые уже были рассмотрены ранее в предыдущих статьях.

//+------------------------------------------------------------------+
//|                                                 SimpleButton.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Класс для создания простой кнопки                                |
//+------------------------------------------------------------------+
class CSimpleButton : public CElement
  {
private:
   //--- Указатель на форму, к которой присоединен элемент 
   CWindow          *m_wnd;
   //---
public:
                     CSimpleButton(void);
                    ~CSimpleButton(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);
  };

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

  • Размер.
  • Цвета фона для различных состояний и относительно положения курсора мыши.
  • Цвета рамки для различных состояний.
  • Цвета текста.

Иногда нужна кнопка, которая автоматически переходит в начальное (отжатое) состояние после нажатия. Но бывает так, что после нажатия кнопка должна оставаться в нажатом состоянии, если была до этого отжата, и отжиматься, если была до этого нажата. Сделаем так, чтобы можно было работать с кнопкой в двух режимах по выбору пользователя. Также понадобится метод IsPressed() для определения того, нажата или отжата сейчас кнопка.

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

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

class CSimpleButton : public CElement
  {
private:
   //--- Объект для создания кнопки
   CButton           m_button;
   //--- Свойства кнопки:
   //    (1) Текст, (2) размеры
   string            m_button_text;
   int               m_button_x_size;
   int               m_button_y_size;
   //--- Цвет фона
   color             m_back_color;
   color             m_back_color_off;
   color             m_back_color_hover;
   color             m_back_color_pressed;
   color             m_back_color_array[];
   //--- Цвет рамки
   color             m_border_color;
   color             m_border_color_off;
   //--- Цвет текста
   color             m_text_color;
   color             m_text_color_off;
   color             m_text_color_pressed;
   //--- Приоритет на нажатие левой кнопкой мыши
   int               m_button_zorder;
   //--- Режим двух состояний кнопки
   bool              m_two_state;
   //--- Доступен/заблокирован
   bool              m_button_state;
   //---
public:
   //--- Методы для создания простой кнопки
   bool              CreateSimpleButton(const long chart_id,const int subwin,const string button_text,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   //---
public:
   //--- (1) установка режима кнопки,
   //    (2) общее состояние кнопки (доступна/заблокирована)
   void              TwoState(const bool flag)               { m_two_state=flag;               }
   bool              IsPressed(void)                   const { return(m_button.State());       }
   bool              ButtonState(void)                 const { return(m_button_state);         }
   void              ButtonState(const bool state);
   //--- Размер кнопки
   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) установка цвета текста кнопки
   string            Text(void)                        const { return(m_button.Description()); }
   void              TextColor(const color clr)              { m_text_color=clr;               }
   void              TextColorOff(const color clr)           { m_text_color_off=clr;           }
   void              TextColorPressed(const color clr)       { m_text_color_pressed=clr;       }
   //--- Установка цвета фона кнопки
   void              BackColor(const color clr)              { m_back_color=clr;               }
   void              BackColorOff(const color clr)           { m_back_color_off=clr;           }
   void              BackColorHover(const color clr)         { m_back_color_hover=clr;         }
   void              BackColorPressed(const color clr)       { m_back_color_pressed=clr;       }
   //--- Установка цвета рамки кнопки
   void              BorderColor(const color clr)            { m_border_color=clr;             }
   void              BorderColorOff(const color clr)         { m_border_color_off=clr;         }
   //---
  };
//+------------------------------------------------------------------+
//| Изменение состояния кнопки                                       |
//+------------------------------------------------------------------+
void CSimpleButton::ButtonState(const bool state)
  {
   m_button_state=state;
   m_button.State(false);
   m_button.Color((state)? m_text_color : m_text_color_off);
   m_button.BackColor((state)? m_back_color : m_back_color_off);
   m_button.BorderColor((state)? m_border_color : m_border_color_off);
  }

Далее рассмотрим логику обработки событий при нажатии на кнопку. В файл Defines.mqh нужно добавить ещё один идентификатор ON_CLICK_BUTTON для генерации пользовательского события. Он будет использоваться во всех классах, предназначенных для создания кнопок.

#define ON_CLICK_BUTTON           (8)  // Нажатие на кнопке

Теперь нужно создать метод CSimpleButton::OnClickButton() для обработки нажатия на кнопке. В начале метода нужны две проверки: (1) по имени нажатого объекта и (2) на текущее состояние объекта. Отрицательный результат проверок приводит к выходу из метода. Если же проверки пройдены, то далее обработка производится с учётом того, в каком режиме пребывает кнопка. Если она самоотжимающаяся, то производится возврат к её исходному состоянию и соответствующему цвету. Для режима с двумя состояниями для каждого используются две группы цветов. В самом конце метода осуществляется отправка сообщения с идентификатором ON_CLICK_BUTTON, идентификатором элемента, индексом элемента и названием кнопки. 

class CSimpleButton : public CElement
  {
private:
   //--- Обработка нажатия на кнопке
   bool              OnClickButton(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Обработка событий                                                |
//+------------------------------------------------------------------+
void CSimpleButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события нажатия левой кнопки мыши на объекте
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickButton(sparam))
         return;
     }
  }
//+------------------------------------------------------------------+
//| Обработка нажатия на кнопку                                      |
//+------------------------------------------------------------------+
bool CSimpleButton::OnClickButton(const string clicked_object)
  {
//--- Проверка по имени объекта
   if(m_button.Name()!=clicked_object)
      return(false);
//--- Если кнопка заблокирована
   if(!m_button_state)
     {
      m_button.State(false);
      return(false);
     }
//--- Если режим кнопки с одним состоянием
   if(!m_two_state)
     {
      m_button.State(false);
      m_button.Color(m_text_color);
      m_button.BackColor(m_back_color);
     }
//--- Если режим кнопки с двумя состояниями
   else
     {
      //--- Если кнопка нажата
      if(m_button.State())
        {
         //--- Изменим цвета кнопки 
         m_button.State(true);
         m_button.Color(m_text_color_pressed);
         m_button.BackColor(m_back_color_pressed);
         CElement::InitColorArray(m_back_color_pressed,m_back_color_pressed,m_back_color_array);
        }
      //--- Если кнопка отжата
      else
        {
         //--- Изменим цвета кнопки 
         m_button.State(false);
         m_button.Color(m_text_color);
         m_button.BackColor(m_back_color);
         CElement::InitColorArray(m_back_color,m_back_color_hover,m_back_color_array);
        }
     }
//--- Отправить сигнал об этом
   ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),CElement::Index(),m_button.Description());
   return(true);
  }

Уже сейчас можно протестировать установку кнопок на график с привязкой к форме, а также приём сообщений в обработчике пользовательского класса приложения. Сделаем копию тестовой программы (эксперта) из предыдущей статьи. Оставим там только главное меню с привязанными к его пунктам контекстными меню. Файл с классом CSimpleButton должен быть подключен к файлу WndContainer.mqh, чтобы его можно было использовать в пользовательском классе. 

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

После этого можно в пользовательском классе приложения CProgram объявить экземпляры класса CSimpleButton и методы для создания кнопок. Для примера создадим три кнопки. Две из них будут самоотжимающимися после нажатия, а одна — с возможностью фиксации, то есть, будет иметь два состояния (нажата/отжата).

class CProgram : public CWndEvents
  {
private:
   //--- Простые кнопки
   CSimpleButton     m_simple_button1;
   CSimpleButton     m_simple_button2;
   CSimpleButton     m_simple_button3;
   //---
private:
#define BUTTON1_GAP_X            (7)
#define BUTTON1_GAP_Y            (50)
   bool              CreateSimpleButton1(const string text);
#define BUTTON2_GAP_X            (128)
#define BUTTON2_GAP_Y            (50)
   bool              CreateSimpleButton2(const string text);
#define BUTTON3_GAP_X            (7)
#define BUTTON3_GAP_Y            (75)
   bool              CreateSimpleButton3(const string text);
  };

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

//+------------------------------------------------------------------+
//| Создаёт простую кнопку 3                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateSimpleButton3(string button_text)
  {
//--- Передать объект панели
   m_simple_button3.WindowPointer(m_window);
//--- Координаты
   int x=m_window.X()+BUTTON3_GAP_X;
   int y=m_window.Y()+BUTTON3_GAP_Y;
//--- Установим свойства перед созданием
   m_simple_button3.TwoState(true);
   m_simple_button3.ButtonXSize(237);
   m_simple_button3.TextColor(clrBlack);
   m_simple_button3.TextColorPressed(clrBlack);
   m_simple_button3.BackColor(clrLightGray);
   m_simple_button3.BackColorHover(C'193,218,255');
   m_simple_button3.BackColorPressed(C'153,178,215');
//--- Создание кнопки
   if(!m_simple_button3.CreateSimpleButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_simple_button3);
   return(true);
  }

Вызов всех методов для создания элементов графического интерфейса производится в методе CProgram::CreateTradePanel():

//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Простые кнопки
   if(!CreateSimpleButton1("Simple Button 1"))
      return(false);
   if(!CreateSimpleButton2("Simple Button 2"))
      return(false);
   if(!CreateSimpleButton3("Simple Button 3"))
      return(false);
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

В обработчике событий CProgram::OnEvent() будем принимать сообщение с идентификатором события ON_CLICK_BUTTON (см. листинг кода ниже). Для примера реализуем блок кода, в котором будет проверяться имя кнопки. Если окажется, что было произведено нажатие на третьей кнопке, то от того, в каком состоянии она сейчас пребывает, будет зависеть состояние первой кнопки. То есть, если третья кнопка нажата, то первая кнопка будет блокироваться, и наоборот.

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие нажатия на кнопке
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      ::Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //---
      if(sparam==m_simple_button3.Text())
        {
         if(m_simple_button3.IsPressed())
            m_simple_button1.ButtonState(false);
         else
            m_simple_button1.ButtonState(true);
        }
     }
  }

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

 Рис. 2. Тест установки элемента «Кнопка» на график. Показаны кнопки в разных состояниях.

Рис. 2. Тест установки элемента «Кнопка» на график. Показаны кнопки в разных состояниях. 

 

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

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

//+------------------------------------------------------------------+
//| Обработка событий                                                |
//+------------------------------------------------------------------+
void CSimpleButton::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>X() && x<X2() && y>Y() && y<Y2());
      //--- Выйти, если форма заблокирована
      if(m_wnd.IsLocked())
         return;
      //--- Выйти, если кнопка мыши отжата
      if(sparam=="0")
         return;
      //--- Выйти, если кнопка заблокирована
      if(!m_button_state)
         return;
      //--- Если нет фокуса
      if(!CElement::MouseFocus())
        {
         //--- Если кнопка отжата
         if(!m_button.State())
            {
             m_button.Color(m_text_color);
             m_button.BackColor(m_back_color);
            }
         //---
         return;
        }
      //--- Если есть фокус
      else
        {
         m_button.Color(m_text_color_pressed);
         m_button.BackColor(m_back_color_pressed);
         return;
        }
      //---
      return;
     }
  }

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

 


Разработка класса для создания кнопки с картинкой

Кнопка с картинкой будет состоять из трёх графических объектов-примитивов:

  1. Фон.
  2. Ярлык.
  3. Текстовая метка.

Рис. 3. Составные части элемента «Кнопка с картинкой».

Рис. 3. Составные части элемента «Кнопка с картинкой».

 

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

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

Далее нужно создать файл IconButton.mqh, а в нём — класс CIconButton. Подключим его к файлу WndContainer.mqh, так же, как и все другие элементы управления. В листинге кода ниже показаны только те поля и методы класса CIconButton, которые отличают его от класса для создания простой кнопки (CSimpleButton).

class CIconButton : public CElement
  {
private:
   //--- Объекты для создания кнопки
   CButton           m_button;
   CBmpLabel         m_icon;
   CLabel            m_label;
   //--- Свойства кнопки:
   //    Ярлыки кнопки в активном и заблокированном состоянии
   string            m_icon_file_on;
   string            m_icon_file_off;
   //--- Отступы ярлыка
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //--- Текст и отступы текстовой метки
   string            m_label_text;
   int               m_label_x_gap;
   int               m_label_y_gap;
   //--- Режим "Только картинка", когда кнопка состоит только из объекта BmpLabel
   bool              m_only_icon;
   //---
public:
   //--- Методы для создания кнопки
   bool              CreateIconButton(const long chart_id,const int subwin,const string button_text,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   //---
public:
   //--- Установка режима "Только картинка"
   void              OnlyIcon(const bool flag)                { m_only_icon=flag;               }
   //--- Установка ярлыков для кнопки в активном и заблокированном состояниях
   void              IconFileOn(const string file_path)       { m_icon_file_on=file_path;       }
   void              IconFileOff(const string file_path)      { m_icon_file_off=file_path;      }
   //--- Отступы ярлыка
   void              IconXGap(const int x_gap)                { m_icon_x_gap=x_gap;             }
   void              IconYGap(const int y_gap)                { m_icon_y_gap=y_gap;             }
   //--- Отступы текстовой метки
   void              LabelXGap(const int x_gap)               { m_label_x_gap=x_gap;            }
   void              LabelYGap(const int y_gap)               { m_label_y_gap=y_gap;            }
  };

Не забываем инициализировать все поля класса значениями по умолчанию:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CIconButton::CIconButton(void) : m_icon_x_gap(4),
                                 m_icon_y_gap(3),
                                 m_label_x_gap(25),
                                 m_label_y_gap(4),
                                 m_icon_file_on(""),
                                 m_icon_file_off(""),
                                 m_button_state(true),
                                 m_two_state(false),
                                 m_only_icon(false),
                                 m_button_y_size(18),
                                 m_back_color(clrLightGray),
                                 m_back_color_off(clrLightGray),
                                 m_back_color_hover(clrSilver),
                                 m_back_color_pressed(clrBlack),
                                 m_border_color(clrWhite),
                                 m_border_color_off(clrDarkGray),
                                 m_label_color(clrBlack),
                                 m_label_color_off(clrDarkGray),
                                 m_label_color_hover(clrBlack),
                                 m_label_color_pressed(clrBlack)
  {
//--- Сохраним имя класса элемента в базовом классе  
   CElement::ClassName(CLASS_NAME);
//--- Установим приоритеты на нажатие левой кнопки мыши
   m_button_zorder =1;
   m_zorder        =0;
  }

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

//+------------------------------------------------------------------+
//| Создаёт фон кнопки                                               |
//+------------------------------------------------------------------+
bool CIconButton::CreateButton(void)
  {
//--- Выйдем, если включен режим "Только картинка"
   if(m_only_icon)
      return(true);
//--- ... и т.д.
  }
//+------------------------------------------------------------------+
//| Создаёт надпись кнопки                                           |
//+------------------------------------------------------------------+
bool CIconButton::CreateLabel(void)
  {
//--- Выйдем, если включен режим "Только картинка"
   if(m_only_icon)
      return(true);
//--- ... и т.д.
  }

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

//+------------------------------------------------------------------+
//| Создаёт картинку кнопки                                          |
//+------------------------------------------------------------------+
bool CIconButton::CreateIcon(void)
  {
//--- Если режим "Только картинка" отключен
   if(!m_only_icon)
     {
      //--- Если ярлык для кнопки не нужен, выйти
      if(m_icon_file_on=="" || m_icon_file_off=="")
         return(true);
     }
//--- Если включен режим "Только картинка" 
   else
     {
      //--- Если ярлык не был определён, вывести сообщение и выйти
      if(m_icon_file_on=="" || m_icon_file_off=="")
        {
         ::Print(__FUNCTION__," > В режиме \"Only icon\" определение картинки обязательно.");
         return(false);
        }
     }
//--- Формирование имени объекта
   string name=CElement::ProgramName()+"_icon_button_bmp_"+(string)CElement::Id();
//--- Координаты
   int x =(!m_only_icon)? m_x+m_icon_x_gap : m_x;
   int y =(!m_only_icon)? m_y+m_icon_y_gap : m_y;
//--- Установим ярлык
   if(!m_icon.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- Установим свойства
   m_icon.BmpFileOn("::"+m_icon_file_on);
   m_icon.BmpFileOff("::"+m_icon_file_off);
   m_icon.State(true);
   m_icon.Corner(m_corner);
   m_icon.GetInteger(OBJPROP_ANCHOR,m_anchor);
   m_icon.Selectable(false);
   m_icon.Z_Order((!m_only_icon)? m_zorder : m_button_zorder);
   m_icon.Tooltip((!m_only_icon)? "\n" : m_label_text);
//--- Сохраним координаты
   m_icon.X(x);
   m_icon.Y(y);
//--- Сохраним размеры
   m_icon.XSize(m_icon.X_Size());
   m_icon.YSize(m_icon.Y_Size());
//--- Отступы от крайней точки
   m_icon.XGap(x-m_wnd.X());
   m_icon.YGap(y-m_wnd.Y());
//--- Сохраним указатель объекта
   CElement::AddToArray(m_icon);
   return(true);
  }

Во всём остальном класс CIconButton ничем особенным не отличается от того, что было уже рассмотрено в классе CSimpleButton. Полную версию можно посмотреть в приложенных к статье файлах.

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

class CProgram : public CWndEvents
  {
private:
   //--- Кнопки с картинками
   CIconButton       m_icon_button1;
   CIconButton       m_icon_button2;
   CIconButton       m_icon_button3;
   CIconButton       m_icon_button4;
   CIconButton       m_icon_button5;
   //---
private:
   //--- Кнопки с картинками
#define ICONBUTTON1_GAP_X        (7)
#define ICONBUTTON1_GAP_Y        (105)
   bool              CreateIconButton1(const string text);
#define ICONBUTTON2_GAP_X        (128)
#define ICONBUTTON2_GAP_Y        (105)
   bool              CreateIconButton2(const string text);
#define ICONBUTTON3_GAP_X        (7)
#define ICONBUTTON3_GAP_Y        (130)
   bool              CreateIconButton3(const string text);
#define ICONBUTTON4_GAP_X        (88)
#define ICONBUTTON4_GAP_Y        (130)
   bool              CreateIconButton4(const string text);
#define ICONBUTTON5_GAP_X        (169)
#define ICONBUTTON5_GAP_Y        (130)
   bool              CreateIconButton5(const string text);
  };

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

//+------------------------------------------------------------------+
//| Создаёт кнопку с картинкой 5                                     |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp64\\gold.bmp"
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp64\\gold_colorless.bmp"
//---
bool CProgram::CreateIconButton5(const string button_text)
  {
//--- Передать объект панели
   m_icon_button5.WindowPointer(m_window);
//--- Координаты
   int x=m_window.X()+ICONBUTTON5_GAP_X;
   int y=m_window.Y()+ICONBUTTON5_GAP_Y;
//--- Установим свойства перед созданием
   m_icon_button5.ButtonXSize(76);
   m_icon_button5.ButtonYSize(87);
   m_icon_button5.LabelXGap(6);
   m_icon_button5.LabelYGap(69);
   m_icon_button5.LabelColor(clrBlack);
   m_icon_button5.LabelColorPressed(clrBlack);
   m_icon_button5.BackColor(clrGainsboro);
   m_icon_button5.BackColorHover(C'193,218,255');
   m_icon_button5.BackColorPressed(C'210,210,220');
   m_icon_button5.BorderColor(C'150,170,180');
   m_icon_button5.BorderColorOff(C'178,195,207');
   m_icon_button5.IconXGap(6);
   m_icon_button5.IconYGap(3);
   m_icon_button5.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp64\\gold.bmp");
   m_icon_button5.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp64\\gold_colorless.bmp");
//--- Создадим элемент управления
   if(!m_icon_button5.CreateIconButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,m_icon_button5);
   return(true);
  }

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

В обработчике событий, для примера, добавим отслеживание нажатия на кнопке Icon Button 2. В версии эксперта, который приложен в конце статьи, эта кнопка работает в двух режимах (нажата/отжата). Доступность кнопок Icon Button 1 и Icon Button 4 зависит от состояния кнопки Icon Button 2. Если эта кнопка отжата, то зависящие от неё кнопки заблокированы, и наоборот. 

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие нажатия на кнопке
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      Print("id: ",id,"; lparam: ",lparam,"; dparam: ",dparam,"; sparam: ",sparam);
      //---
      if(sparam==m_simple_button3.Text())
        {
         if(m_simple_button3.IsPressed())
            m_simple_button1.ButtonState(false);
         else
            m_simple_button1.ButtonState(true);
        }
      //---
      if(sparam==m_icon_button2.Text())
        {
         if(m_icon_button2.IsPressed())
           {
            m_icon_button1.ButtonState(true);
            m_icon_button4.ButtonState(true);
           }
         else
           {
            m_icon_button1.ButtonState(false);
            m_icon_button4.ButtonState(false);
           }
        }
     }
  }

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

Рис. 4. Тест элемента управления «Кнопка с картинкой».

Рис. 4. Тест элемента управления «Кнопка с картинкой».

 

Мы завершили разработку класса CIconButton для создания кнопки с расширенными возможностями. Картинки в кнопках, которые вы видите на скриншоте выше, можно скачать в конце статьи. Далее рассмотрим класс для создания сдвоенной кнопки (split button). 



Разработка класса для создания сдвоенной кнопки

Что из себя представляет сдвоенная кнопка? Это кнопка состоящая из двух функционирующих частей:

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

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

Сдвоенная кнопка будет собираться из пяти объектов (графических примитивов) и одного подключаемого элемента (контекстное меню):

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

 

Рис. 5. Составные части элемента «Сдвоенная кнопка».

Рис. 5. Составные части элемента «Сдвоенная кнопка».

 

Здесь мы пришли к тому, что контекстное меню (объект класса CContextMenu) не будет привязываться к объекту класса CMenuItem. А значит, нам нужен дополнительный режим, когда контекстное меню может быть частью любого другого элемента или даже абсолютно ни к чем не привязанным. 

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

class CWindow : public CElement
  {
private:
   //--- Идентификатор активированного элемента управления
   int               m_id_activated_element;
   //---
public:
   //--- Методы для сохранения и получения id активированного элемента
   int               IdActivatedElement(void)                          const { return(m_id_activated_element);     }
   void              IdActivatedElement(const int id)                        { m_id_activated_element=id;          }
  };

Теперь, когда какой-либо элемент блокирует форму в момент своей активации, нужно сохранить в форме идентификатор этого элемента. А там, где производится разблокировка формы, нужно поставить проверку на идентификатор того элемента, который её заблокировал. Но там, где перед разблокировкой есть проверка на имя нажатого объекта, проверка на идентификатор элемента, который заблокировал форму, не нужна. 

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

class CContextMenu : public CElement
  {
   //--- Режим свободного контекстного меню. То есть, без привязки к предыдущему узлу.
   bool              m_free_context_menu;
   //---
public:
   //--- Установка режима свободного контекстного меню
   void              FreeContextMenu(const bool flag)               { m_free_context_menu=flag;             }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void) : m_free_context_menu(false)
  {
  }

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

Добавим новый идентификатор (ON_CLICK_FREEMENU_ITEM) для генерации события от свободного контекстного меню в файл Defines.mqh:

#define ON_CLICK_FREEMENU_ITEM    (9)  // Нажатие на пункте свободного контекстного меню

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

1. В обработчике событий:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CContextMenu::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка перемещения курсора мыши
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если элемент скрыт
      //--- Получим фокус
      //--- Выйти, если это свободное контекстное меню
      if(m_free_context_menu)
         return;
      //--- Если контекстное меню включено и левая кнопка мыши нажата
      //--- Проверим условия на закрытие всех контекстных меню, которые были открыты после этого
      return;
     }
//--- Обработка события нажатия левой кнопки мыши на объекте
//--- Обработка события ON_CLICK_MENU_ITEM
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_MENU_ITEM)
     {
      //--- Выйти, если это свободное контекстное меню
      if(m_free_context_menu)
         return;
      //--- Приём сообщения от пункта меню для обработки
      return;
     }
  }

2. В методе создания контекстного меню:

//+------------------------------------------------------------------+
//| Создаёт контекстное меню                                         |
//+------------------------------------------------------------------+
bool CContextMenu::CreateContextMenu(const long chart_id,const int subwin,const int x=0,const int y=0)
  {
//--- Выйти, если нет указателя на форму
//--- Если это привязанное контекстное меню
   if(!m_free_context_menu)
     {
      //--- Выйти, если нет указателя на предыдущий узел 
      if(::CheckPointer(m_prev_node)==POINTER_INVALID)
        {
         ::Print(__FUNCTION__," > Перед созданием контекстного меню ему нужно передать "
                 "указатель на предыдущий узел с помощью метода CContextMenu::PrevNodePointer(CMenuItem &object).");
         return(false);
        }
     }
//--- Инициализация переменных
//--- Если координаты не указаны
//--- Если координаты указаны
//--- Отступы от крайней точки
//--- Создание контекстного меню
//--- Скрыть элемент
   return(true);
  }

3. В методе создания списка пунктов меню:

//+------------------------------------------------------------------+
//| Создаёт список пунктов меню                                      |
//+------------------------------------------------------------------+
bool CContextMenu::CreateItems(void)
  {
   int s =0;     // Для определения положения разделительных линий
   int x =m_x+1; // Координата X
   int y =m_y+1; // Координата Y. Будет рассчитываться в цикле для каждого пункта меню.
//--- Количество разделительных линий
//---
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Расчёт координаты Y
      //--- Сохраним указатель формы
      //--- Если контекстное меню с привязкой, то добавим указатель на предыдущий узел
      if(!m_free_context_menu)
         m_items[i].PrevNodePointer(m_prev_node);
      //--- Установим свойства
      //--- Отступы от крайней точки панели
      //--- Создание пункта меню
      //--- Перейти к следующему, если все разделительные линии установлены
      //--- Если индексы совпали, значит после этого пункта нужно установить разделительную линию
     }
   return(true);
  }

4. В методах показа и скрытия контекстного меню: 

//+------------------------------------------------------------------+
//| Показывает контекстное меню                                      |
//+------------------------------------------------------------------+
void CContextMenu::Show(void)
  {
//--- Выйти, если элемент уже видим
//--- Показать объекты контекстного меню
//--- Показать пункты меню
//--- Присвоить статус видимого элемента
//--- Состояние контекстного меню
//--- Отметить состояние в предыдущем узле
   if(!m_free_context_menu)
      m_prev_node.ContextMenuState(true);
//--- Заблокируем форму
  }
//+------------------------------------------------------------------+
//| Скрывает контекстное меню                                        |
//+------------------------------------------------------------------+
void CContextMenu::Hide(void)
  {
//--- Выйти, если элемент скрыт
//--- Скрыть объекты контекстного меню
//--- Скрыть пункты меню
//--- Обнулить фокус
//--- Присвоить статус скрытого элемента
//--- Состояние контекстного меню
//--- Отметить состояние в предыдущем узле
   if(!m_free_context_menu)
      m_prev_node.ContextMenuState(false);
  }

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

//+------------------------------------------------------------------+
//| Обработка нажатия на пункт меню                                  |
//+------------------------------------------------------------------+
bool CContextMenu::OnClickMenuItem(const string clicked_object)
  {
//--- Выйдем, если это контекстное меню имеет предыдущий узел и уже открыто
   if(!m_free_context_menu && m_context_menu_state)
      return(true);
//--- Выйдем, если нажатие было не на пункте меню
   if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",0)<0)
      return(false);
//--- Получим идентификатор и индекс из имени объекта
   int id    =IdFromObjectName(clicked_object);
   int index =IndexFromObjectName(clicked_object);
//--- Если контекстное меню имеет предыдущий узел
   if(!m_free_context_menu)
     {
      //--- Выйдем, если нажали не на пункте, к которому это контекстное меню привязано
      if(id!=m_prev_node.Id() || index!=m_prev_node.Index())
         return(false);
      //--- Показать контекстное меню
      Show();
     }
//--- Если это свободное контекстное меню
   else
     {
      //--- Найдём в цикле пункт меню, на который нажали
      int total=ItemsTotal();
      for(int i=0; i<total; i++)
        {
         if(m_items[i].Object(0).Name()!=clicked_object)
            continue;
         //--- Отправим сообщение об этом
         ::EventChartCustom(m_chart_id,ON_CLICK_FREEMENU_ITEM,CElement::Id(),i,DescriptionByIndex(i));
         break;
        }
     }
//---
   return(true);
  }

Всё готово для разработки класса сдвоенной кнопки. В папке Controls создадим файл SplitButton.mqh с классом CSplitButton и со стандартными для всех элементов управления методами:

//+------------------------------------------------------------------+
//|                                                  SplitButton.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "ContextMenu.mqh"
//+------------------------------------------------------------------+
//| Класс для создания сдвоенной кнопки                              |
//+------------------------------------------------------------------+
class CSplitButton : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //---
public:
                     CSplitButton();
                    ~CSplitButton();

   //--- Сохраняет указатель формы
   void              WindowPointer(CWindow &object)           { m_wnd=::GetPointer(object);         }

   //--- Обработчик событий графика
   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);
  };

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

  • Размер. В данной версии используется только ширина. Высота будет равна высоте главной кнопки.
  • Приоритет на нажатие левой кнопкой мыши. Для кнопки с выпадающим меню приоритет должен быть выше, чем у основной кнопки.
  • Картинки и отступы для ярлыка кнопки.
  • Состояние контекстного меню кнопки (видимо/скрыто). 

class CSplitButton : public CElement
  {
private:
   //--- Размер и приоритет кнопки с выпадающим меню на нажатие левой кнопкой мыши
   int               m_drop_button_x_size;
   int               m_drop_button_zorder;
   //--- Отступы ярлыка
   int               m_drop_arrow_x_gap;
   int               m_drop_arrow_y_gap;
   //--- Ярлыки кнопки с выпадающим меню в активном и заблокированном состоянии
   string            m_drop_arrow_file_on;
   string            m_drop_arrow_file_off;
   //--- Состояние контекстного меню 
   bool              m_drop_menu_state;
   //---
public:
   //--- Размер кнопки с выпадающим меню
   void              DropButtonXSize(const int x_size)        { m_drop_button_x_size=x_size;        }
   //--- Установка ярлыков для кнопки с выпадающим меню в активном и заблокированном состояниях
   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;           }
  };

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

class CSplitButton : public CElement
  {
private:
   //--- Объекты для создания кнопки
   CButton           m_button;
   CBmpLabel         m_icon;
   CLabel            m_label;
   CEdit             m_drop_button;
   CBmpLabel         m_drop_arrow;
   CContextMenu      m_drop_menu;
   //---
public:
   //--- Методы для создания кнопки
   bool              CreateSplitButton(const long chart_id,const string button_text,const int window,const int x,const int y);
   //---
private:
   bool              CreateButton(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   bool              CreateDropButton(void);
   bool              CreateDropIcon(void);
   bool              CreateDropMenu(void);
   //---
public:
   //--- Получение указателя контекстного меню,
   CContextMenu     *GetContextMenuPointer(void)        const { return(::GetPointer(m_drop_menu));  }
   //--- Добавляет пункт меню с указанными свойствами до создания контекстного меню
   void              AddItem(const string text,const string path_bmp_on,const string path_bmp_off);
   //--- Добавляет разделительную линию после указанного пункта до создания контекстного меню
   void              AddSeparateLine(const int item_index);
  };

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

//+------------------------------------------------------------------+
//| Создаёт выпадающее меню                                          |
//+------------------------------------------------------------------+
bool CSplitButton::CreateDropMenu(void)
  {
//--- Передать объект панели
   m_drop_menu.WindowPointer(m_wnd);
//--- Свободное контекстное меню
   m_drop_menu.FreeContextMenu(true);
//--- Координаты
   int x=m_x;
   int y=m_y+m_y_size;
//--- Установим свойства
   m_drop_menu.Id(CElement::Id());
   m_drop_menu.XSize((m_drop_menu.XSize()>0)? m_drop_menu.XSize() : m_button_x_size);
//--- Установим контекстное меню
   if(!m_drop_menu.CreateContextMenu(m_chart_id,m_subwin,x,y))
      return(false);
//---
   return(true);
  }

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

class CSplitButton : public CElement
  {
private:
   //--- Обработка нажатия на кнопку
   bool              OnClickButton(const string clicked_object);
   //--- Обработка нажатия на кнопку с выпадающим меню
   bool              OnClickDropButton(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события нажатия левой кнопки мыши на объекте
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Нажатие на основной кнопке
      if(OnClickButton(sparam))
         return;
      //--- Нажатие на кнопке с выпадающим меню
      if(OnClickDropButton(sparam))
         return;
     }
  }

В методе обработчике нажатия на главной кнопке CSplitButton::OnClickButton() сначала проверяется имя объекта. Если это имя объекта этого экземпляра класса, то далее проверяется состояние кнопки. Если кнопка заблокирована, то программа выходит из метода. Главная кнопка может быть только в одном состоянии, то есть, должна самоотжиматься после нажатия на ней. Если все проверки пройдены, то далее нужно (1) скрыть контекстное меню, если оно было открыто, (2) установить соответствующий статус состояния и цвета для меню и кнопки, (3) разблокировать форму и обнулить идентификатор элемента-активатора в её памяти. 

В самом конце метода нужно отправить сообщение, которое можно будет принять в пользовательском классе. В сообщении будет содержаться (1) идентификатор события — ON_CLICK_BUTTON, (2) идентификатор элемента, (3) индекс элемента и (4) отображаемое описание кнопки. 

//+------------------------------------------------------------------+
//| Нажатие на кнопку                                                |
//+------------------------------------------------------------------+
bool CSplitButton::OnClickButton(const string clicked_object)
  {
//--- Выйдем, если чужое имя объекта  
   if(clicked_object!=m_button.Name())
      return(false);
//--- Выйти, если кнопка заблокирована
   if(!m_button_state)
     {
      //--- Отожмём кнопку
      m_button.State(false);
      return(false);
     }
//--- Скроем меню
   m_drop_menu.Hide();
   m_drop_menu_state=false;
//--- Отожмём кнопку и установим цвет фокуса
   m_button.State(false);
   m_button.BackColor(m_back_color_hover);
   m_drop_button.BackColor(m_back_color_hover);
//--- Разблокируем форму
   m_wnd.IsLocked(false);
   m_wnd.IdActivatedElement(WRONG_VALUE);
//--- Отправим сообщение об этом
   ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElement::Id(),CElement::Index(),m_label.Description());
   return(true);
  }

В методе CSplitButton::OnClickDropButton(), в котором обрабатывается нажатие на кнопке с выпадающим меню, также в самом начале две проверки (1) на имя и (2) доступность кнопки. Далее программа заходит в один из двух блоков кода в зависимости от текущего состояния видимости контекстного меню кнопки, чтобы скрыть или показать его.

//+------------------------------------------------------------------+
//| Нажатие на кнопку с выпадающим меню                              |
//+------------------------------------------------------------------+
bool CSplitButton::OnClickDropButton(const string clicked_object)
  {
//--- Выйдем, если чужое имя объекта  
   if(clicked_object!=m_drop_button.Name())
      return(false);
//--- Выйти, если кнопка заблокирована
   if(!m_button_state)
     {
      //--- Отожмём кнопку
      m_button.State(false);
      return(false);
     }
//--- Если список открыт, скроем его
   if(m_drop_menu_state)
     {
      m_drop_menu_state=false;
      m_drop_menu.Hide();
      m_button.BackColor(m_back_color_hover);
      m_drop_button.BackColor(m_back_color_hover);
      //--- Разблокируем форму и обнулим id элемента-активатора
      m_wnd.IsLocked(false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
     }
//--- Если список скрыт, откроем его
   else
     {
      m_drop_menu_state=true;
      m_drop_menu.Show();
      m_button.BackColor(m_back_color_hover);
      m_drop_button.BackColor(m_back_color_pressed);
      //--- Заблокируем форму и сохраним id элемента-активатора
      m_wnd.IsLocked(true);
      m_wnd.IdActivatedElement(CElement::Id());
     }
//---
   return(true);
  }

Чуть ранее в статье было внесено дополнение в класс контекстного меню CContextMenu, которое обеспечивает в свободном режиме отправку события для внутреннего использования с идентификатором ON_CLICK_FREEMENU_ITEM. Это сообщение будет приниматься в обработчике класса сдвоенной кнопки CSplitButton. Для определения того, что пришло сообщение от родственного контекстного меню, нужно проверить идентификатор элемента, который содержится в параметре lparam. Если идентификаторы совпадают, то далее нужно (1) скрыть меню, (2) установить соответствующие состоянию кнопки цвета и (3) разблокировать форму, если активатором был этот элемент. После этого отправляется сообщение с идентификатором ON_CLICK_CONTEXTMENU_ITEM, которое можно принять в пользовательском классе.

Кроме этого, создадим ещё один дополнительный метод CSplitButton::HideDropDownMenu() для неоднократного использования, задача которого — скрыть меню и разблокировать форму с обнулением идентификатора элемента-активатора.

class CSplitButton : public CElement
  {
private:
   //--- Скрывает выпадающее меню
   void              HideDropDownMenu(void);
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CSplitButton::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события нажатия на пункте свободного меню
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_FREEMENU_ITEM)
     {
      //--- Выйти, если идентификаторы не совпадают
      if(CElement::Id()!=lparam)
         return;
      //--- Скрыть выпадающее меню
      HideDropDownMenu();
      //--- Отправим сообщение
      ::EventChartCustom(m_chart_id,ON_CLICK_CONTEXTMENU_ITEM,lparam,dparam,sparam);
      return;
     }
  }
//+------------------------------------------------------------------+
//| Скрывает выпадающее меню                                         |
//+------------------------------------------------------------------+
void CSplitButton::HideDropDownMenu(void)
  {
//--- Скрыть меню и установить соответствующие признаки
   m_drop_menu.Hide();
   m_drop_menu_state=false;
   m_button.BackColor(m_back_color);
   m_drop_button.BackColor(m_back_color);
//--- Разблокируем форму, если идентификаторы формы и этого элемента совпадают
   if(m_wnd.IdActivatedElement()==CElement::Id())
     {
      m_wnd.IsLocked(false);
      m_wnd.IdActivatedElement(WRONG_VALUE);
     }
  }

Далее нужно настроить реакцию сдвоенной кнопки на местоположение курсора мыши и на состояние левой кнопки мыши, когда курсор находится над кнопкой. Для этого понадобится ещё один метод, который назовём CSplitButton::CheckPressedOverButton(). У метода один параметр – состояние левой кнопки мыши. В самом начале стоят две проверки. Если окажется, что (1) курсор вне области кнопки или (2) форма заблокирована и при этом не этот элемент был активатором, то программа выйдет из метода. Если проверки пройдены, то далее, в зависимости от состояния левой кнопки мыши и от того, над какой частью кнопки находится курсор, программа устанавливает соответствующие цвета.

class CSplitButton : public CElement
  {
private:
   //--- Проверка нажатой левой кнопки мыши над сдвоенной кнопкой
   void              CheckPressedOverButton(const bool mouse_state);
  };
//+------------------------------------------------------------------+
//| Проверка нажатой левой кнопки мыши над сдвоенной кнопкой         |
//+------------------------------------------------------------------+
void CSplitButton::CheckPressedOverButton(const bool mouse_state)
  {
//--- Выйти, если вне области элемента
   if(!CElement::MouseFocus())
      return;
//--- Выйти, если форма заблокирована и идентификаторы формы и этого элемента не совпадают
   if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id())
      return;
//--- Кнопка мыши нажата
   if(mouse_state)
     {
      //--- В зоне кнопки меню
      if(m_drop_button.MouseFocus())
        {
         m_button.BackColor(m_back_color_hover);
         m_drop_button.BackColor(m_back_color_pressed);
        }
      else
        {
         m_button.BackColor(m_back_color_pressed);
         m_drop_button.BackColor(m_back_color_pressed);
        }
     }
//--- Кнопка мыши отжата
   else
     {
      if(m_drop_menu_state)
        {
         m_button.BackColor(m_back_color_hover);
         m_drop_button.BackColor(m_back_color_pressed);
        }
     }
  }

Метод CSplitButton::CheckPressedOverButton() вызывается в обработчике по событию перемещения курсора мыши. До его вызова осуществляется ещё несколько проверок, таких как: (1) скрыт ли элемент, (2) фокус, (3) доступен ли элемент, а также (4) если курсор находится вне области элемента, нужно скрыть меню и выйти, не доходя до вызова метода CSplitButton::CheckPressedOverButton(). 

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CSplitButton::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>X() && x<X2() && y>Y() && y<Y2());
      m_drop_button.MouseFocus(x>m_drop_button.X() && x<m_drop_button.X2() && 
                               y>m_drop_button.Y() && y<m_drop_button.Y2());
      //--- Выйти, если кнопка заблокирована
      if(!m_button_state)
         return;
      //--- Вне области элемента и с нажатой кнопкой мыши
      if(!CElement::MouseFocus() && sparam=="1")
        {
         //--- Выйти, если фокус в контекстном меню
         if(m_drop_menu.MouseFocus())
            return;
         //--- Скрыть выпадающее меню
         HideDropDownMenu();
         return;
        }
      //--- Проверка нажатой левой кнопки мыши над сдвоенной кнопкой
      CheckPressedOverButton(bool((int)sparam));
      return;
     }
  }

Класс элемента управления «Сдвоенная кнопка» готов к тестированию. Но для корректной работы его нужно правильно внедрить в структуру библиотеки. Помните, что это нужно делать каждый раз, когда создаётся сложный (составной) элемент управления. Если в случае с кнопками типа CSimpleButton и CIconButton не нужно было вносить каких-то дополнений, то со сдвоенной кнопкой всё обстоит иначе, ведь кроме самой кнопки, в её составе присутствует ещё и контекстное меню, которое тоже должно попасть в базу указателей на элементы управления в соответствующий персональный массив. Конечный пользователь библиотеки, цель которого — только использование окончательной её версии без вмешательства в код, даже не будет знать, как всё это устроено. Главная цель разработчика библиотеки состоит в том, чтобы максимально упростить использование библиотеки, то есть, чтобы создание графического интерфейса программы требовало минимальное количество действий.

Подключим файл с классом CSplitButton к файлу WndContainer.mqh

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

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

//+------------------------------------------------------------------+
//| Класс для хранения всех объектов интерфейса                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
private:
   //--- Сохраняет указатели на элементы сдвоенной кнопки в базу
   bool              AddSplitButtonElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Сохраняет указатели на объекты сдвоенной кнопки в базу           |
//+------------------------------------------------------------------+
bool CWndContainer::AddSplitButtonElements(const int window_index,CElement &object)
  {
//--- Выйдем, если это не сдвоенная кнопка
   if(object.ClassName()!="CSplitButton")
      return(false);
//--- Получим указатель на сдвоенную кнопку
   CSplitButton *sb=::GetPointer(object);
//--- Увеличение массива элементов
   int size=::ArraySize(m_wnd[window_index].m_elements);
   ::ArrayResize(m_wnd[window_index].m_elements,size+1);
//--- Получим указатель контекстного меню
   CContextMenu *cm=sb.GetContextMenuPointer();
//--- Сохраним элемент и объекты в базу
   m_wnd[window_index].m_elements[size]=cm;
   AddToObjectsArray(window_index,cm);
//--- Сохраним указатели на его объекты в базе
   int items_total=cm.ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Увеличение массива элементов
      size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      //--- Получение указателя на пункт меню
      CMenuItem *mi=cm.ItemPointerByIndex(i);
      //--- Сохраняем указатель в массив
      m_wnd[window_index].m_elements[size]=mi;
      //--- Добавляем указатели на все объекты пункта меню в общий массив
      AddToObjectsArray(window_index,mi);
     }
//--- Добавим указатель в персональный массив
   AddToRefArray(cm,m_wnd[window_index].m_context_menus);
   return(true);
  }

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

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

Всё готово для теста сдвоенных кнопок. Создадим четыре такие кнопки в тестовом эксперте. Объявите экземпляры класса CSplitButton и методы создания кнопок с отступами от левой верхней точки формы. Их вызов разместим в главном методе создания графического интерфейса программы.

class CProgram : public CWndEvents
  {
private:
   //--- Сдвоенные кнопки
   CSplitButton      m_split_button1;
   CSplitButton      m_split_button2;
   CSplitButton      m_split_button3;
   CSplitButton      m_split_button4;
   //---
private:
   //--- Сдвоенные кнопки
#define SPLITBUTTON1_GAP_X       (7)
#define SPLITBUTTON1_GAP_Y       (225)
   bool              CreateSplitButton1(const string text);
#define SPLITBUTTON2_GAP_X       (128)
#define SPLITBUTTON2_GAP_Y       (225)
   bool              CreateSplitButton2(const string text);
#define SPLITBUTTON3_GAP_X       (7)
#define SPLITBUTTON3_GAP_Y       (250)
   bool              CreateSplitButton3(const string text);
#define SPLITBUTTON4_GAP_X       (128)
#define SPLITBUTTON4_GAP_Y       (250)
   bool              CreateSplitButton4(const string text);
  };
//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Простые кнопки
//--- Кнопки с картинкой
//--- Сдвоенные кнопки
   if(!CreateSplitButton1("Split Button 1"))
      return(false);
   if(!CreateSplitButton2("Split Button 2"))
      return(false);
   if(!CreateSplitButton3("Split Button 3"))
      return(false);
   if(!CreateSplitButton4("Split Button 4"))
      return(false);
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

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

//+------------------------------------------------------------------+
//| Создаёт сдвоенную кнопку 1                                       |
//+------------------------------------------------------------------+
bool CProgram::CreateSplitButton1(const string button_text)
  {
//--- Три пункта в контекстном меню
#define CONTEXTMENU_ITEMS5 3
//--- Передать объект панели
   m_split_button1.WindowPointer(m_window);
//--- Координаты
   int x=m_window.X()+SPLITBUTTON1_GAP_X;
   int y=m_window.Y()+SPLITBUTTON1_GAP_Y;
//--- Массив названий пунктов
   string items_text[]=
     {
      "Item 1",
      "Item 2",
      "Item 3"
     };
//--- Массив ярлыков для доступного режима
   string items_bmp_on[]=
     {
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\coins.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\line_chart.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart.bmp"
     };
//--- Массив ярлыков для заблокированного режима 
   string items_bmp_off[]=
     {
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\coins_colorless.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\line_chart_colorless.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart_colorless.bmp"
     };
//--- Установим свойства перед созданием
   m_split_button1.ButtonXSize(116);
   m_split_button1.ButtonYSize(22);
   m_split_button1.DropButtonXSize(16);
   m_split_button1.LabelColor(clrBlack);
   m_split_button1.LabelColorPressed(clrBlack);
   m_split_button1.BackColor(clrGainsboro);
   m_split_button1.BackColorHover(C'193,218,255');
   m_split_button1.BackColorPressed(C'190,190,200');
   m_split_button1.BorderColor(C'150,170,180');
   m_split_button1.BorderColorOff(C'178,195,207');
   m_split_button1.BorderColorHover(C'150,170,180');
   m_split_button1.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp");
   m_split_button1.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp16\\script_colorless.bmp");
//--- Получим указатель на контекстное меню кнопки
   CContextMenu *cm=m_split_button1.GetContextMenuPointer();
//--- Установим свойства контекстному меню
   cm.AreaBackColor(C'240,240,240');
   cm.AreaBorderColor(clrSilver);
   cm.ItemBackColor(C'240,240,240');
   cm.ItemBorderColor(C'240,240,240');
   cm.LabelColor(clrBlack);
   cm.LabelColorHover(clrWhite);
   cm.SeparateLineDarkColor(C'160,160,160');
   cm.SeparateLineLightColor(clrWhite);
//--- Добавить пункты в контекстное меню
   for(int i=0; i<CONTEXTMENU_ITEMS5; i++)
      m_split_button1.AddItem(items_text[i],items_bmp_on[i],items_bmp_off[i]);
//--- Разделительная линия после первого пункта меню
   m_split_button1.AddSeparateLine(1);
//--- Создадим элемент управления
   if(!m_split_button1.CreateSplitButton(m_chart_id,button_text,m_subwin,x,y))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,m_split_button1);
   return(true);
  }

 

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

Рис. 6. Тест элемента управления «Сдвоенная кнопка».

Рис. 6. Тест элемента управления «Сдвоенная кнопка».

 

Разработка класса для создания элемента управления «Сдвоенная кнопка» завершена. Из приложенных к статье файлов вы можете загрузить к себе на компьютер версию эксперта, представленного на скриншоте выше. Далее мы рассмотрим разработку классов для создания групп кнопок, то есть, кнопок которые взаимосвязаны между собой. 

 


Заключение

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (32)
Ruslan Piraliyev
Ruslan Piraliyev | 25 июл. 2022 в 19:19
Anatoli Kazharski #:

В последней версии работает так, как Вам нужно:

EasyAndFastGUI - библиотека для создания графических интерфейсов

Спасибо, попробую!

Ruslan Piraliyev
Ruslan Piraliyev | 27 июл. 2022 в 10:29
Anatoli Kazharski #:

В последней версии работает так, как Вам нужно:

EasyAndFastGUI - библиотека для создания графических интерфейсов

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

Anatoli Kazharski
Anatoli Kazharski | 27 июл. 2022 в 11:33
Ruslan Piraliyev #:

...

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

...

Именно этого нет.

Ruslan Piraliyev
Ruslan Piraliyev | 27 июл. 2022 в 11:59
Anatoli Kazharski #:

Именно этого нет.

Понял спасибо.

Ruslan Piraliyev
Ruslan Piraliyev | 27 июл. 2022 в 18:20
Anatoli Kazharski #:

Именно этого нет.

Нашел возможность сделать любой графический объект не прозрачным, подсмотрел реализацию в одном из Ваших кодов). Делается это через выкл/вкл скрола графика ChartSetInteger(0,CHART_MOUSE_SCROLL,true), т.е. если курсор в границах кнопки то выключаем если за пределами включаем. Еще раз спасибо за публичные труды!

В MetaTrader 5 добавлена хеджинговая система учета позиций В MetaTrader 5 добавлена хеджинговая система учета позиций
Чтобы расширить возможности трейдеров ритейл-форекса, в платформу добавлена вторая система учета — хеджинг. Теперь по инструменту можно иметь множество позиций, в том числе — разнонаправленных. Это позволяет реализовывать торговые стратегии с так называемым локированием — если цена пошла против трейдера, он имеет возможность открыть позицию в противоположном направлении.
Рецепты MQL5 - Программируем скользящие каналы Рецепты MQL5 - Программируем скользящие каналы
В данной статье представлен способ программирования системы равноудалённых каналов. Рассматриваются некоторые нюансы построения таких каналов. Приводится типизация каналов, предлагается способ универсального типа скользящих каналов. При реализации кода используется инструментарий ООП.
Графические интерфейсы III: Группы простых и многофункциональных кнопок (Глава 2) Графические интерфейсы III: Группы простых и многофункциональных кнопок (Глава 2)
Первая глава третьей части серии была посвящена простым и многофункциональным кнопкам. Во второй главе рассмотрим группы взаимосвязанных кнопок, позволяющих создавать в приложениях элементы, когда пользователь может выбирать какую-либо одну опцию из представленного набора (группы).
Графические интерфейсы II: Элемент "Главное меню" (Глава 4) Графические интерфейсы II: Элемент "Главное меню" (Глава 4)
Это завершающая глава второй части серии о графических интерфейсах. В ней мы рассмотрим создание такого элемента управления, как «Главное меню». Будет продемонстрирован процесс его разработки и настройка обработчиков классов библиотеки для правильной реакции на действия пользователя. Также мы рассмотрим, как подключить к пунктам главного меню контекстные меню. Кроме того, затронем тему блокировки неактивных на текущий момент элементов.