English 中文 Español Deutsch 日本語 Português
preview
DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления

DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления

MetaTrader 5Примеры | 22 апреля 2022, 16:15
1 326 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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

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


Доработка классов библиотеки

В файле \MQL5\Include\DoEasy\Defines.mqh у нас прописаны макроподстановки для указания значений по умолчанию для некоторых свойств объектов библиотеки.

В блоке параметров канваса изменим наименования макроподстановки CLR_FORE_COLOR на CLR_DEF_FORE_COLOR, добавим значение по умолчанию для непрозрачности объектов-графических элементов, и ещё несколько значений по умолчанию для свойств объекта-тени:

//--- Параметры канваса
#define PAUSE_FOR_CANV_UPDATE          (16)                       // Частота обновления канваса
#define CLR_CANV_NULL                  (0x00FFFFFF)               // Ноль для канваса с альфа-каналом
#define CLR_DEF_FORE_COLOR             (C'0x2D,0x43,0x48')        // Цвет по умолчанию для текстов объектов на канвасе
#define CLR_DEF_OPACITY                (200)                      // Непрозрачность цвета по умолчанию для объектов на канвасе
#define CLR_DEF_SHADOW_COLOR           (C'0x6B,0x6B,0x6B')        // Цвет по умолчанию для теней объектов на канвасе
#define CLR_DEF_SHADOW_OPACITY         (127)                      // Непрозрачность цвета по умолчанию для теней объектов на канвасе
#define DEF_SHADOW_BLUR                (4)                        // Размытие по умолчанию для теней объектов на канвасе
#define DEF_FONT                       ("Calibri")                // Шрифт по умолчанию
#define DEF_FONT_SIZE                  (8)                        // Размер шрифта по умолчанию
#define OUTER_AREA_SIZE                (16)                       // Размер одной стороны внешней области вокруг рабочего пространства формы
#define DEF_FRAME_WIDTH_SIZE           (3)                        // Ширина рамки формы/панели/окна по умолчанию
//--- Параметры графических объектов

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

В перечислении типов графических элементов у нас не совсем логично расположены константы:

//+------------------------------------------------------------------+
//| Список типов графических элементов                               |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Стандартный графический объект
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Расширенный стандартный графический объект
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Элемент
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Объект тени
   GRAPH_ELEMENT_TYPE_FORM,                           // Форма
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Окно
   //--- WinForms
   GRAPH_ELEMENT_TYPE_PANEL,                          // Windows Forms Panel
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Список типов графических элементов                               |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Стандартный графический объект
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Расширенный стандартный графический объект
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Объект тени
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Элемент
   GRAPH_ELEMENT_TYPE_FORM,                           // Форма
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Окно
   //--- WinForms
   GRAPH_ELEMENT_TYPE_PANEL,                          // Windows Forms Panel
  };
//+------------------------------------------------------------------+

Теперь мы сможем быстро выбирать нужные объекты для их обработки в методах библиотеки.


В файле \MQL5\Include\DoEasy\Data.mqh впишем новый индекс сообщения библиотеки:

//--- CForm
   MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ,      // Не удалось создать новый объект для тени
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ,          // Не удалось создать новый объект-копировщик пикселей
   MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST,            // В списке уже есть объект-копировщик пикселей с идентификатором 
   MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST,             // В списке нет объекта-копировщика пикселей с идентификатором 
   MSG_FORM_OBJECT_ERR_NOT_INTENDED,                  // Метод не предназначен для создания такого объекта: 

и текст сообщения, соответствующий вновь добавленному индексу:

//--- CForm
   {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"},
   {"Не удалось создать новый объект для тени","Failed to create new object for shadow"},
   {"Не удалось создать новый объект-копировщик пикселей","Failed to create new pixel copier object"},
   {"В списке уже есть объект-копировщик пикселей с идентификатором ","There is already a pixel copier object in the list with ID "},
   {"В списке нет объекта-копировщика пикселей с идентификатором ","No pixel copier object with ID "},
   {"Метод не предназначен для создания такого объекта: ","The method is not intended to create such an object: "},


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

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

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

В файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh в его защищённой секции объявим указатель на базовый объект — к которому прикреплён графический элемент, а в приватной секции объявим переменные для хранения смещения его координат относительно базового объекта:

//+------------------------------------------------------------------+
//| Класс объекта графического элемента                              |
//+------------------------------------------------------------------+
class CGCnvElement : public CGBaseObj
  {
protected:
   CGCnvElement     *m_element_base;                           // Указатель на родительский элемент
   CCanvas           m_canvas;                                 // Объект класса CCanvas
   CPause            m_pause;                                  // Объект класса "Пауза"
   bool              m_shadow;                                 // Наличие тени
   color             m_chart_color_bg;                         // Цвет фона графика
   uint              m_duplicate_res[];                        // Массив для хранения копии данных ресурса

//--- Создаёт (1) структуру объекта, (2) объект из структуры
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
   
private:
   int               m_shift_coord_x;                          // Смещение координаты X относительно базового объекта
   int               m_shift_coord_y;                          // Смещение координаты Y относительно базового объекта
   struct SData

В публичной секции напишем методы для установки и возврата указателя на базовый объект:

//--- Создаёт элемент
   bool              Create(const long chart_id,
                            const int wnd_num,
                            const string name,
                            const int x,
                            const int y,
                            const int w,
                            const int h,
                            const color colour,
                            const uchar opacity,
                            const bool redraw=false);

//--- (1) Устанавливает, (2) возвращает указатель на родительский элемент
   void              SetBase(CGCnvElement *element)                                    { this.m_element_base=element;               }
   CGCnvElement     *GetBase(void)                                                     { return this.m_element_base;                }
//--- Возвращает указатель на объект-канвас
   CCanvas          *GetCanvasObj(void)                                                { return &this.m_canvas;                     }

Метод Move() сделаем виртуальным:

//--- Возвращает размер массива-копии графического ресурса
   uint              DuplicateResArraySize(void)                                       { return ::ArraySize(this.m_duplicate_res);  }
   
//--- Обновляет координаты (сдвигает канвас)
   virtual bool      Move(const int x,const int y,const bool redraw=false);

//--- Сохраняет изображение в массив
   bool              ImageCopy(const string source,uint &array[]);

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

В публичной же секции напишем ещё методы — для установки и возврата относительных координат объекта:

protected:
//--- Защищённый конструктор
                     CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  const long    chart_id,
                                  const int     wnd_num,
                                  const string  name,
                                  const int     x,
                                  const int     y,
                                  const int     w,
                                  const int     h);
public:
//--- (1) Устанавливает, (2) возвращает смещение координаты X относительно базового объекта
   void              SetCoordXRelative(const int value)                                { this.m_shift_coord_x=value;                }
   int               CoordXRelative(void)                                        const { return this.m_shift_coord_x;               }
//--- (1) Устанавливает, (2) возвращает смещение координаты Y относительно базового объекта
   void              SetCoordYRelative(const int value)                                { this.m_shift_coord_y=value;                }
   int               CoordYRelative(void)                                        const { return this.m_shift_coord_y;               }
   
//--- Обработчик событий


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

//--- Конструктор по умолчанию/Деструктор
                     CGCnvElement() : m_shadow(false),m_chart_color_bg((color)::ChartGetInteger(::ChartID(),CHART_COLOR_BACKGROUND))
                        { this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_base=NULL; this.m_shift_coord_x=0; this.m_shift_coord_y=0; }
                    ~CGCnvElement()
                        { this.m_canvas.Destroy();             }

Если указатель на базовый объект равен NULL, то это будет означать, что этот объект не прикреплён ни к какому-либо другому графическому элементу.

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

//--- Возвращает (1) цвет фона, (2) непрозрачность, координату (3) правого, (4) нижнего края элемента
   color             ColorBackground(void)               const { return this.m_color_bg;                                               }
   uchar             Opacity(void)                       const { return this.m_opacity;                                                }
   int               RightEdge(void)                     const { return this.CoordX()+this.m_canvas.Width();                           }
   int               BottomEdge(void)                    const { return this.CoordY()+this.m_canvas.Height();                          }
//--- Возвращает относительную координату (1) правого, (2) нижнего края элемента
   int               RightEdgeRelative(void)             const { return this.CoordXRelative()+this.m_canvas.Width();                   }
   int               BottomEdgeRelative(void)            const { return this.CoordYRelative()+this.m_canvas.Height();                  }
//--- Возвращает координату (1) X, (2) Y, (3) ширину, (4) высоту элемента,

А метод BringToTop() сделаем виртуальным:

//--- Устанавливает объект выше всех
   virtual void      BringToTop(void)                          { CGBaseObj::SetVisible(false,false); CGBaseObj::SetVisible(true,false);}
//--- (1) Показывает, (2) скрывает элемент
   virtual void      Show(void)                                { CGBaseObj::SetVisible(true,false);                                    }
   virtual void      Hide(void)                                { CGBaseObj::SetVisible(false,false);                                   }

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

В параметрическом и защищённом конструкторах инициализируем указатель на базовый объект и относительное смещение координат:

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const int      element_id,
                           const int      element_num,
                           const long     chart_id,
                           const int      wnd_num,
                           const string   name,
                           const int      x,
                           const int      y,
                           const int      w,
                           const int      h,
                           const color    colour,
                           const uchar    opacity,
                           const bool     movable=true,
                           const bool     activity=true,
                           const bool     redraw=false) : m_shadow(false)
  {
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
   this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.m_type_element=element_type;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.m_color_bg=colour;
   this.m_opacity=opacity;
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw))
     {

Точно так же вписана инициализация указателя и переменных и в защищённом конструкторе.


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

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

В файле \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh в приватной секции объявим переменные для хранения смещения тени относительно графического объекта, который отбрасывает эту тень:

//+------------------------------------------------------------------+
//| Класс объекта Тени                                               |
//+------------------------------------------------------------------+
class CShadowObj : public CGCnvElement
  {
private:
   color             m_color_shadow;                  // Цвет тени
   uchar             m_opacity_shadow;                // Непрозрачность тени
   int               m_offset_x;                      // Смещение тени по оси X
   int               m_offset_y;                      // Смещение тени по оси Y

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

//--- Поддерживаемые свойства объекта (1) целочисленные, (2) строковые
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)  { return true; }
   
//--- Возвращает смещение тени по оси (1) X, (2) Y
   int               OffsetX(void)                                      const { return this.m_offset_x;        }
   int               OffsetY(void)                                      const { return this.m_offset_y;        }

//--- Рисует тень объекта

В конструкторе класса инициализируем значения этих переменных:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CShadowObj::CShadowObj(const long chart_id,
                       const int subwindow,
                       const string name,
                       const int x,
                       const int y,
                       const int w,
                       const int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_SHADOW_OBJ,chart_id,subwindow,name,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GSHADOW; 
   CGCnvElement::SetColorBackground(clrNONE);
   CGCnvElement::SetOpacity(0);
   CGCnvElement::SetActive(false);
   this.m_opacity_shadow=127;
   color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100);
   this.m_color_shadow=CGCnvElement::ChangeColorLightness(gray,-50);
   this.m_shadow=false;
   this.m_visible=true;
   this.m_offset_x=0;
   this.m_offset_y=0;
   CGCnvElement::Erase();
  }
//+------------------------------------------------------------------+


В методе, рисующем объект тени впишем значения смещения, передаваемые в аргументах метода в эти новые переменные:

//+------------------------------------------------------------------+
//| Рисует тень объекта                                              |
//+------------------------------------------------------------------+
void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value)
  {
//--- Устанавливаем в переменные значения смещения тени по осям X и Y
   this.m_offset_x=shift_x;
   this.m_offset_y=shift_y;
//--- Рассчитываем ширину и высоту рисуемого прямоугольника
   int w=this.Width()-OUTER_AREA_SIZE*2;
   int h=this.Height()-OUTER_AREA_SIZE*2;
//--- Рисуем закрашенный прямоугольник с рассчитанными размерами
   this.DrawShadowFigureRect(w,h);
//--- Рассчитываем радиус размытия, который не может быть больше четверти размера константы OUTER_AREA_SIZE
   int radius=(blur_value>OUTER_AREA_SIZE/4 ? OUTER_AREA_SIZE/4 : blur_value);
//--- Если размыть фигуру не удалось - уходим из метода (ошибку в журнал выведет GaussianBlur())
   if(!this.GaussianBlur(radius))
      return;
//--- Смещаем объект тени на указанные в аргументах метода смещения по X и Y и обновляем канвас
   CGCnvElement::Move(this.CoordX()+this.m_offset_x,this.CoordY()+this.m_offset_y);
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

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


В файле объекта-формы \MQL5\Include\DoEasy\Objects\Graph\Form.mqh переместим два метода CreateNameDependentObject() и CreateShadowObj()
из приватной секции класса в защищённую:

protected:
   int               m_frame_width_left;                       // Ширина рамки формы слева
   int               m_frame_width_right;                      // Ширина рамки формы справа
   int               m_frame_width_top;                        // Ширина рамки формы сверху
   int               m_frame_width_bottom;                     // Ширина рамки формы снизу
//--- Инициализирует переменные
   void              Initialize(void);
   void              Deinitialize(void);
//--- Создаёт объект для тени
   void              CreateShadowObj(const color colour,const uchar opacity);
//--- Возвращает имя зависимого объекта
   string            CreateNameDependentObject(const string base_name)  const
                       { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name;   }
   
public:

Эти методы должны быть доступными в объектах классов-наследников, поэтому их место в защищённой секции класса.

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

//--- Создаёт новый графический объект
   virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int element_num,
                                          const string name,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity);
//--- Возвращает начальные координаты привязанного объекта
   virtual void      GetCoords(int &x,int &y);

Метод, создающий новый привязанный объект, расположен внутри класса объекта-формы. Объект-форма унаследован от класса объекта-графического элемента. Соответственно, эти два класса (элемент и форма) известны и видны в объекте класса-формы. И они здесь могут создаваться. Но нам необходимо иметь возможность создания и других элементов управления — окно, панель и другие объекты классов, которые будем делать в процессе разработки объектов WinForms. Но они не будут видны в этом классе. Соответственно, в каждом из них будет присутствовать виртуальный метод CreateNewGObject(), в котором будет прописан код для создания тех элементов управления, которые видны внутри этих классов.

В публичной секции класса объявим виртуальные методы, которые мы сделали виртуальными в классе объекта-графического элемента:

public:
//--- Возвращает (1) состояние мышки относительно формы, координату (2) X, (3) Y курсора
   ENUM_MOUSE_FORM_STATE MouseFormState(const int id,const long lparam,const double dparam,const string sparam);
   int               MouseCursorX(void)            const { return this.m_mouse.CoordX();     }
   int               MouseCursorY(void)            const { return this.m_mouse.CoordY();     }
//--- Устанавливает для графика флаги прокрутки колёсиком мышки, контекстного меню и инструмента "перекрестие"
   void              SetChartTools(const bool flag);
//--- (1) Устанавливает, (2) возвращает смещение координат X и Y относительно курсора
   void              SetOffsetX(const int value)         { this.m_offset_x=value;            }
   void              SetOffsetY(const int value)         { this.m_offset_y=value;            }
   int               OffsetX(void)                 const { return this.m_offset_x;           }
   int               OffsetY(void)                 const { return this.m_offset_y;           }

//--- Обновляет координаты (сдвигает канвас)
   virtual bool      Move(const int x,const int y,const bool redraw=false);
//--- Устанавливает приоритет графического объекта на получение события нажатия мышки на графике
   virtual bool      SetZorder(const long value,const bool only_prop);
//--- Устанавливает объект выше всех
   virtual void      BringToTop(void);
   
//--- Обработчик событий

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

Метод, возвращающий объект тени, ранее по ошибке возвращал тип объекта-графического элемента CGCnvElement. Критической ошибки в этом нет, за исключением того, что нам будут не доступны методы, написанные в классе объекта-тени.
Поэтому исправим эту ошибку, чтобы метод возвращал тип объекта-тени, и добавим два метода для возврата количества привязанных элементов управления и элемента управления по его индексу в списке привязанных объектов:

//--- Возвращает (1) себя, список (2) присоединённых объектов, (3) объект тени
   CForm            *GetObject(void)                                          { return &this;                  }
   CArrayObj        *GetListElements(void)                                    { return &this.m_list_elements;  }
   CShadowObj       *GetShadowObj(void)                                       { return this.m_shadow_obj;      }
//--- Возвращает указатель на (1) объект анимаций, список (2) текстовых, (3) прямоугольных кадров анимаций
   CAnimations      *GetAnimationsObj(void)                                   { return this.m_animations;      }
   CArrayObj        *GetListFramesText(void)
                       { return(this.m_animations!=NULL ? this.m_animations.GetListFramesText() : NULL);       }
   CArrayObj        *GetListFramesQuad(void)
                       { return(this.m_animations!=NULL ? this.m_animations.GetListFramesQuad() : NULL);       }

//--- Возвращает (1) количество привязанных элементов, (2) привязанный жлемент по индексу в списке
   int               ElementsTotal(void)                       const { return this.m_list_elements.Total();    }
   CGCnvElement     *GetElement(const int index)                     { return this.m_list_elements.At(index);  }
   
//--- Устанавливает (1) цветовую схему, (2) стиль формы


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

//--- Создаёт новый присоединённый элемент
   bool              CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool activity);
//--- Добавляет новый присоединённый элемент


В методе, рисующем тень объекта, впишем значение по умолчанию для величины размытия тени вместо ранее передаваемого значения 4:

//--- Добавляет новый присоединённый элемент
   bool              AddNewElement(CGCnvElement *obj,const int x,const int y);

//--- Рисует тень объекта
   void              DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=DEF_SHADOW_BLUR);

//--- Рисует рамку формы


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

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                      const int obj_num,
                                      const string obj_name,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool movable,
                                      const bool activity)
  {
   string name=this.CreateNameDependentObject(obj_name);
   CGCnvElement *element=NULL;
   //--- В зависимости от типа создаваемого объекта
   switch(type)
     {
      //--- создаём объект-графический элемент
      case GRAPH_ELEMENT_TYPE_ELEMENT :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
        break;
      //--- создаём объект-форму
      case GRAPH_ELEMENT_TYPE_FORM :
         element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   return element;
  }
//+------------------------------------------------------------------+


Метод, создающий новый присоединённый элемент, претерпел значительные изменения:

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//+------------------------------------------------------------------+
bool CForm::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                             const int x,
                             const int y,
                             const int w,
                             const int h,
                             const color colour,
                             const uchar opacity,
                             const bool activity)
  {
//--- Если тип создаваемого графического элемента меньше, чем "элемент" - сообщаем об эом и возвращаем false
   if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19));
      return false;
     }
//--- Задаём номер элемента в списке
   int num=this.m_list_elements.Total()+1;
//--- Создаём имя графического элемента
   string ns=(::StringLen((string)num)<2 ? ::IntegerToString(num,2,'0') : (string)num);
   string name="Elm"+ns;
//--- Получаем начальные координаты объекта относительно системы координат базового объекта
   int elm_x=x;
   int elm_y=y;
   this.GetCoords(elm_x,elm_y);
//--- Создаём новый графический элемент
   CGCnvElement *obj=this.CreateNewGObject(element_type,num,name,elm_x,elm_y,w,h,colour,opacity,false,activity);
   if(obj==NULL)
      return false;
//--- и добавляем его в список привязанных графических элементов
   if(!this.AddNewElement(obj,elm_x,elm_y))
     {
      delete obj;
      return false;
     }
//--- Устанавливаем минимальные свойства привязанному графическому элементу
   obj.SetBase(this.GetObject());
   obj.SetCoordXRelative(x);
   obj.SetCoordYRelative(y);
   obj.SetZorder(this.Zorder(),false);
//--- Рисуем добавленный объект и возвращаем true
   obj.Erase(colour,opacity,true);
   return true;
  }
//+------------------------------------------------------------------+

Вся логика расписана в комментариях к коду. При создании имени графического объекта будем использовать имя базового объекта + строка "_Elm" + номер объекта в списке. Для номера объекта будем использовать такую логику: если номер объекта от 1 до 9 включительно, то к цифре добавляем лидирующий ноль, чтобы было 01, 02, 03,.., .., 08, 09. Для 10 и далее — ничего прибавлять не будем. В этом методе создаётся только имя объекта, а имя базового к нему приписывается в методе CreateNewGObject() в первой его строке при вызове метода CreateNameDependentObject().

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

//+------------------------------------------------------------------+
//| Добавляет новый присоединённый элемент                           |
//+------------------------------------------------------------------+
bool CForm::AddNewElement(CGCnvElement *obj,const int x,const int y)
  {
   if(obj==NULL)
      return false;
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      CGCnvElement *elm=this.m_list_elements.At(i);
      if(elm==NULL)
         continue;
      if(elm.Name()==obj.Name())
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_OBJ_ALREADY_IN_LIST),": ",obj.NameObj());
         return false;
        }
     }
   if(!this.m_list_elements.Add(obj))
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST),": ",obj.NameObj());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Тот поиск, который был ранее

   this.m_list_elements.Sort(SORT_BY_CANV_ELEMENT_NAME_OBJ);
   int index=this.m_list_elements.Search(obj);
   if(index>WRONG_VALUE)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_OBJ_ALREADY_IN_LIST),": ",obj.NameObj());
      return false;
     }

по непонятным пока для меня причинам не срабатывает — всегда возвращает наличие объекта с переданным в метод именем в списке привязанных объектов, чего быть не должно. С этим мы разберёмся позже. И вот этот поиск и был заменён на цикл.

Метод, возвращающий начальные координаты привязанного объекта:

//+------------------------------------------------------------------+
//| Возвращает начальные координаты привязанного объекта             |
//+------------------------------------------------------------------+
void CForm::GetCoords(int &x,int &y)
  {
   x=this.CoordX()+this.m_frame_width_left+x;
   y=this.CoordY()+this.m_frame_width_top+y;
  }
//+------------------------------------------------------------------+

Метод возвращает по ссылке значения его относительных координат, переданных по ссылке, на значения, скорректированные на величину ширины рамки слева для кооринаты X и сверху — для координаты Y. Просто потому, что если у объекта есть рамка, то на ней не должны располагаться добавляемые объекты. Поэтому, если передана координата, например, 0, то она будет смещена на значение ширины рамки. Т.е. если рамка имеет ширину 3, то все координаты будут сдвигаться на эту величину.

Виртуальный метод, обновляющий координаты элемента:

//+------------------------------------------------------------------+
//| Обновляет координаты элемента                                    |
//+------------------------------------------------------------------+
bool CForm::Move(const int x,const int y,const bool redraw=false)
  {
   CGCnvElement *base=this.GetBase();
   bool res=true;
//--- Если элемент не перемещаемый, и это базовый объект - уходим
   if(!this.Movable() && base==NULL)
      return false;
//--- Если есть тень у объекта и не удалось записать новые значения координат в свойства объекта-тени - возвращаем false
   if(this.m_shadow)
     {
      if(this.m_shadow_obj==NULL || !this.m_shadow_obj.Move(x-OUTER_AREA_SIZE+this.m_shadow_obj.OffsetX(),y-OUTER_AREA_SIZE+this.m_shadow_obj.OffsetY(),false))
         return false;
     }
//--- Если не удалось записать новые значения координат в свойства графического объекта - возвращаем false
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return false;
//--- В цикле по всем привязанным объектам
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      //--- получаем очередной объект и смещаем его
      CGCnvElement *obj=m_list_elements.At(i);
      if(obj==NULL)
         continue;
      if(!obj.Move(x+this.m_frame_width_left+obj.CoordXRelative(),y+this.m_frame_width_top+obj.CoordYRelative(),false))
         return false;
     }
   //--- Если стоит флаг обновления, и это базовый объект - перерисовываем график.
   if(redraw && base==NULL)
      ::ChartRedraw(this.ChartID());
   //--- Возвращаем true
   return true;
  }
//+------------------------------------------------------------------+

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

Метод, устанавливающий приоритет графического объекта на получение события нажатия мышки на графике:

//+------------------------------------------------------------------+
//| Устанавливает приоритет графического объекта                     |
//| на получение события нажатия мышки на графике                    |
//+------------------------------------------------------------------+
bool CForm::SetZorder(const long value,const bool only_prop)
  {
   if(!CGCnvElement::SetZorder(value,only_prop))
      return false;
   if(this.m_shadow)
     {
      if(this.m_shadow_obj==NULL || !this.m_shadow_obj.SetZorder(value,only_prop))
         return false;
     }
   int total=this.m_list_elements.Total();
   for(int i=0;i<total;i++)
     {
      CGCnvElement *obj=this.m_list_elements.At(i);
      if(obj==NULL)
         continue;
      if(!obj.SetZorder(value,only_prop))
         return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Здесь точно так же, как и в вышерассмотренном методе, добавлен цикл по всем прикреплённым объектам, для которых вызывается этот же метод.

Метод, устанавливающий объект выше всех:

//+------------------------------------------------------------------+
//| Устанавливает объект выше всех                                   |
//+------------------------------------------------------------------+
void CForm::BringToTop(void)
  {
//--- Если стоит флаг использования тени
   if(this.m_shadow)
     {
      //--- Если объект тени создан - переносим его на передний план
      if(this.m_shadow_obj!=NULL)
         this.m_shadow_obj.BringToTop();
     }
//--- Переносим объект на передний план (объект станет выше тени)
   CGCnvElement::BringToTop();
//--- В цикле по всем привязанным объектам
   int total=this.m_list_elements.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередной объект из списка
      CGCnvElement *obj=this.m_list_elements.At(i);
      if(obj==NULL)
         continue;
      //--- и переносим его на передний план
      obj.BringToTop();
     }
  }
//+------------------------------------------------------------------+

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


Доработаем класс объекта панели в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh.

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

//+------------------------------------------------------------------+
//| Класс объекта Panel элементов управления WForms                  |
//+------------------------------------------------------------------+
class CPanel : public CForm
  {
private:
   color             m_fore_color;                                   // Цвет текста по умолчанию для всех объектов на панели
   ENUM_FW_TYPE      m_bold_type;                                    // Тип толщины шрифта
   ENUM_FRAME_STYLE  m_border_style;                                 // Стиль рамки панели
   bool              m_autoscroll;                                   // Флаг автоматического появления полосы прокрутки
   int               m_autoscroll_margin[2];                         // Массив полей вокруг элемента управления при автоматической прокрутке
   bool              m_autosize;                                     // Флаг автоматического изменения размера элемента под содержимое
   ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode;                 // Режим автоматического изменения размера элемента под содержимое
   ENUM_CANV_ELEMENT_DOCK_MODE m_dock_mode;                          // Режим привязки границ элемента к контейнеру
   int               m_margin[4];                                    // Массив промежутков всех сторон между полями данного и другого элемента управления
   int               m_padding[4];                                   // Массив промежутков всех сторон внутри элемента управления
//--- Возвращает флаги шрифта
   uint              GetFontFlags(void);
//--- Создаёт новый графический объект
   virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int element_num,
                                          const string name,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity);
//--- Возвращает начальные координаты привязанного объекта
   virtual void      GetCoords(int &x,int &y);

public:

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

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

//--- Устанавливает промежуток (1) слева, (2) сверху, (3) справа, (4) снизу, (5) со всех сторон внутри элемента управления
   void              PaddingLeft(const uint value)
                       {
                        this.m_padding[0]=((int)value<this.m_frame_width_left ? this.m_frame_width_left : (int)value);
                       }
   void              PaddingTop(const uint value)
                       {
                        this.m_padding[1]=((int)value<this.m_frame_width_top ? this.m_frame_width_top : (int)value);
                       }
   void              PaddingRight(const uint value)
                       {
                        this.m_padding[2]=((int)value<this.m_frame_width_right ? this.m_frame_width_right : (int)value);
                       }
   void              PaddingBottom(const uint value)
                       {
                        this.m_padding[3]=((int)value<this.m_frame_width_bottom ? this.m_frame_width_bottom : (int)value);
                       }
   void              PaddingAll(const uint value)
                       {
                        this.PaddingLeft(value); this.PaddingTop(value); this.PaddingRight(value); this.PaddingBottom(value);
                       }
//--- Возвращает промежуток (1) слева, (2) сверху, (3) справа, (4) снизу между полями внутри элемента управления

Теперь, если устанавливаемое значение Padding меньше ширины рамки панели, то значение будет равно ширине соответствующей стороны рамки.


В конструкторе по умолчанию добавим установку типа графического объекта как "панель" в свойства объекта:

                     CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0)
                       {
                        CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL);
                        CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_PANEL);
                        this.m_type=OBJECT_DE_TYPE_GWF_PANEL; 
                        this.m_fore_color=CLR_DEF_FORE_COLOR;
                        this.m_bold_type=FW_TYPE_NORMAL;
                        this.MarginAll(3);
                        this.PaddingAll(0);
                        this.Initialize();
                       }
//--- Деструктор

И точно такую же строку впишем во все остальные конструкторы класса. Без этой строки тип объекта, регистрируемый конструктором родительского класса, устанавливается как "Форма". Поэтому в этих конструкторах добавили принудительную установку этого свойства как "Панель".
Да, это недоработка логики этих классов. Посмотрим, что можно с этим сделать в дальнейшем...

Наименования ранее переименованных макроподстановок CLR_FORE_COLOR во всех конструкторах класса заменены на новые: CLR_DEF_FORE_COLOR.

Метод, создающий новый графический объект:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CPanel::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                       const int obj_num,
                                       const string obj_name,
                                       const int x,
                                       const int y,
                                       const int w,
                                       const int h,
                                       const color colour,
                                       const uchar opacity,
                                       const bool movable,
                                       const bool activity)
  {
   string name=this.CreateNameDependentObject(obj_name);
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
        break;
      case GRAPH_ELEMENT_TYPE_FORM :
         element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_PANEL :
         element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   return element;
  }
//+------------------------------------------------------------------+

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

Метод, возвращающий начальные координаты привязанного объекта:

//+------------------------------------------------------------------+
//| Возвращает начальные координаты привязанного объекта             |
//+------------------------------------------------------------------+
void CPanel::GetCoords(int &x,int &y)
  {
   x=this.CoordX()+this.FrameWidthLeft()+x;
   y=this.CoordY()+this.FrameWidthTop()+y;
  }
//+------------------------------------------------------------------+

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


В классе-коллекции графических элементов \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh добавим методы для быстрого доступа к графическим элементам управления:

//--- Возвращает список графических элементов по типу элемента
   CArrayObj        *GetListCanvElementByType(const ENUM_GRAPH_ELEMENT_TYPE type)
                       {
                        return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_TYPE,type,EQUAL);
                       }

//--- ...
                
//--- Возвращает графический элемент по идентификатору графика и имени
   CGCnvElement     *GetCanvElement(const long chart_id,const string name)
                       {
                        CArrayObj *list=this.GetListCanvElementByName(chart_id,name);
                        return(list!=NULL ? list.At(0) : NULL);
                       }
//--- Возвращает графический элемент по идентификатору графика и идентификатору объекта
   CGCnvElement     *GetCanvElement(const long chart_id,const int element_id)
                       {
                        CArrayObj *list=this.GetListCanvElementByID(chart_id,element_id);
                        return(list!=NULL ? list.At(0) : NULL);
                       }

//--- Конструктор
                     CGraphElementsCollection();

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

В методе, создающем объект-графический объект WinForms Panel на канвасе на указанном графике и подокне, впишем создание и отрисовку объекта-тени
в случае, если наличие тени указано в аргументах метода:

//--- Создаёт объект-графический объект WinForms Panel на канвасе на указанном графике и подокне
   int               CreatePanel(const long chart_id,
                                 const int subwindow,
                                 const string name,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h,
                                 const color clr,
                                 const uchar opacity,
                                 const bool movable,
                                 const bool activity,
                                 const int  frame_width=-1,
                                 ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                 const bool shadow=false,
                                 const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetColorBackground(clr);
                        obj.SetColorFrame(clr);
                        obj.SetOpacity(opacity,false);
                        //--- Установим флаг рисования тени
                        obj.SetShadow(shadow);
                        if(shadow)
                          {
                           //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный
                           //--- и затемним монохромный цвет на 20 единиц
                           color clrS=obj.ChangeColorLightness(obj.ChangeColorSaturation(obj.ColorBackground(),-100),-20);
                           //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
                           //--- Непрозрачность тени при этом установим равной значению по умолчанию, а радиус размытия равный 4
                           obj.DrawShadow(3,3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR);
                          }
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity());
                        obj.Erase(clr,opacity,redraw);
                        if(frame_width>0)
                           obj.FrameWidthAll(frame_width);
                        obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop());
                        obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style);
                        obj.Done();
                        return obj.ID();
                       }

И добавим методы, создающие объекты-панели с разными типами заливок (вертикальный и горизонтальный градиент, и циклический вертикальный и горизонтальный градиент). Здесь мы рассмотрим только один из этих методов, так как все остальные отличаются лишь комбинацией флагов, передаваемых в метод Erase() класса CGCnvElement:

//--- Создаёт объект-графический объект WinForms Panel на канвасе на указанном графике и подокне с заливкой вертикальным градиентом
   int               CreatePanelVGradient(const long chart_id,
                                          const int subwindow,
                                          const string name,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          color &clr[],
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity,
                                          const int  frame_width=-1,
                                          ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                          const bool shadow=false,
                                          const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetColorBackground(clr[0]);
                        obj.SetColorFrame(clr[0]);
                        obj.SetOpacity(opacity,false);
                        //--- Установим флаг рисования тени
                        obj.SetShadow(shadow);
                        if(shadow)
                          {
                           //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный
                           //--- и затемним монохромный цвет на 20 единиц
                           color clrS=obj.ChangeColorLightness(obj.ChangeColorSaturation(obj.ColorBackground(),-100),-20);
                           //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
                           //--- Непрозрачность тени при этом установим равной значению по умолчанию, а радиус размытия равный 4
                           obj.DrawShadow(3,3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR);
                          }
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity());
                        obj.Erase(clr,opacity,true,false,redraw);
                        if(frame_width>0)
                           obj.FrameWidthAll(frame_width);
                        obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop());
                        obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style);
                        obj.Done();
                        return obj.ID();
                       }
//--- ...

  };
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+

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

Методы Erase() родительского класса CGCnvElement, используемые для закрашивания прямоугольной области объекта, позволяют закрашивать либо одним цветом, либо различными способами заливки градиентом. Их мы рассматривали ранее при создании класса объекта-графического элемента.

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

Для того, чтобы мы могли удобно получать доступ к созданию и работе с элементами управления из своих программ, в главном классе библиотеки CEngine в файле \MQL5\Include\DoEasy\Engine.mqh в публичной секции напишем методы для быстрого доступа к этим объектам и добавим переменную для хранения префикса имён объектов в приватной секции класса:

   ENUM_PROGRAM_TYPE    m_program_type;                  // Тип программы
   string               m_name_program;                  // Имя программы
   string               m_name_prefix;                   // Префикс имени объектов
//--- Возвращает индекс счётчика по id
   int                  CounterIndex(const int id) const;

...

//--- Возвращает список графических элементов по идентификатору графика и имени объекта
   CArrayObj           *GetListCanvElementByName(const long chart_id,const string name)
                          {
                           return this.m_graph_objects.GetListCanvElementByName(chart_id,name);
                          }
//--- Возвращает список графических элементов по типу объекта
   CArrayObj           *GetListCanvElementByType(const ENUM_GRAPH_ELEMENT_TYPE type)
                          {
                           return this.m_graph_objects.GetListCanvElementByType(type);
                          }
   
//--- Возвращает графический элемент по идентификатору графика и имени объекта
   CGCnvElement        *GetCanvElementByName(const long chart_id,const string name)
                          {
                           return this.m_graph_objects.GetCanvElement(chart_id,name);
                          }
//--- Возвращает графический элемент по идентификатору графика и идентификатору объекта
   CGCnvElement        *GetCanvElementByID(const long chart_id,const int element_id)
                          {
                           return this.m_graph_objects.GetCanvElement(chart_id,element_id);
                          }
   
//--- Возвращает объект WForm Element по имени объекта на текущем графике
   CGCnvElement        *GetWFElement(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Element по идентификатору графика и имени объекта
   CGCnvElement        *GetWFElement(const long chart_id,const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Element по идентификатору объекта
   CGCnvElement        *GetWFElement(const int element_id)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
   
//--- Возвращает объект WForm Form по имени объекта на текущем графике
   CForm               *GetWFForm(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Form по идентификатору графика и имени объекта
   CForm               *GetWFForm(const long chart_id,const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Element по идентификатору объекта
   CForm               *GetWFForm(const int element_id)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
   
//--- Возвращает объект WForm Panel по имени объекта на текущем графике
   CPanel              *GetWFPanel(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Panel по идентификатору графика и имени объекта
   CPanel              *GetWFPanel(const long chart_id,const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Panel по идентификатору объекта
   CPanel              *GetWFPanel(const int element_id)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }

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

Напишем метод, создающий объект WinForm Element:

//--- Создаёт объект WinForm Element
   CGCnvElement        *CreateWFElement(const long chart_id,
                                        const int subwindow,
                                        const string name,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h,
                                        color &clr[],
                                        const uchar opacity,
                                        const bool v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           //--- Получаем идентификатор созданного объекта
                           int obj_id=
                             (
                              //--- Если вертикальный градиент:
                              v_gradient ?
                                (
                                 //--- если не циклический градиент - создаём объект с заливкой вертикальным градиентом
                                 !c_gradient ? this.m_graph_objects.CreateElementVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- иначе - создаём объект с заливкой циклическим вертикальным градиентом
                                 this.m_graph_objects.CreateElementVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              //--- Если не вертикальный градиент:
                              !v_gradient ?
                                (
                                 //--- если не циклический градиент - создаём объект с заливкой горизонтальным градиентом
                                 !c_gradient ? this.m_graph_objects.CreateElementHGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- иначе - создаём объект с заливкой циклическим горизонтальным градиентом
                                 this.m_graph_objects.CreateElementHGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           //--- возвращаем указатель на объект по его идентификатору
                           return this.GetWFElement(obj_id);
                          }

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

На основе этого метода напишем методы для создания объектов-графических элементов на текущем графике и указанном подокне, и на текущем графике в главном окне:

//--- Создаёт объект WinForm Element на текущем графике, в указанном подокне
   CGCnvElement        *CreateWFElement(const int subwindow,
                                        const string name,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h,
                                        color &clr[],
                                        const uchar opacity,
                                        const bool v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           return this.CreateWFElement(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,v_gradient,c_gradient,redraw);
                          }
//--- Создаёт объект WinForm Element на текущем графике, в главном окне
   CGCnvElement        *CreateWFElement(const string name,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h,
                                        color &clr[],
                                        const uchar opacity,
                                        const bool v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           return this.CreateWFElement(::ChartID(),0,name,x,y,w,h,clr,opacity,v_gradient,c_gradient,redraw);
                          }

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

Аналогично напишем методы для создания объектов WinForm Form:

//--- Создаёт объект WinForm Form
   CForm               *CreateWFForm(const long chart_id,
                                     const int subwindow,
                                     const string name,
                                     const int x,
                                     const int y,
                                     const int w,
                                     const int h,
                                     color &clr[],
                                     const uchar opacity,
                                     const bool movable,
                                     const bool v_gradient=true,
                                     const bool c_gradient=false,
                                     const bool shadow=false,
                                     const bool redraw=false)
                          {
                           int obj_id=
                             (
                              v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreateFormVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw) :
                                 this.m_graph_objects.CreateFormVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw)
                                ) :
                              !v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreateFormVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw) :
                                 this.m_graph_objects.CreateFormVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           return this.GetWFForm(obj_id);
                          }
//--- Создаёт объект WinForm Form на текущем графике, в указанном подокне
   CForm               *CreateWFForm(const int subwindow,
                                     const string name,
                                     const int x,
                                     const int y,
                                     const int w,
                                     const int h,
                                     color &clr[],
                                     const uchar opacity,
                                     const bool movable,
                                     const bool v_gradient=true,
                                     const bool c_gradient=false,
                                     const bool shadow=false,
                                     const bool redraw=false)
                          {
                           return this.CreateWFForm(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw);
                          }
//--- Создаёт объект WinForm Form на текущем графике, в главном окне
   CForm               *CreateWFForm(const string name,
                                     const int x,
                                     const int y,
                                     const int w,
                                     const int h,
                                     color &clr[],
                                     const uchar opacity,
                                     const bool movable,
                                     const bool v_gradient=true,
                                     const bool c_gradient=false,
                                     const bool shadow=false,
                                     const bool redraw=false)
                          {
                           return this.CreateWFForm(::ChartID(),0,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw);
                          }

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

Точно таким же образом напишем методы для создания объекта WinForm Panel:

//--- Создаёт объект WinForm Panel
   CForm               *CreateWFPanel(const long chart_id,
                                      const int subwindow,
                                      const string name,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      color &clr[],
                                      const uchar opacity,
                                      const bool movable,
                                      const bool v_gradient=true,
                                      const bool c_gradient=false,
                                      const int frame_width=-1,
                                      const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                      const bool shadow=false,
                                      const bool redraw=false)
                          {
                           int obj_id=
                             (
                              v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreatePanelVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) :
                                 this.m_graph_objects.CreatePanelVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw)
                                ) :
                              !v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreatePanelHGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) :
                                 this.m_graph_objects.CreatePanelHGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           return this.GetWFPanel(obj_id);
                          }
//--- Создаёт объект WinForm Panel на текущем графике, в указанном подокне
   CForm               *CreateWFPanel(const int subwindow,
                                      const string name,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      color &clr[],
                                      const uchar opacity,
                                      const bool movable,
                                      const bool v_gradient=true,
                                      const bool c_gradient=false,
                                      const int frame_width=-1,
                                      const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                      const bool shadow=false,
                                      const bool redraw=false)
                          {
                           return this.CreateWFPanel(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,frame_width,frame_style,shadow,redraw);
                          }
//--- Создаёт объект WinForm Panel на текущем графике, в главном окне
   CForm               *CreateWFPanel(const string name,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      color &clr[],
                                      const uchar opacity,
                                      const bool movable,
                                      const bool v_gradient=true,
                                      const bool c_gradient=false,
                                      const int frame_width=-1,
                                      const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                      const bool shadow=false,
                                      const bool redraw=false)
                          {
                           return this.CreateWFPanel(::ChartID(),0,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,frame_width,frame_style,shadow,redraw);
                          }

Оставим эти методы для самостоятельного разбора — они все идентичны вышерассмотренным.

В конструкторе класса создадим префикс имени графических объектов:

//+------------------------------------------------------------------+
//| CEngine конструктор                                              |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true),
                     m_last_trade_event(TRADE_EVENT_NO_EVENT),
                     m_last_account_event(WRONG_VALUE),
                     m_last_symbol_event(WRONG_VALUE),
                     m_global_error(ERR_SUCCESS)
  {
   this.m_is_hedge=#ifdef __MQL4__ true #else bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) #endif;
   this.m_is_tester=::MQLInfoInteger(MQL_TESTER);
   this.m_program_type=(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE);
   this.m_name_program=::MQLInfoString(MQL_PROGRAM_NAME);
   this.m_name_prefix=this.m_name_program+"_";
   
//--- ...

//--- ...

  }

Как вы могли заметить, переменная m_name была переименована в m_name_program — для более точного указания её назначения.


Тестирование

Для тестирования возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part103\ под новым именем TestDoEasyPart103.mq5.

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Установка глобальных переменных советника
   ArrayResize(array_clr,2);        // Массив цветов градиентной заливки
   array_clr[0]=C'26,100,128';      // Исходный ≈Тёмно-лазурный цвет
   array_clr[1]=C'35,133,169';      // Осветлённый исходный цвет
//--- Создадим массив с текущим символом и установим его для использования в библиотеке
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Создадим объект-таймсерию для текущего символа и периода и выведем его описание в журнал
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Краткие описания
//--- Создадим объекты-формы
   string name="";
   int obj_id=WRONG_VALUE;
   CArrayObj *list=NULL;
   CForm *form=NULL;
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      form=engine.CreateWFForm("Form_0"+string(i+1),30,(form==NULL ? 100 : form.BottomEdge()+20),100,30,array_clr,245,true);
      if(form==NULL)
         continue;
      //--- Установим ZOrder в ноль и выведем текст с описанием типа градиента и обновим форму
      //--- Параметры текста: координаты текста в центре формы и точка привязки - тоже по центру
      //--- Создаём новый кадр текстовой анимации с идентификатором 0 и выводим текст на форму
      form.SetZorder(0,false);
      form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,false);
     }
//--- Создадим четыре графических элемента
   CGCnvElement *elm=NULL;
   array_clr[0]=C'0x65,0xA4,0xA9';
   array_clr[1]=C'0x48,0x75,0xA2';
//--- Вертикальный градиент
   elm=engine.CreateWFElement("CElmVG",form.RightEdge()+20,20,200,50,array_clr,127,true);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Вертикальный циклический градиент
   elm=engine.CreateWFElement("CElmVGC",form.RightEdge()+20,80,200,50,array_clr,127,true,true);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Горизонтальный градиент
   elm=engine.CreateWFElement("CElmHG",form.RightEdge()+20,140,200,50,array_clr,127,false,false);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Горизонтальный циклический градиент
   elm=engine.CreateWFElement("CElmHGC",form.RightEdge()+20,200,200,50,array_clr,127,false,true);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Создадим объект WinForms Panel
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",elm.RightEdge()+20,50,230,150,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true);
   if(pnl!=NULL)
     {
      pnl.FontDrawStyle(FONT_STYLE_NORMAL);
      pnl.Bold(true);
      pnl.SetFontSize(10);
      pnl.TextOnBG(0,pnl.TypeElementDescription()+": ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',pnl.Opacity());
      //--- В цикле создадим пять привязанных объектов-форм, расположенных горизонтально сверху панели с отступом друг от друга на 4 пикселя
      CGCnvElement *obj=NULL;
      for(int i=0;i<5;i++)
        {
         //--- создадим объект-форму с рассчитанными координатами по оси X 
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_FORM,(obj==NULL ? 3 : obj.RightEdgeRelative()+4),3,40,30,C'0xCD,0xDA,0xD7',200,true);
         //--- Для контроля за созданием привязанных объектов
         //--- получим указатель на привязанный объект по индексу цикла
         obj=pnl.GetElement(i);
         //--- из полученного объекта возьмём указатель на его базовый объект
         CPanel *p=obj.GetBase();
         //--- и выведем в журнал наименование созданного привязанного объекта и имя его базового объекта
         Print
           (
            TextByLanguage("Объект ","Object "),obj.TypeElementDescription()," ",obj.Name(),
            TextByLanguage(" привязан к объекту "," is attached to object "),p.TypeElementDescription()," ",p.Name()
           );
        }
      pnl.Update(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Скомпилируем советник и запустим его на графике:


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

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

Object Form TestDoEasyPart103_WFPanel_Elm01 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm02 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm03 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm04 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm05 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel


Что дальше

В следующей статье продолжим работу над функционалом элемента управления "Панель".

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

К содержанию

*Статьи этой серии:

DoEasy. Элементы управления (Часть 1): Первые шаги
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel


Прикрепленные файлы |
MQL5.zip (4412.24 KB)
DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock
В статье реализуем работу таких параметров панели как Padding (внутренние отступы/поля со всех сторон элемента) и Dock (способ расположения объекта внутри контейнера).
Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений
Сегодня мы впервые обновляем функциональность системы индикаторов. В предыдущей статье "Несколько индикаторов на одном графике" мы рассмотрели основы кода, позволяющего использовать более одного индикатора в подокне, но то, что было представлено, было лишь начальной основой для гораздо более крупной системы.
Несколько индикаторов на графике (Часть 04): Начинаем работу с советником Несколько индикаторов на графике (Часть 04): Начинаем работу с советником
В предыдущих статьях я рассказывал, как создать индикатор с несколькими подокнами — такая возможность становится интересной при использовании пользовательских индикаторов. В этот раз мы рассмотрим, как добавить несколько окон в советник.
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel
В статье избавимся от некоторых ошибок при работе с графическими элементами и продолжим разработку элемента управления CPanel. Это будут методы для установки параметров шрифта, который используется по умолчанию для всех текстовых объектов панели, которые в свою очередь могут быть на ней расположены в дальнейшем.