Улучшаем работу с Панелями: добавляем прозрачность, меняем цвет фона и наследуемся от CAppDialog/CWndClient

Vladimir Karputov | 23 апреля, 2018

Содержание

Введение

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

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


Прозрачная панель при перемещении

Покажу сначала, что можно сделать для панели на базе класса CAppDialog (это пример работы кода "Live panel.mq5").

Live panel

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

Как это сделано

Вся работа будет проводиться вокруг класса CDialog. Он расположен в файле [data folder]\MQL5\Include\Controls\Dialog.mqh

Напомню, из каких объектов состоит панель (эти объекты объявлены в классе CDialog в секции private) и как они визуально воплощаются в графические элементы:

//+------------------------------------------------------------------+
//| Class CDialog                                                    |
//| Usage: base class to create dialog boxes                         |
//|             and indicator panels                                 |
//+------------------------------------------------------------------+
class CDialog : public CWndContainer
  {
private:
   //--- dependent controls
   CPanel            m_white_border;        // the "white border" object
   CPanel            m_background;          // the background object
   CEdit             m_caption;             // the window title object
   CBmpButton        m_button_close;        // the "Close" button object
   CWndClient        m_client_area;         // the client area object

protected:

CDialog objects

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

1. Нас будут интересовать графические элементы "Border" и "Back" (созданные объектами m_white_border и m_background класса CPanel) и элемент "Client" (созданный объектом m_client_area класса CWndClient). Как они создаются и какой им задаётся цвет, видно в функциях CDialog::CreateWhiteBorder, CDialog::CreateBackground и CDialog::CreateClientArea, соответственно.

2. Панель при своём создании получает уникальное имя — цифровой префикс, который ставится перед именами всех графических объектов (на рисунке ниже префикс — это цифры 03082):

Objects

3. В классе CDialog есть три обработчика перетаскивания:

 Обработка перетаскивания
 OnDialogDragStart  Виртуальный обработчик события "DialogDragStart" элемента управления
 OnDialogDragProcess  Виртуальный обработчик события "DialogDragProcess" элемента управления
 OnDialogDragEnd  Виртуальный обработчик события "DialogDragEnd" элемента управления

4. Фокус из статьи "Как создать графическую панель любой сложности и как это работает" — обход объектов панели (здесь ExtDialog — объект класса панели):

   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
...

Почему при обходе цикла указатель на объект obj объявляется с типом CWnd?

Потому что CWnd — базовый класс, от которого происходят все остальные классы-потомки:

Теперь можно приступать к реализации

Для работы с цветом понадобятся два макроса, а для перехвата перемещения панели нужно переопределить три функции из класса CDialog:

//+------------------------------------------------------------------+
//|                                                   Live panel.mq5 |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.000"
#property description "Панель меняет свою прозрачность при перемещении"
#include <Controls\Dialog.mqh>
#define XRGB(r,g,b)    (0xFF000000|(uchar(r)<<16)|(uchar(g)<<8)|uchar(b))
#define GETRGB(clr)    ((clr)&0xFFFFFF)
//+------------------------------------------------------------------+
//| Class CLivePanel                                                 |
//| Usage: main dialog of the Controls application                   |
//+------------------------------------------------------------------+
class CLivePanel : public CAppDialog
  {
public:
                     CLivePanel(void);
                    ~CLivePanel(void);
   //--- create
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   //--- handlers of drag
   virtual bool      OnDialogDragStart(void);
   virtual bool      OnDialogDragProcess(void);
   virtual bool      OnDialogDragEnd(void);

  };

Стандартную часть работы с панелью (создание, удаление и передача событий) я пропущу, а вот на обработчиках перемещения остановлюсь подробнее.

Обработчик "OnDialogDragStart": начало перемещения панели

Получаем префикс, а дальше в цикле обходим все объекты панели и ищем имя "Border", "Back" или "Client" с префиксом:

//+------------------------------------------------------------------+
//| Start dragging the dialog box                                    |
//+------------------------------------------------------------------+
bool CLivePanel::OnDialogDragStart(void)
  {
   string prefix=Name();
   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Border")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(clrNONE);
         ChartRedraw();
        }
      if(name==prefix+"Back")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(clrNONE);
         ChartRedraw();
        }
      if(name==prefix+"Client")
        {
         CWndClient *wndclient=(CWndClient*) obj;
         wndclient.ColorBackground(clrNONE);
         wndclient.ColorBorder(clrNONE);
         ChartRedraw();
        }
     }
   return(CDialog::OnDialogDragStart());
  }

Когда объекты найдены, удаляем цвет фона (метод "ColorBackground") и рамки (метод "ColorBorder"), применяя цвет clrNONE. Таким образом реализуется прозрачность формы.

Обработчик "OnDialogDragProcess": продолжение перемещения панели

Ищем только один объект — "Back", и динамически меняем его цвет (при помощи двух макросов XRGB и GETRGB):

//+------------------------------------------------------------------+
//| Continue dragging the dialog box                                 |
//+------------------------------------------------------------------+
bool CLivePanel::OnDialogDragProcess(void)
  {
   string prefix=Name();
   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
      string name=obj.Name();
      if(name==prefix+"Back")
        {
         CPanel *panel=(CPanel*) obj;
         color clr=(color)GETRGB(XRGB(rand()%255,rand()%255,rand()%255));
         panel.ColorBorder(clr);
         ChartRedraw();
        }
     }
   return(CDialog::OnDialogDragProcess());
  }

Обработчик "OnDialogDragEnd": окончание перемещения панели

Восстанавливаем цвет фона и рамки объектам "Border", "Back" или "Client":

//+------------------------------------------------------------------+
//| End dragging the dialog box                                      |
//+------------------------------------------------------------------+
bool CLivePanel::OnDialogDragEnd(void)
  {
   string prefix=Name();
   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Border")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(CONTROLS_DIALOG_COLOR_BG);
         panel.ColorBorder(CONTROLS_DIALOG_COLOR_BORDER_LIGHT);
         ChartRedraw();
        }
      if(name==prefix+"Back")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(CONTROLS_DIALOG_COLOR_BG);
         color border=(m_panel_flag) ? CONTROLS_DIALOG_COLOR_BG : CONTROLS_DIALOG_COLOR_BORDER_DARK;
         panel.ColorBorder(border);
         ChartRedraw();
        }
      if(name==prefix+"Client")
        {
         CWndClient *wndclient=(CWndClient*) obj;
         wndclient.ColorBackground(CONTROLS_DIALOG_COLOR_CLIENT_BG);
         wndclient.ColorBorder(CONTROLS_DIALOG_COLOR_CLIENT_BORDER);
         ChartRedraw();
        }
     }
   return(CDialog::OnDialogDragEnd());
  }

Добавим на панель кнопку и сделаем кнопку прозрачной при перемещении панели

Это пример содержится в коде "Live panel and Button.mq5"

Чтобы работать с кнопкой, первым делом необходимо подключить в наш советник класс кнопки и добавить макросы, отвечающие за ее позиционирование и размер:

#property description "Панель меняет свою прозрачность при перемещении,"
#property description " но при этом добавленная кнопка остаётся в своём цвете"
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#define XRGB(r,g,b)    (0xFF000000|(uchar(r)<<16)|(uchar(g)<<8)|uchar(b))
#define GETRGB(clr)    ((clr)&0xFFFFFF)
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
//--- indents and gaps
#define INDENT_LEFT                         (11)      // indent from left (with allowance for border width)
#define INDENT_TOP                          (11)      // indent from top (with allowance for border width)
#define CONTROLS_GAP_X                      (5)       // gap by X coordinate
//--- for buttons
#define BUTTON_WIDTH                        (100)     // size by X coordinate
#define BUTTON_HEIGHT                       (20)      // size by Y coordinate
//+------------------------------------------------------------------+
//| Class CLivePanelAndButton                                        |
//| Usage: main dialog of the Controls application                   |
//+------------------------------------------------------------------+
class CLivePanelAndButton : public CAppDialog

Также, чтобы иметь возможность работы с кнопкой, нужно объявить объект класса CButton:

//+------------------------------------------------------------------+
//| Class CLivePanelAndButton                                        |
//| Usage: main dialog of the Controls application                   |
//+------------------------------------------------------------------+
class CLivePanelAndButton : public CAppDialog
  {
private:
   CButton           m_button1;                       // the button object

public:
                     CLivePanelAndButton(void);

и процедуру создания кнопки:

   virtual bool      OnDialogDragEnd(void);

protected:
   //--- create dependent controls
   bool              CreateButton1(void);

  };

Код "CreateButton1" — после создания кнопки не забываем добавить кнопку к создаваемой панели:

//+------------------------------------------------------------------+
//| Create the "Button1" button                                      |
//+------------------------------------------------------------------+
bool CLivePanelAndButton::CreateButton1(void)
  {
//--- coordinates
   int x1=INDENT_LEFT;        // x1            = 11  pixels
   int y1=INDENT_TOP;         // y1            = 11  pixels
   int x2=x1+BUTTON_WIDTH;    // x2 = 11 + 100 = 111 pixels
   int y2=y1+BUTTON_HEIGHT;   // y2 = 11 + 20  = 32  pixels
//--- create
   if(!m_button1.Create(0,"Button1",0,x1,y1,x2,y2))
      return(false);
   if(!m_button1.Text("Button1"))
      return(false);
   if(!Add(m_button1))
      return(false);
//--- succeed
   return(true);
  }

Вот результат, если на панель из примера выше добавить кнопку:

Live panel and Button

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

Делаем кнопку также прозрачной при перемещении панели

Сделаем это при помощи кода "Live panel and transparent Button.mq5".

Когда на панель добавляется простой или комбинированный элемент управления, то создающий его объект добавляется к объекту m_client_area (напомню, что объект m_client_area объявляется в классе CDialog). Следовательно, при обнаружении перемещения панели нужно организовать обход в цикле по всем объектам, добавленным к m_client_area. Делать это удобно в первом обработчике — "OnDialogDragStart" (начало перемещения панели):

//+------------------------------------------------------------------+
//| Start dragging the dialog box                                    |
//+------------------------------------------------------------------+
bool CLivePaneTransparentButton::OnDialogDragStart(void)
  {
   string prefix=Name();
   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Border")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(clrNONE);
         ChartRedraw();
        }
      if(name==prefix+"Back")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(clrNONE);
         ChartRedraw();
        }
      if(name==prefix+"Client")
        {
         CWndClient *wndclient=(CWndClient*) obj;
         wndclient.ColorBackground(clrNONE);
         wndclient.ColorBorder(clrNONE);
         //---
         int client_total=wndclient.ControlsTotal();
         for(int j=0;j<client_total;j++)
           {
            CWnd*client_obj=wndclient.Control(j);
            string client_name=client_obj.Name();
            if(client_name=="Button1")
              {
               CButton *button=(CButton*) client_obj;
               button.ColorBackground(clrNONE);
               ChartRedraw();
              }
           }
         ChartRedraw();
        }
     }
   return(CDialog::OnDialogDragStart());
  }

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

Второй наш обработчик — "OnDialogDragProcess" (продолжение перемещения панели) менять не будем. А вот третий — "OnDialogDragEnd" (окончание перемещения панели) изменим, так как нужно возвратить цвет для кнопки:

//+------------------------------------------------------------------+
//| End dragging the dialog box                                      |
//+------------------------------------------------------------------+
bool CLivePaneTransparentButton::OnDialogDragEnd(void)
  {
   string prefix=Name();
   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Border")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(CONTROLS_DIALOG_COLOR_BG);
         panel.ColorBorder(CONTROLS_DIALOG_COLOR_BORDER_LIGHT);
         ChartRedraw();
        }
      if(name==prefix+"Back")
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(CONTROLS_DIALOG_COLOR_BG);
         color border=(m_panel_flag) ? CONTROLS_DIALOG_COLOR_BG : CONTROLS_DIALOG_COLOR_BORDER_DARK;
         panel.ColorBorder(border);
         ChartRedraw();
        }
      if(name==prefix+"Client")
        {
         CWndClient *wndclient=(CWndClient*) obj;
         wndclient.ColorBackground(CONTROLS_DIALOG_COLOR_CLIENT_BG);
         wndclient.ColorBorder(CONTROLS_DIALOG_COLOR_CLIENT_BORDER);
         //---
         int client_total=wndclient.ControlsTotal();
         for(int j=0;j<client_total;j++)
           {
            CWnd*client_obj=wndclient.Control(j);
            string client_name=client_obj.Name();
            if(client_name=="Button1")
              {
               CButton *button=(CButton*) client_obj;
               button.ColorBackground(CONTROLS_BUTTON_COLOR_BG);
               ChartRedraw();
              }
           }
         ChartRedraw();
        }
     }
   return(CDialog::OnDialogDragEnd());
  }

Теперь в коде "Live panel and transparent Button.mq5" полностью реализовано изменение цвета панели и кнопки при перемещении панели:

Live panel and transparent Button


Добавим на панель две кнопки: управляем цветом фона панели и цветом заголовка

Это пример содержится в коде "Live panel and button Clicks.mq5", и создан он на основе предыдущего кода "Live panel and transparent Button.mq5". Только теперь вместо событий перемещения панели мы будем "ловить" события клика по кнопкам.  Также на панели будут две кнопки: одна кнопка отвечает за смену цвета фона панели, вторая — за смену цвета ее заголовка .

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

//+------------------------------------------------------------------+
//| Class CLivePaneButtonClicks                                      |
//| Usage: main dialog of the Controls application                   |
//+------------------------------------------------------------------+
class CLivePaneButtonClicks : public CAppDialog
  {
private:
   CButton           m_button1;                       // the button object
   CButton           m_button2;                       // the button object

public:
                     CLivePaneButtonClicks(void);
                    ~CLivePaneButtonClicks(void);
   //--- create
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   //--- chart event handler
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

protected:
   //--- create dependent controls
   bool              CreateButton1(void);
   bool              CreateButton2(void);
   //--- handlers of the dependent controls events
   void              OnClickButton1(void);
   void              OnClickButton2(void);

  };
//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CLivePaneButtonClicks)
ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
EVENT_MAP_END(CAppDialog)
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+

Обработчик есть не что иное, как метод OnEvent, записанный макросами из блока "Events" и из блока "Macro of event handling map" файла "Defines.mqh" (подробнее см. в статье Как создать графическую панель любой сложности и как это работает).

Обработчик читается так:

Обработчики "OnClickButton1" и "OnClickButton2"

В обоих обработчиках организован обход в цикле по всем объектам, из которых состоит панель (они перечислены в пункте Как это сделано) — в данном случае по всем объектам объект-панели "ExtDialog". В результате вызывается метод CWndContainer::ControlsTotal()

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CLivePaneButtonClicks::OnClickButton1(void)
  {
   string prefix=Name();
   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Client")
        {
         CWndClient *wndclient=(CWndClient*) obj;
         color clr=(color)GETRGB(XRGB(rand()%255,rand()%255,rand()%255));
         wndclient.ColorBackground(clr);
         ChartRedraw();
         return;
        }
     }
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CLivePaneButtonClicks::OnClickButton2(void)
  {
   string prefix=Name();
   int total=ExtDialog.ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=ExtDialog.Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Caption")
        {
         CEdit *edit=(CEdit*) obj;
         color clr=(color)GETRGB(XRGB(rand()%255,rand()%255,rand()%255));
         edit.ColorBackground(clr);
         ChartRedraw();
         return;
        }
     }
  }

В обработчике "OnClickButton1" ищем объект-клиентскую область с именем prefix+"Client" (это будет объект класса CWndClient), а в обработчике "OnClickButton2" — объект-заголовок с именем prefix+"Caption" (это будет объект класса CEdit). В обоих случаях рандомно выбираем цвет фона для найденных обьектов. Вот как выглядит результат:

Live panel and button Clicks


Наследуемся от CAppDialog

Схема реализации отличается от применяемой в примерах стандартной библотеки (\MQL5\Experts\Examples\Controls\ и \MQL5\Indicators\Examples\Panels\SimplePanel\), а именно: в подключаемом файле "MyAppDialog.mqh" создаётся класс "CMyAppDialog", унаследованный от CAppDialog. В классе реализованы только три метода управления цветом формы и заголовка. В нем нет методов создания добавленных элементов управления, обработчика OnEvent и обработчиков клика по кнопкам (добавленным элементам управления). 

Объекты класса CButton (добавленные элементы управления, две кнопки) создаются в запускаемом файле "MyAppWindow.mq5". Также в файле "MyAppWindow.mq5", в обработчике OnChartEvent, отлавливаются события клика по кнопкам и вызываются методы изменения цвета.

MyAppDialog.mqh

В нашем классе добавим три метода: 

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

//+------------------------------------------------------------------+
//|                                                  MyAppDialog.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property description "Класс CMyAppDialog, унаслелованный от CAppDialog"
#property description "Добавлены методы для установки цвета фона и заголовка"
#include <Controls\Dialog.mqh>
//+------------------------------------------------------------------+
//| Class CLivePanelTwoButtons                                       |
//| Usage: main dialog of the Controls application                   |
//+------------------------------------------------------------------+
class CMyAppDialog : public CAppDialog
  {
public:
   void              ColorBackground(const color clr);
   color             ColorCaption(void);
   void              ColorCaption(const color clr);
//--- конструтор и деструктор   
public:
                     CMyAppDialog(void){};
                    ~CMyAppDialog(void){};
  };
//+------------------------------------------------------------------+
//| Устанавливает цвет фона                                          |
//+------------------------------------------------------------------+
void CMyAppDialog::ColorBackground(const color clr)
  {
   string prefix=Name();
   int total=ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Client")
        {
         CWndClient *wndclient=(CWndClient*) obj;
         wndclient.ColorBackground(clr);
         ChartRedraw();
         return;
        }
     }
//---     
  } 
//+------------------------------------------------------------------+
//| Устанавливает цвет заголовка                                     |
//+------------------------------------------------------------------+
void CMyAppDialog::ColorCaption(const color clr)
  {
   string prefix=Name();
   int total=ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Caption")
        {
         CEdit *edit=(CEdit*) obj;
         edit.ColorBackground(clr);
         ChartRedraw();
         return;
        }
     }
//---     
  }  
//+------------------------------------------------------------------+
//| Получает цвет заголовка                                          |
//+------------------------------------------------------------------+
color CMyAppDialog::ColorCaption(void)
  {
   string prefix=Name();
   int total=ControlsTotal();
   color clr=clrNONE;
   for(int i=0;i<total;i++)
     {
      CWnd*obj=Control(i);
      string name=obj.Name();
      //---
      if(name==prefix+"Caption")
        {
         CEdit *edit=(CEdit*) obj;
         clr=edit.ColorBackground(clr);
         return clr;
        }
     }
//--- вернем цвет
   return clr;     
  }  
//+------------------------------------------------------------------+


MyAppWindow.mq5

 "MyAppWindow.mq5" — запускающий файл. В этом файле объявлены макросы XRGB и GETRGB для генерации цвета. В OnInit создаётся панель, добавляются кнопки и сама панель запускается в работу.

//+------------------------------------------------------------------+
//|                                                  MyAppWindow.mq5 |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property description "Приложение MyAppWindow на основе класса CMyAppDialog"
#property description "Добавлены кнопки для установки цвета фона и заголовка"
#include "MyAppDialog.mqh"
#include <Controls\Button.mqh>
//--- макросы для работы с цветом
#define XRGB(r,g,b)    (0xFF000000|(uchar(r)<<16)|(uchar(g)<<8)|uchar(b))
#define GETRGB(clr)    ((clr)&0xFFFFFF)
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
//--- indents and gaps
#define INDENT_LEFT                         (11)      // indent from left (with allowance for border width)
#define INDENT_TOP                          (11)      // indent from top (with allowance for border width)
#define CONTROLS_GAP_X                      (5)       // gap by X coordinate
//--- for buttons
#define BUTTON_WIDTH                        (100)     // size by X coordinate
#define BUTTON_HEIGHT                       (20)      // size by Y coordinate
//---
CMyAppDialog         AppWindow;
CButton              m_button1;                       // the button object
CButton              m_button2;                       // the button object
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create application dialog
   if(!AppWindow.Create(0,"CMyAppDialog: change Back and Caption colors",0,40,40,380,344))
      return(INIT_FAILED);
//--- create dependent controls
   if(!CreateBackButton())
      return(false);
   if(!CreateCaptionButton())
      return(false);
//--- run application
   AppWindow.Run();
//--- succeed
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   Comment("");
//--- destroy dialog
   AppWindow.Destroy(reason);
  }
//+------------------------------------------------------------------+
//| Expert chart event function                                      |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,         // event ID  
                  const long& lparam,   // event parameter of the long type
                  const double& dparam, // event parameter of the double type
                  const string& sparam) // event parameter of the string type
  {
//--- сначала мы обработаем события кнопок 
   if((StringFind(sparam,"Back")!=-1) && id==(CHARTEVENT_OBJECT_CLICK))
     {
      Print(__FUNCSIG__," sparam=",sparam);
      AppWindow.ColorBackground(GetRandomColor());
     }
   if((StringFind(sparam,"Caption")!=-1) && id==(CHARTEVENT_OBJECT_CLICK))
     {
      Print(__FUNCSIG__," sparam=",sparam);
      AppWindow.ColorCaption(GetRandomColor());
     }
//--- а теперь остальные события обработает метод класса CMyAppDialog   
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//| Create the "Button1" button                                      |
//+------------------------------------------------------------------+
bool CreateBackButton(void)
  {
//--- coordinates
   int x1=INDENT_LEFT;        // x1            = 11  pixels
   int y1=INDENT_TOP;         // y1            = 11  pixels
   int x2=x1+BUTTON_WIDTH;    // x2 = 11 + 100 = 111 pixels
   int y2=y1+BUTTON_HEIGHT;   // y2 = 11 + 20  = 32  pixels
//--- create
   if(!m_button1.Create(0,"Back",0,x1,y1,x2,y2))
      return(false);
   if(!m_button1.Text("Back"))
      return(false);
   if(!AppWindow.Add(m_button1))
      return(false);
//--- succeed
   return(true);
  }
//+------------------------------------------------------------------+
//| Create the "Button2"                                             |
//+------------------------------------------------------------------+
bool CreateCaptionButton(void)
  {
//--- coordinates
   int x1=INDENT_LEFT+2*(BUTTON_WIDTH+CONTROLS_GAP_X);   // x1 = 11  + 2 * (100 + 5) = 221 pixels
   int y1=INDENT_TOP;                                    // y1                       = 11  pixels
   int x2=x1+BUTTON_WIDTH;                               // x2 = 221 + 100           = 321 pixels
   int y2=y1+BUTTON_HEIGHT;                              // y2 = 11  + 20            = 31  pixels
//--- create
   if(!m_button2.Create(0,"Caption",0,x1,y1,x2,y2))
      return(false);
   if(!m_button2.Text("Caption"))
      return(false);
   if(!AppWindow.Add(m_button2))
      return(false);
//--- succeed
   return(true);
  }
//+------------------------------------------------------------------+
//| Получает цвет случайным образом                                  |
//+------------------------------------------------------------------+
color GetRandomColor()
  {
   color clr=(color)GETRGB(XRGB(rand()%255,rand()%255,rand()%255));
   return clr;
  }
//+------------------------------------------------------------------+

Также в основном файле находится обработчик событий OnChartEvent. Он безусловно пересылает все события в панель, но при обнаружении события клика по одной из кнопок (CHARTEVENT_OBJECT_CLICK) вызывает методы класса панели (AppWindow.ColorBackground или AppWindow.ColorCaption).

Так работает связка из двух файлов: главного запускающего mq5 и включаемого mqh, в котором находится класс панели.


Наследуемся от CWndClient

В этом примере разберём наследование от CWndClient: создадим объект класса "CWndClient". Этот объект будет содержать такой функционал:

  • создание объекта "CMyWndClient" — клиентской области панели;
  • создание объекта добавления двух кнопок в клиентскую область;
  • обработчики кликов по добавленным кнопкам (изменение цвета фона клиенской области и цвета заголовка панели);
  • кроме того, для клиенской области будет включён горизонтальный скролл (помните, что класс CWndClient представляет собой комбинированный элемент управления "Клиентская область" и является базовым классом для создания областей с полосами прокрутки);
  • соответственно, будут и обработчики кликов по горизонтальному скроллу (перемещение добавленных кнопок по клиенской области).

Остановимся подробнее на файлах MyWndClient.mq5 и MyWndClient.mqh.

MyWndClient.mq5

Отличие от примеров стандартной библотеки (\MQL5\Experts\Examples\Controls\ и \MQL5\Indicators\Examples\Panels\SimplePanel\) состоит в том, что  в подключаемом файле находится класс, наследованный от класса CWndClient 10 — клиентской области. Полный цикл создания панели выглядит так.

  1. Создаётся панель (объект AppWindow класса CAppDialog вызывает метод Create).
  2. Создаётся наша клиентская область (объект ClientArea класса CMyWndClient, из подключаемого файла MyWndClient.mqh вызывает метод Create).
  3. Созданная клиентская область добавляется в панель (по сути, ложится поверх клиенской области панели).
CAppDialog           AppWindow;
CMyWndClient         ClientArea;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create application dialog
   bool result_create=false;
   if(!InpTwoButtonsVisible)
     {
      //--- after creation of the panel, one button will be visible
      result_create=AppWindow.Create(0,"CAppDialog with CMyWndClient",0,40,40,360,344);
     }
   else
     {
      //--- after creation of the panel, will two buttons are visible
      result_create=AppWindow.Create(0,"CAppDialog with CMyWndClient",0,40,40,420,344);
     }
   if(!result_create)
      return(INIT_FAILED);
//--- create the panel
   PrintFormat("Application Rect: Height=%d  Width=%d",AppWindow.Rect().Height(),AppWindow.Rect().Width());
   CRect inner_rect=ClientArea.GetClientRect(GetPointer(AppWindow));
   PrintFormat("Client Area: Height=%d  Width=%d",inner_rect.Height(),inner_rect.Width());
   ClientArea.Create(0,"MyWndClient",0,0,0,inner_rect.Width(),inner_rect.Height());
   AppWindow.Add(ClientArea);
//--- установим владельца
   ClientArea.SetOwner(GetPointer(AppWindow));
//--- скрывать невидимое
   ClientArea.HideInvisble(HideInvisble);
//--- run application
   AppWindow.Run();
//--- succeed
   return(INIT_SUCCEEDED);
  }

В запускаемом файле есть два входных параметра:

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

При создании панели учитывается только один входной параметр — "ширина панели". Вот так мы создаём панель нормальной ширины:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create application dialog
   bool result_create=false;
   if(!InpTwoButtonsVisible)
     {
      //--- after creation of the panel, one button will be visible
      result_create=AppWindow.Create(0,"CAppDialog with CMyWndClient",0,40,40,360,344);
     }
   else
     {
      //--- after creation of the panel, will two buttons are visible
      result_create=AppWindow.Create(0,"CAppDialog with CMyWndClient",0,40,40,420,344);
     }
   if(!result_create)
      return(INIT_FAILED);
//--- create the panel

MyWndClient normal width panel

а так — широкую панель:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create application dialog
   bool result_create=false;
   if(!InpTwoButtonsVisible)
     {
      //--- after creation of the panel, one button will be visible
      result_create=AppWindow.Create(0,"CAppDialog with CMyWndClient",0,40,40,360,344);
     }
   else
     {
      //--- after creation of the panel, will two buttons are visible
      result_create=AppWindow.Create(0,"CAppDialog with CMyWndClient",0,40,40,420,344);
     }
   if(!result_create)
      return(INIT_FAILED);
//--- create the panel

MyWndClient wide panel

Код создания панели отличается только задаваемой шириной (360 и 420), и при создании двух кнопок эта ширина не учитывается. Сравните две последних картинки. А теперь наложите их одну на другую:

CMyWndClient imposition

Видим, что кнопка "Caption" немного выходит за границу панели нормальной ширины — за границу клиенской области. Когда добавляемый элемент управления выходит за границы, он принудительно скрывается от пользователя (но не удаляется и не уничтожается). Процедура определения того, нужно ли скрыть элемент управления, запускается, когда он добавляется в клиентскую область — при вызове метода CWndContainer::Add. В нашем примере метод Add вызывается в AddButton2:

//+------------------------------------------------------------------+
//| Create the "Button2"                                             |
//+------------------------------------------------------------------+
bool CMyWndClient::AddButton2(void)
  {
...
   if(!Add(m_button2))
     {
      Print("Add(m_button2) --> false");
      return(false);
     }
//--- succeed
   return(true);
  }
//+------------------------------------------------------------------+
//| Add control to the group (by reference)                          |
//+------------------------------------------------------------------+
bool CWndContainer::Add(CWnd &control)
  {
//--- add by pointer
   return(Add((CWnd*)GetPointer(control)));
  }

и последовательно вызывается самое главное — именно здесь определяется, скрывать или нет элемент управления

//+------------------------------------------------------------------+
//| Add control to the group (by pointer)                            |
//+------------------------------------------------------------------+
bool CWndContainer::Add(CWnd *control)
  {
//--- check of pointer
   if(control==NULL)
      return(false);
//--- correct the coordinates of added control
   control.Shift(Left(),Top());
 //--- "projecting" the group flag "visibility" to the added element
   if(IS_VISIBLE && control.IsVisible())
     {
      //--- element will be "visible" only if the group is "visible" and the element is completely "within" this group
      control.Visible(Contains(control));
     }
   else
      control.Hide();
//--- "projecting" the group flag "enabled" to the added element
   if(IS_ENABLED)
      control.Enable();
   else
      control.Disable();
//--- adding
   return(m_controls.Add(control));
  }

Задание видимости объекта создаётся ТОЛЬКО В МОМЕНТ ДОБАВЛЕНИЯ элемента управления в клиенскую область. Это и есть недостаток, который может проявиться после сворачивания и разворачивания панели.

Пример: оба входных параметра установить в "false", затем свернуть и развернуть панель. В итоге после создания панели кнопка "Caption" создаётся, но она визуально скрыта (так как кнопка выходит за границу клиентской области — она скрывается при добавлении в клиентскую область). Но после того, как панель свернули и развернули, проверка на видимость добавленных элементов уже не производится, и поэтому кнопка Caption будет видна:

CMyWndClient button caption

В данном файле находится класс, унаследованный от класса CWndClient — клиентской области. В этом файле сосредоточен весь функционал:

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

Горизонтальный скролл и скрытые элементы управления

Так как класс панели наследуется от класса CWndClient, а класс CWndClient представляет собой комбинированный элемент управления "Клиентская область" и является базовым классом для создания областей с полосами прокрутки, то включим в нашей клиентской области горизонтальную полосу прокрутки:

//+------------------------------------------------------------------+
//| Создание панели                                                  |
//+------------------------------------------------------------------+
bool CMyWndClient::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
//---
   if(!CWndClient::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
//--- enable horizontal scrollbar
   if(!HScrolled(true))
      return(false);
   m_scroll_h.MaxPos(5);
   m_scroll_h.CurrPos(5);
   Print("CurrPos: ",m_scroll_h.CurrPos());
   if(!AddButton1())

Чтобы скролл можно было двигать, задаём количество его градаций: значение максимальной позиции, и сразу позиционируем скролл в крайнее правое положение

Горизонтальный скролл также используется для отлавливания события разворачивания панели — для этого "ловим" событие ON_SHOW для объекта m_scroll_h и вызываем обработчик OnShowScrollH:

//+------------------------------------------------------------------+
//| Event Handling                                                   |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CMyWndClient)
ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
ON_EVENT(ON_SHOW,m_scroll_h,OnShowScrollH)
EVENT_MAP_END(CWndClient)

EVENT_MAP_BEGIN есть не что иное, как метод OnEvent, записанный макросами из блока "Events" и из блока "Macro of event handling map" файла "Defines.mqh" (подробнее см. в статье Как создать графическую панель любой сложности и как это работает).

В обработчике OnShowScrollH проверяем значение внутреннего флага m_hide_invisble (этот флаг принимает значение входной переменной "скрыть невидимое" файла MyWndClient.mq5 через метод CMyWndClient::HideInvisble), и если не нужно скрывать элементы — просто выходим из процедуры:

//+------------------------------------------------------------------+
//| Появился скролл скрываем/отображаем кнопки                       |
//+------------------------------------------------------------------+
void CMyWndClient::OnShowScrollH(void)
  {
   if(!m_hide_invisble)
      return;
   int total=CWndClient::ControlsTotal();
   for(int i=0;i<total;i++)
     {
      CWnd*obj=Control(i);
      string name=obj.Name();
      //---
      if(StringFind(name,"Button")!=-1)
        {
         CButton *button=(CButton*)obj;
         button.Visible(Contains(GetPointer(button)));
         ChartRedraw();
        }
     }
  }

Если нужно скрывать невидимые элементы — тогда в цикле по всем объектам клиентской области ищем объекты, содержащие в своём имени "Button", и принудительно проверяем/назначаем видимость.

Клики по горизонтальному скроллу

Для кликов по горизонтальному скроллу зададим возможность перемещения двух добавленных кнопок по нашей клиентской области. Для этого переопределим обработчики OnScrollLineRight и OnScrollLineLeft — обработчики клика по кнопкам горизонтального скролла. Если был клик на правой кнопке горизонтального скролла, значит перемещаем кнопки (метод ShiftButton) на шаг m_scroll_size. Если был клик на левой кнопке — то двигаем кнопки на шаг "-m_scroll_size", то есть задаём отрицательное смещение:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
bool CMyWndClient::OnScrollLineRight(void)
  {
   Print(__FUNCTION__);
   ShiftButton(GetPointer(m_button1),m_scroll_size);
   ShiftButton(GetPointer(m_button2),m_scroll_size);
   return(true);
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
bool CMyWndClient::OnScrollLineLeft(void)
  {
   Print(__FUNCTION__);
   ShiftButton(GetPointer(m_button1),-m_scroll_size);
   ShiftButton(GetPointer(m_button2),-m_scroll_size);
   return(true);
  }
В методе CMyWndClient::ShiftButton получаем объект с координатами кнопки, задаём сдвиг координат, смещаем кнопку и принудительно проверяем/задаём видимость кнопки:

//+------------------------------------------------------------------+
//| Сдвиг кнопки влево или вправо (в зависимости от shift)           |
//+------------------------------------------------------------------+
bool CMyWndClient::ShiftButton(CButton *button,const int shift)
  {
   Print(__FUNCTION__);
//--- смещаем кнопку
   CRect rect=button.Rect();
   rect.Move(rect.left+shift,rect.top);
   button.Move(rect.left,rect.top);
   button.Visible(Contains(GetPointer(button)));
   return(true);
  }

Вот как это выглядит (не забудьте параметр "скрыть невидимое" поставить в значение true):

CMyWndClient move buttons



Новые проекты. Чем они могут помочь в изучении панелей?

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

К счастью, не так давно были представлены Новые Проекты в редакторе MetaEditor.

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

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

Попробуем создать проект из последнего файла "Live panel and button Clicks.mq5". Для этого кликнем правой кнопкой по файлу "Live panel and button Clicks.mq5" и в выпадающем меню выберем пункт "Новый проект из исходного файла":

New Project from Source

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

New Project

и "Wnd.mqh" (в нем класс CWnd), и "Dialog.mqh (в нем классы CDialog и CAppDialog), и Button.mhq (класс CButton). Из этой вкладки удобно переходить в нужный класс. Это намного удобнее перехода по вкладкам редактора MetaEditor. Например, у меня есть небольшой "зоопарк" из самых различных файлов. Перейти из него, например в Dialog.mqh через поиск во вкладках проблематично:

Many windows

Заключение

В статье показан довольно необычный способ доступа к свойствам "Цвет фона", "Цвет рамки" и "Цвет заголовка" для элементов управления панели. Лично я раньше такого способа не встречал. Весь фокус кроется в знании, что все элементы панели наследуются от родительского класса CWnd, а значит объект — класс создаваемой панели  — есть контейнер для всех элементов управления. Значит, можно в цикле пройтись по всем элементам управления и получить/установить нужные свойства.

Наименование файла Комментарий
 Live panel.mq5  Панель без добавленных элементов управления. При перетаскивании становится прозрачной
 Live panel and Button.mq5  Панель с добавленной кнопкой. При перетаскивании панель становится прозрачной, а кнопка остаётся в своем цвете
 Live panel and transparent Button.mq5  Панель с добавленной кнопкой. При перетаскивании и панель, и кнопка становятся прозрачными
 Live panel and button Clicks.mq5  Панель с двумя кнопками. Обработка кликов по кнопкам: генерация цвета фона для панели и заголовка
 MyAppWindow.mq5 и MyAppDialog.mqh  Пример создания панели наследованием от CAppDialog
 MyWndClient.mq5 и MyWndClient.mqh  Пример создания панели наследованием от CWndClient — от класса клиентской области