English 中文 Español Deutsch 日本語 Português
Применение контейнеров для компоновки графического интерфейса: класс CBox

Применение контейнеров для компоновки графического интерфейса: класс CBox

MetaTrader 5Примеры | 11 августа 2015, 13:41
4 238 3
Enrico Lambino
Enrico Lambino

Содержание


1. Введение

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

Реализованный и используемый в данной статье класс менеджера компоновки в общих чертах схож с классами, применяемыми в некоторых широко распространенных языках программирования, например BoxLayout (Java) и Pack geometry manager (Python/Tkinter).


2. Цели

Если мы посмотрим на представленные в MetaTrader 5 примеры SimplePanel и Controls, мы увидим, что элементы управления таких панелей располагаются попиксельно (абсолютное расположение). Каждый элемент управления имеет определенную позицию в клиентской области, а расположение каждого такого элемента зависит от позиции элемента, созданного ранее, с учетом дополнительных отступов. Хотя такой подход и является самым естественным и логичным, в большинстве случаев подобная точность не требуется, и применение такого способа расположения имеет недостатки во многих аспектах.

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

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

Это подтолкнуло нас к созданию системы компоновки, имеющей следующие свойства:

  • Код должен использоваться многократно.
  • Изменение одной составляющей интерфейса должно оказывать минимальное воздействие на другие компоненты.
  • Расположение компонентов внутри интерфейса должно рассчитываться автоматически.

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


3. Класс CBox

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

Компоновка CBox

Рис. 1. Компоновка CBox

Внешний блок - это размер всего контейнера, а нарисованный пунктиром блок внутри него обозначает границы отступа. Голубым цветом показана величина отступа. Оставшаяся белая область — это пространство, предоставленное для расположения элементов управления внутри контейнера.

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

CBox создается путем расширения класса CWndClient (без полос прокрутки), как показано во фрагменте кода ниже:

#include <Controls\WndClient.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CBox : public CWndClient
  {
public:
                     CBox();
                    ~CBox();   
   virtual bool      Create(const long chart,const string name,const int subwin,
                           const int x1,const int y1,const int x2,const int y2);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CBox::CBox() 
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CBox::~CBox()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CBox::Create(const long chart,const string name,const int subwin,
                  const int x1,const int y1,const int x2,const int y2)
  {
   if(!CWndContainer::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateBack())
      return(false);
   if(!ColorBackground(CONTROLS_DIALOG_COLOR_CLIENT_BG))
      return(false);
   if(!ColorBorder(clrNONE))
      return(false);
   return(true);
  }
//+------------------------------------------------------------------+

Класс CBox также может напрямую наследовать класс CWndContainer. Однако это может лишить класс некоторых полезных свойств, как, например, отображение фона и границы. Другую намного более простую версию можно создать путем прямого расширения класса CWndObj, но для этого необходимо добавить экземпляр CArrayObj в качестве одного из закрытых или защищенных членов и воссоздать методы класса, включая объекты, которые должны в нем храниться.


3.1. Стили компоновки

CBox предлагает два стиля компоновки: вертикальный и горизонтальный.

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

Горизонтальный стиль класса CBox

Рис. 2. Горизонтальный стиль (центрированный)

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

Вертикальный стиль класса CBox

Рис. 3. Вертикальный стиль (центрированный)

По умолчанию CBox применяет горизонтальный стиль.

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

Для реализации горизонтального и вертикального стилей в CBox необходимо объявить перечисление, которое затем будет храниться как один из членов указанного класса:

enum LAYOUT_STYLE
  {
   LAYOUT_STYLE_VERTICAL,
   LAYOUT_STYLE_HORIZONTAL
  };

3.2. Расчет пространства между элементами управления

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

Глядя на эти рисунки, мы можем составить формулу расчета пространства между элементами управления в данном контейнере CBox при помощи приведенного ниже псевдокода:

горизонтальная компоновка:
пространство x = ((доступное пространство x)-(общий размер x всех элементов управления))/(общее количество элементов управления + 1)
пространство y = ((доступное пространство y)-(размер y элемента управления))/2

вертикальная компоновка:
пространство x = ((доступное пространство x)-(размер x элемента управления))/2
пространство y = ((доступное пространство y)-(общий размер y всех элементов управления))/(общее количество элементов управления + 1)

3.3. Выравнивание

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

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

Горизонтальная коробка - выравнивание по левому краю

Рис. 4. Горизонтальный стиль (выравнивание по левому краю)

Горизонтальная коробка - выравнивание по правому краю

Рис. 5. Горизонтальный стиль (выравнивание по правому краю)

Горизонтальная коробка - выравнивание по центру (без пространства по бокам)

Рис. 6. Горизонтальный стиль (центрированный, без пространства по бокам)


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

Вертикальная коробка - выравнивание по верхнему краю Вертикальная коробка - выравнивание по центру (без пространства сверху и снизу) Вертикальная коробка - выравнивание по нижнему краю

Рис. 7. Выравнивание вертикального стиля: (слева) выравнивание по верхнему краю, (в центре) выравнивание по центру без пространства сверху и снизу, (справа) выравнивание по нижнему краю

Обратите внимание, что класс CBox должен автоматически рассчитывать пространство по x и y между элементами управления на основе типа выравнивания. Поэтому вместо указанного ниже разделителя

(общее количество элементов управления + 1)

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

Аналогично стилям компоновки, реализация свойств выравнивания в классе CBox требует перечислений. Для каждого стиля выравнивания вы объявим одно перечисление:

enum VERTICAL_ALIGN
  {
   VERTICAL_ALIGN_CENTER,
   VERTICAL_ALIGN_CENTER_NOSIDES,
   VERTICAL_ALIGN_TOP,
   VERTICAL_ALIGN_BOTTOM
  };
enum HORIZONTAL_ALIGN
  {
   HORIZONTAL_ALIGN_CENTER,
   HORIZONTAL_ALIGN_CENTER_NOSIDES,
   HORIZONTAL_ALIGN_LEFT,
   HORIZONTAL_ALIGN_RIGHT
  };

3.4. Визуализация компонентов

Обычно мы создаем элементы управления, указывая параметры x1, y1, x2 и y2, как в следующем фрагменте кода создания кнопки:

CButton m_button;
int x1 = currentX;
int y1 = currentY;
int x2 = currentX+BUTTON_WIDTH; 
int y2 = currentY+BUTTON_HEIGHT
if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,x1,y1,x2,y2))
      return(false);

где x2 минус x1 и y2 минус y1 эквивалентны ширине и высоте элемента управления соответственно. Но вместо этого мы можем создать такую же кнопку в классе CBox, используя гораздо более простой метод, который показан в следующем фрагменте кода:

if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,0,0,BUTTON_WIDTH,BUTTON_HEIGHT))
      return(false);

Класс CBox автоматически переместит компонент при создании окна панели в дальнейшем. Для перемещения элементов управления и контейнеров необходимо задействовать метод Pack(), который вызывает метод Render():

bool CBox::Pack(void)
  {
   GetTotalControlsSize();
   return(Render());
  }

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

bool CBox::Render(void)
  {
   int x_space=0,y_space=0;
   if(!GetSpace(x_space,y_space))
      return(false);
   int x=Left()+m_padding_left+
      ((m_horizontal_align==HORIZONTAL_ALIGN_LEFT||m_horizontal_align==HORIZONTAL_ALIGN_CENTER_NOSIDES)?0:x_space);
   int y=Top()+m_padding_top+
      ((m_vertical_align==VERTICAL_ALIGN_TOP||m_vertical_align==VERTICAL_ALIGN_CENTER_NOSIDES)?0:y_space);
   for(int j=0;j<ControlsTotal();j++)
     {
      CWnd *control=Control(j);
      if(control==NULL) 
         continue;
      if(control==GetPointer(m_background)) 
         continue;
      control.Move(x,y);     
      if (j<ControlsTotal()-1)
         Shift(GetPointer(control),x,y,x_space,y_space);      
     }
   return(true);
  }

3.5. Изменение размеров компонентов

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

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


3.6. Рекурсивная визуализация

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

#define CLASS_LAYOUT 999

Затем мы переопределяем метод Type() класса CObject так, чтобы он вернул подготовленное нами значение макроса:

virtual int       Type() const {return CLASS_LAYOUT;}

Наконец, внутри метода Pack() класса CBox мы применяем метод визуализации к дочерним контейнерам, которые являются экземплярами класса компоновки:

for(int j=0;j<ControlsTotal();j++)
     {
      CWnd *control=Control(j);
      if(control==NULL) 
         continue;
      if(control==GetPointer(m_background)) 
         continue;
      control.Move(x,y);

      //вызов метода Pack(), если это класс компоновки
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }     
   
      if (j<ControlsTotal()-1)
         Shift(GetPointer(control),x,y,x_space,y_space);      
     }

Метод визуализации начинается с расчета доступного пространства для размещения элементов управления внутри контейнера. Значения хранятся в m_total_x и m_total_y соответственно. Следующей задачей является расчет пространства между элементами управления на основе стиля компоновки и выравнивания. И последним этапом является реализация действительного переразмещения элементов управления внутри контейнера.

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

CBox также сохраняет минимальный размер элемента управления внутри контейнера (без учета фона), определяемый m_min_size (struct CSize). Это необходимо для равномерного горизонтального либо вертикального расположения элементов управления в пределах контейнера. Определение достаточно контринтуитивно, так как в действительности это размер самого крупного элемента управления. Но здесь мы определяем его как минимальный размер, так CBox воспримет его таковым и на его основе рассчитает доступное пространство

Обратите внимание, что стандартное действие метода Shift() схоже с реализацией расположения элементов управления (абсолютное расположение). Методы визуализации сохраняют информацию о координатах x и y, которые запоминаются и обновляются, когда CBox перемещает каждый элемент управления. Но при использовании CBox это совершается автоматически, и разработчику панели остается только указать реальный размер каждого элемента управления.


4. Реализация диалогового окна

Используя CBox, мы в сущности замещаем функционал родной клиентской области CDialog или CAppDialog, m_client_area, которая является экземпляром класса CWndClient. И в этом случае у нас появляется три варианта действия:

  1. Расширить/переписать CAppDialog или CDialog, чтобы позволить CBox заместить клиентскую область.
  2. Использовать контейнеры и добавить их в клиентскую область.
  3. Использовать основной контейнер CBox для размещения в нем небольших контейнеров.

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

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

Мы рекомендуем третий вариант. А именно, мы создаем основной контейнер CBox для размещения в нем меньших по размеру контейнеров и элементов управления. Основной контейнер займет ширину всей родной клиентской области и будет добавлен к ней как единственный потомок. Это приведет к небольшой избыточности родной клиентской области, но, по крайней мере, она не останется неиспользованной. Более того, данный вариант позволяет нам избежать такой масштабной задачи, как кодирование/перекодирование.


5. Примеры


5.1. Пример №1: простой калькулятор размера пипса

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

  • наименование символа или инструмента;
  • размер 1 пипса для указанного символа или инструмента;
  • значение 1 пипса для символа или инструмента.

В итоге мы получаем 7 различных элементов управления, включая метки (CLabel) для каждого поля и кнопку (CButton) для выполнения расчета. Скриншот калькулятора представлен ниже:

Калькулятор размера пипса - скриншот

Рис. 8. Калькулятор размера пипса

Как видно на панели калькулятора, он применяет 5 различных контейнеров CBox. На панели должны находиться 3 горизонтальных контейнера для каждого из полей и еще один горизонтальный контейнер, выравненный по правому краю, для кнопки. Все эти контейнеры размещаются внутри основного контейнера с вертикальным стилем. И наконец, основной контейнер должен быть прикреплен к клиентской области экземпляра класса CAppDialog. На рисунке ниже показана компоновка контейнеров. Фиолетовые блоки представляют горизонтальные ряды. Белые блоки — это основные элементы управления, а большой серый блок, в котором находятся все меньшие блоки, является основным окном.

Калькулятор размера пипса - компоновка диалогового окна

Рис. 9. Компоновка калькулятора размера пипса

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

Создание панели мы начнем с создания заголовочного файла "PipValueCalculator.mqh", который должен находиться в той же папке, что и основной исходный файл, подготовкой которого мы займемся позднее (PipValueCalculator.mq5). В этот файл мы включим заголовочный файл класса CBox, а также другие включения, необходимые для этой панели. Нам также потребуется класс CSymbolInfo, который будет использоваться для действительного расчета размера пипса любого выбранного символа:

#include <Trade\SymbolInfo.mqh>
#include <Layouts\Box.mqh>
#include <Controls\Dialog.mqh>
#include <Controls\Label.mqh>
#include <Controls\Button.mqh>

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

#define CONTROL_WIDTH   (100)
#define CONTROL_HEIGHT  (20)

Теперь мы переходим к созданию фактического объекта класса панели. Это делается обычным способом при помощи нового класса, наследующего класс CAppDialog:

class CPipValueCalculatorDialog : public CAppDialog

Начальная структура класса похожа на следующее:

class CPipValueCalculatorDialog : public CAppDialog
  {
protected:
//защищенные члены класса
public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();

protected:
//защищенные методы класса
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPipValueCalculatorDialog::CPipValueCalculatorDialog(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPipValueCalculatorDialog::~CPipValueCalculatorDialog(void)
  {
  }

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

class CPipValueCalculatorDialog : public CAppDialog
  {
protected:
   CBox              m_main;
//продолжение кода...

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

// начало определения класса
// ...
public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
protected:
   virtual bool      CreateMain(const long chart,const string name,const int subwin);
// оставшаяся часть определения
// ...

Теперь вне класса мы определим фактическое тело метода класса (аналогично определению тела конструктора и деструктора класса):

bool CPipValueCalculatorDialog::CreateMain(const long chart,const string name,const int subwin)
  {   
   //создание основного контейнера CBox
   if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);   

   //применить вертикальную компоновку
   m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
   
   //установить отступ в 10 пикселей со всех сторон
   m_main.Padding(10);
   return(true);
  }

При помощи CDialog::m_client_area.Width() and CDialog::m_client_area.Height() мы указываем ширину и высоту контейнера. То есть все пространство клиентской области панели. Мы также немного модифицируем контейнер: применим вертикальный стиль и настроим отступ в 10 пикселей со всех сторон. Эти функции предоставляются классом CBox.

После того как мы определили член класса основного контейнера и способ его создания, мы создаем члены для рядов, как показано на рис.9. Для верхнего ряда, который предназначен для символа, мы объявляем их путем создания контейнера, а затем основных элементов управления, содержащихся в этом контейнере. Все это происходит непосредственно под объявлением члена класса основного контейнера, показанного во фрагменте кода выше:

CBox              m_main;
CBox              m_symbol_row;   //контейнер ряда
CLabel            m_symbol_label; //контейнер надписи
CEdit             m_symbol_edit;  //элемент управления Edit

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

bool CPipValueCalculatorDialog::CreateSymbolRow(const long chart,const string name,const int subwin)
  {
   //создание контейнера CBox для этого ряда (ряд символа)
   if(!m_symbol_row.Create(chart,name+"symbol_row",subwin,0,0,CDialog::m_client_area.Width(),CONTROL_HEIGHT*1.5))
      return(false);

   //создание элемента управления с надписью
   if(!m_symbol_label.Create(chart,name+"symbol_label",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))
      return(false);
   m_symbol_label.Text("Symbol");
   
   //создание элемента управления edit
   if(!m_symbol_edit.Create(chart,name+"symbol_edit",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))
      return(false);
   m_symbol_edit.Text(m_symbol.Name());

   //добавление основных элементов управления к родительскому контейнеру (ряд)
   if(!m_symbol_row.Add(m_symbol_label))
      return(false);
   if(!m_symbol_row.Add(m_symbol_edit))
      return(false);
   return(true);
  }

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

После создания ряда мы переходим к созданию индивидуальных элементов управления. На этот раз они используют определенные нами ранее макросы ширины и высоты элементов управления. Также обратите внимание, как мы создаем эти элементы управления:

Create(chart,name+"symbol_edit",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))

Значения красного цвета — это координаты x1 и y1. Это значит, что при создании все элементы управления размещаются в верхнем левом углу графика. Они перестроятся после того, как мы вызовем метод Pack() класса CBox.

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

if(!m_symbol_row.Add(m_symbol_label))
   return(false);
if(!m_symbol_row.Add(m_symbol_edit))
   return(false);

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

При использовании класса CBox необходимо создание основного контейнера и других дочерних рядов. Теперь мы перейдем к более знакомой теме, а именно к созданию самого объекта панели. Она создается (независимо от того, используете ли вы CBox) посредством переопределения виртуального метода Create() класса CAppDialog. Именно в этом методе два созданных нами ранее метода наконец-то обретают смысл, так как мы вызываем их именно здесь:

bool CPipValueCalculatorDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   //создание панели CAppDialog
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   
   //создание основного контейнера CBox при помощи ранее определенной функции  
   if(!CreateMain(chart,name,subwin))
      return(false);  

   //создание контейнера CBox для ряда символа при помощи ранее определенной функции  
   if(!CreateSymbolRow(chart,name,subwin))
      return(false);

   //добавление контейнера CBox для ряда символа в качестве потомка основного контейнера CBox
   if(!m_main.Add(m_symbol_row))
      return(false);

   //визуализация основного контейнера CBox и всех его дочерних контейнеров (рекурсивно)
   if (!m_main.Pack())
      return(false);
   
   //добавление основного контейнера CBox в качестве единственного потомка клиентской области панели
   if (!Add(m_main))
      return(false);
   return(true);
  }

Не забудьте объявить переопределенный метод Create() в классе CPipValueCalculatorDialog следующим образом:

public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);

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

#include "PipValueCalculator.mqh"
CPipValueCalculatorDialog ExtDialog;
//+------------------------------------------------------------------+
//| Функция инициализации советника                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
//--- создать окно приложения
   if(!ExtDialog.Create(0,"Pip Value Calculator",0,50,50,279,250))
      return(INIT_FAILED);
//--- запуск приложения
   if(!ExtDialog.Run())
      return(INIT_FAILED);
//--- ok
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Функция деинициализации советника                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   ExtDialog.Destroy(reason);
  }
//+------------------------------------------------------------------+
//| Тиковая функция эксперта                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  }
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
    //закомментировано, обсуждение будет далее в разделе  
    //ExtDialog.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

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

  1. В качестве заголовочного файла мы добавляем "PipValueCalculator.mqh", а не включаемый файл для CAppDalog. "PipValueCalculator.mqh" уже включает заголовочный файл, так что нам больше не нужно включать его в основной исходный файл. "PipValueCalculator.mqh" также отвечает за включение заголовочного файла класса CBox.
  2. Мы объявляем ExtDialog в качестве экземпляра класса, который мы определили в "PipValueCalculator.mqh" (класс PipValueCalculator).
  3. Мы указываем наиболее подходящий пользовательский размер для панели, как определено в ExtDialog.Create().

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

Калькулятор размера пипса с одним рядом

Рис. 10. Калькулятор размера пипса с одним рядом

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

m_button_row.HorizontalAlign(HORIZONTAL_ALIGN_RIGHT);

Описание обработки событий не входит в задачу данной статьи, но для полноты данного примера мы все-таки кратко ее обсудим. Начнем с объявления нового члена класса PipValueCalculator — m_symbol. Мы также включим два дополнительных члена — m_digits_adjust и m_points_adjust, — которые используем позднее для перевода размера из пунктов в пипсы.

CSymbolInfo      *m_symbol;
int               m_digits_adjust;
double            m_points_adjust;

Мы инициализируем m_symbol либо в конструкторе класса, либо в методе Create() при помощи следующего кода:

if (m_symbol==NULL)
      m_symbol=new CSymbolInfo();
if(m_symbol!=NULL)
{
   if (!m_symbol.Name(_Symbol))
      return(false);
}   

Если указатель символа равен NULL, мы создаем новый экземпляр класса CSymbolInfo. Если он не равен NULL, мы приписываем имя символа, одинаковое с именем символа графика.

На следующем этапе мы определяем обработчик события нажатия кнопки. Это реализуется методом класса OnClickButton(). Определяем его тело следующим образом:

void CPipValueCalculatorDialog::OnClickButton()
  {
   string symbol=m_symbol_edit.Text();
   StringToUpper(symbol);
   if(m_symbol.Name(symbol))
     {
      m_symbol.RefreshRates();
      m_digits_adjust=(m_symbol.Digits()==3 || m_symbol.Digits()==5)?10:1;
      m_points_adjust=m_symbol.Point()*m_digits_adjust;
      m_pip_size_edit.Text((string)m_points_adjust);      
      m_pip_value_edit.Text(DoubleToString(m_symbol.TickValue()*(StringToDouble(m_pip_size_edit.Text()))/m_symbol.TickSize(),2));
     }
   else Print("invalid input");
  }

Метод класса рассчитывает размер пипса, получая значение из элемента управления m_symbol_edit. Затем он передает название символа экземпляру класса CSymbolInfo. Указанный класс получает значение тика выбранного символа, которое затем изменяется при помощи определенного множителя, чтобы вычислить значение 1 пипса.

Последним этапом включения обработки событий класса является определение обработчика событий (также в пределах класса PipValueCalculator). Вставьте данную строчку кода в блок объявления открытых методов класса:

virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

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

EVENT_MAP_BEGIN(CPipValueCalculatorDialog)
   ON_EVENT(ON_CLICK,m_button,OnClickButton)
EVENT_MAP_END(CAppDialog)


5.2. Пример №2: воссоздание примера Controls

Пример панели элементов управления автоматически устанавливается при установке MetaTrader. В окне навигатора его можно найти через Советники\Examples\Controls. Скриншот такой панели представлен ниже:

Элементы управления - диалоговое окно

Рис. 11. Диалоговое окно с элементами управления (оригинальное)

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

  1. элемент управления Edit;
  2. три элемента управления Button;
  3. SpinEdit и DatePicker;
  4. ComboBox, RadioGroup и CheckGroup (столбец 1), а также ListView (столбец 2).

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

Рисунок 12. Компоновка элементов управления в диалоговом окне

Рис. 12. Компоновка элементов управления в диалоговом окне

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

После установки начинайте компиляцию и выполнение. Все должно отлично функционировать за исключением объекта выбора даты, чьи кнопки инкремента, декремента и списка откажутся работать. Это связано с тем, что выпадающий список класса CDatePicker установлен на фоне остальных контейнеров. Чтобы решить эту проблему, обратитесь к файлу класса CDatePicker, расположенному в %Каталог данных%\MQL5\Include\Controls\DatePicker.mqh. Найдите метод ListShow() и вставьте следующую строчку кода в начале этой функции:

BringToTop();

Перекомпилируйте и протестируйте. Это переместит выпадающий список объекта выбора даты на передний план, а также даст приоритет событиям нажатия кнопки, когда бы они ни произошли. Ниже представлен фрагмент кода всей функции:

bool CDatePicker::ListShow(void)
  {
   BringToTop();
//--- установка значения   
   m_list.Value(m_value);
//--- показываем список
   return(m_list.Show());
  }

Скриншот воссозданного диалогового окна Controls представлен ниже:

Элементы управления - перестроенное диалоговое окно

Рис. 13. Диалоговое окно с элементами управления (с использованием CBox)

Глядя на большой рисунок, можно подумать, что он практически идентичен оригинальному. Но все же имеется одно удивительное различие, которое появилось случайно: столбец 1 идеально выравнен по столбцу 2. В оригинальной версии мы видим, что CheckGroup выравнен с ListView по нижнему краю. Но в верхней части ComboBox не выравнен по верхнему краю c ListView. Конечно, можно переместить координаты и на оригинальной панели, но для этого потребуется настройка не только координат пикселей для ComboBox, но также координат RadioGroup и промежутков между этими элементами управления. С другой стороны, использование контейнера CBox требует нулевое значение отступов сверху и снизу и применение верного выравнивания.

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


6. Плюсы и минусы

Плюсы:

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

Минусы:

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


7. Заключение

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

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

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/1867

Прикрепленные файлы |
box.mqh (23.76 KB)
controls2.mq5 (4.23 KB)
controlsdialog2.mqh (40.63 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Alexandr Gavrilin
Alexandr Gavrilin | 11 авг. 2015 в 22:11

такой класс нужно в стандартные  внести разработчиками!

супер!!!

Stanislav Korotky
Stanislav Korotky | 29 февр. 2020 в 19:49

Столкнулся с проблемой в данной полезной надстройке.

Прошу совета у тех, кто разбирается в стандартных контролах.

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

Суть проблемы. Берем программу Controls2.mq5 из статьи (для компиляции нужны также ControlsDialog2.mqh и Box.mqh), компилируем, запускаем.

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

Исходная аналогичная демка от MQ (Experts/Examples/Controls.mq5) работает с "дейтпикером" нормально.

Stanislav Korotky
Stanislav Korotky | 1 мар. 2020 в 19:58
Stanislav Korotky:

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

Исходная аналогичная демка от MQ (Experts/Examples/Controls.mq5) работает с "дейтпикером" нормально.

Вопрос снимается. В CDatePicker отсутствует вызов BringToTop в момент раскрытия выпадающей части, как сделано в CComboBox, например. В стандартном примере дейтпикер работает за счет того, что его инициализацию подвинули (намеренно или случайно) после создания "листбокса", который оказывается топологически под ним. А контролы в CWndContainer::OnMouseEvent обходятся от последних добавленных к первым.

Чтобы нормально пофиксить, надо было бы переопределить CDatePicker::ListShow, но он не виртуальный. Приходится переопределять CDatePicker::OnClickButton и добавлять туда BringToTop. Однако корректно это сделать не получается как и с любым виртуальным методом в стандартной библиотеке, потому что все переменные члены объявлены private. В частности нельзя написать:

bool MyDatePicker::OnClickButton(void) // override
{
    return ((m_drop.Pressed()) ? BringToTop() && ListShow() : ListHide());
}

потому что m_drop недоступен. Приходится вызывать BringToTop и при раскрытии, и при схлопывании.

#include <Controls/DatePicker.mqh>

class CDatePickerFixed: public CDatePicker
{
  protected:
    virtual bool OnClickButton() override
    {
      BringToTop();
      return CDatePicker::OnClickButton();
    }
};
Price Action. Автоматизация торговли по паттерну "Поглощение" Price Action. Автоматизация торговли по паттерну "Поглощение"
В статье описывается создание советника для MetaTrader 4, торгующего по паттерну "Поглощение", включая принцип нахождения паттерна, правила установки отложенных и стоп-ордеров. Приведены результаты тестирования и оптимизации.
Оценка эффективности торговых систем путем анализа их компонентов Оценка эффективности торговых систем путем анализа их компонентов
В данной статье исследуется эффективность составных торговых систем путем анализа эффективности отдельных ее компонентов. Любой анализ, будь то графический, на основе индикаторов или какой-то другой, является одной из ключевых составляющих успешной торговли на финансовых рынках. Эта статья — своего рода исследование нескольких независимых простых торговых систем, анализ их эффективности и полезности совместного применения.
Теория рынка Теория рынка
До сих пор не существует логически завершенной теории рынка, охватывающей все типы и разновидности рынков товаров и услуг, микро- и макро-рынков, наподобие Форекс. Статья повествует о сущности новой теории рынка, основанной на анализе прибыли, вскрывает закономерности изменения текущей цены, а также выявляет принцип работы механизма, позволяющего цене находить наиболее оптимальное свое значение путем образования цепи виртуальных цен, способных вырабатывать управляющие воздействия на саму цену. Выявлены механизмы образования и смены трендов на рынке.
Управление терминалом MetaTrader с помощью DLL Управление терминалом MetaTrader с помощью DLL
В данной статье рассматривается управление элементами интерфейса MetaTrader с использованием вспомогательной DLL-библиотеки на примере изменения настроек рассылки Push-сообщений. К статье приложен исходный код библиотеки и пример скрипта.