Понравилась статья?
Поставьте ссылку на нее —
пусть другие почитают
Используй новые возможности MetaTrader 5

DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock

29 апреля 2022, 16:58
Artyom Trishkin
0
933

Содержание


Концепция

Продолжаем работу над функционалом элемента управления WinForms "Панель", и сегодня рассмотрим такие её свойства как Padding и Dock.

WinForm-объект "Панель" по сути является обычным контейнером для размещения внутри него других WinForm-объектов. При размещении таких объектов мы можем самостоятельно указать требуемые координаты размещения объекта, и он будет расположен на указанных координатах. Но мы можем также указать и способ привязки размещаемого в контейнере объекта после его создания внутри контейнера. И для этого существует шесть способов привязки объекта внутри своего контейнера (свойство Dock объекта):

  1. Присоединение к верхней границе и растягивание на ширину контейнера,
  2. Присоединение к нижней границе и растягивание на ширину контейнера,
  3. Присоединение к левой границе и растягивание на высоту контейнера,
  4. Присоединение к правой границе и растягивание на высоту контейнера,
  5. Растягивание на ширину и высоту всего контейнера (заполнение),
  6. Объект прикреплён к указанным координатам и его размеры не меняются.

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

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

На изображении показано как в MS Visual Studio притягиваются объекты к верхней грани своего контейнера, для которого установлено значение Padding равное 20, если для них выбрать притягивание к верхней границе контейнера:


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

Зачем так? Просто это будет удобнее для расположения объектов с учётом значения Padding контейнера — сам объект-подложка будет смещён на величину этого свойства, плюс, когда нам потребуется рисовать что-либо на канвасе, то рисовать это будем как раз на подложке — она уже будет смещена на значение Padding, и не потребуется рассчитывать координаты и размеры рисунка на этом объекте. И ещё другие дополнительные удобства, которые впоследствии сможем оценить при разработке других WinForm-объектов.


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

У нас есть возможность создания различных стилей объектов-форм, и для их определения существует отдельный файл в библиотеке, где мы постепенно прописываем дополнительные параметры для стилей. Так как объект-форма у нас может закрашиваться не только одним цветом, но и градиентной заливкой, то укажем в файле \MQL5\Include\DoEasy\GraphINI.mqh новые параметры стилей для градиентной заливки фона объекта-формы:

//+------------------------------------------------------------------+
//| Список индексов параметров стилей форм                           |
//+------------------------------------------------------------------+
enum ENUM_FORM_STYLE_PARAMS
  {
   //--- CForm
   FORM_STYLE_FRAME_SHADOW_OPACITY,             // Непрозрачность тени
   FORM_STYLE_FRAME_SHADOW_BLUR,                // Размытие тени
   FORM_STYLE_DARKENING_COLOR_FOR_SHADOW,       // Затемнённость цвета тени формы
   FORM_STYLE_FRAME_SHADOW_X_SHIFT,             // Смещение тени по оси X
   FORM_STYLE_FRAME_SHADOW_Y_SHIFT,             // Смещение тени по оси Y
   FORM_STYLE_GRADIENT_V,                       // Флаг вертикальной градиентной заливки
   FORM_STYLE_GRADIENT_C,                       // Флаг циклической градиентной заливки
   //--- CPanel
   FORM_STYLE_FRAME_WIDTH_LEFT,                 // Ширина рамки панели слева
   FORM_STYLE_FRAME_WIDTH_RIGHT,                // Ширина рамки панели справа
   FORM_STYLE_FRAME_WIDTH_TOP,                  // Ширина рамки панели сверху
   FORM_STYLE_FRAME_WIDTH_BOTTOM,               // Ширина рамки панели снизу
  };
#define TOTAL_FORM_STYLE_PARAMS        (11)     // Количество параметров стиля формы
//+------------------------------------------------------------------+
//| Массив, содержащий параметры стилей форм                         |
//+------------------------------------------------------------------+
int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]=
  {
//--- Параметры стиля формы "Плоская форма"
   {
      //--- CForm
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
      false,                                    // Флаг вертикальной градиентной заливки
      false,                                    // Флаг циклической градиентной заливки
      //--- CPanel
      3,                                        // Ширина рамки панели слева
      3,                                        // Ширина рамки панели справа
      3,                                        // Ширина рамки панели сверху
      3,                                        // Ширина рамки панели снизу
   },
//--- Параметры стиля формы "Рельефная форма"
   {
      //--- CForm
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
      true,                                     // Флаг вертикальной градиентной заливки
      false,                                    // Флаг циклической градиентной заливки
      //--- CPanel
      3,                                        // Ширина рамки панели слева
      3,                                        // Ширина рамки панели справа
      3,                                        // Ширина рамки панели сверху
      3,                                        // Ширина рамки панели снизу
   },
  };
//+------------------------------------------------------------------+

Количество параметров стилей увеличим с 9 до 11.


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

//+------------------------------------------------------------------+
//| Границы элемента управления, привязанные к контейнеру            |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_DOCK_MODE
  {
   CANV_ELEMENT_DOCK_MODE_NONE,                       // Прикреплён к указанным координатам, размеры не меняются
   CANV_ELEMENT_DOCK_MODE_TOP,                        // Присоединение сверху и растягивание на ширину контейнера
   CANV_ELEMENT_DOCK_MODE_BOTTOM,                     // Присоединение снизу и растягивание на ширину контейнера
   CANV_ELEMENT_DOCK_MODE_LEFT,                       // Присоединение слева и растягивание на высоту контейнера
   CANV_ELEMENT_DOCK_MODE_RIGHT,                      // Присоединение справа и растягивание на высоту контейнера
   CANV_ELEMENT_DOCK_MODE_FILL,                       // Растягивание на ширину и высоту всего контейнера
  };
//+------------------------------------------------------------------+

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

В файле E:\MetaQuotes\MetaTrader 5\MQL5\Include\DoEasy\Data.mqh впишем индексы новых сообщений:

   MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE,            // Не удалось изменить размер массива цветов
   MSG_LIB_SYS_FAILED_ARRAY_RESIZE,                   // Не удалось изменить размер массива
   MSG_LIB_SYS_FAILED_ARRAY_COPY,                     // Не удалось скопировать массив
   MSG_LIB_SYS_FAILED_ADD_BUFFER,                     // Не удалось добавить объект-буфер в список
   MSG_LIB_SYS_FAILED_CREATE_BUFFER_OBJ,              // Не удалось создать объект \"Индикаторный буфер\"

...

//--- CGCnvElement
   MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,                  // Ошибка! Пустой массив
   MSG_CANV_ELEMENT_ERR_ARRAYS_NOT_MATCH,             // Ошибка! Массив-копия ресурса не совпадает с оригиналом
   MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH,             // Ошибка! Не удалось установить ширину канваса
   MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT,            // Ошибка! Не удалось установить высоту канваса

...

//--- CGStdGraphObjExtToolkit
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA,     // Не удалось изменить размер массива данных времени опорной точки
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA,    // Не удалось изменить размер массива данных цены опорной точки
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM,   // Не удалось создать объект-форму для контроля опорной точки
   
//--- CPanel
   MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ,   // Не удалось создать объект-подложку
   
  };
//+------------------------------------------------------------------+

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

   {"Не удалось изменить размер массива цветов","Failed to resize color array"},
   {"Не удалось изменить размер массива ","Failed to resize array "},
   {"Не удалось скопировать массив","Failed to copy array"},
   {"Не удалось добавить объект-буфер в список","Failed to add buffer object to list"},
   {"Не удалось создать объект \"Индикаторный буфер\"","Failed to create object \"Indicator buffer\""},

...

//--- CGCnvElement
   {"Ошибка! Пустой массив","Error! Empty array"},
   {"Ошибка! Массив-копия ресурса не совпадает с оригиналом","Error! Array-copy of the resource does not match the original"},
   {"Ошибка! Не удалось установить ширину канваса","Error! Failed to set canvas width"},
   {"Ошибка! Не удалось установить высоту канваса","Error! Failed to set canvas height"},

...

//--- CGStdGraphObjExtToolkit
   {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"},
   {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"},
   {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"},
   
//--- CPanel
   {"Не удалось создать объект-подложку","Failed to create underlay object"},
   
  };
//+---------------------------------------------------------------------+


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

Кроме того, разные объекты могут быть присоединены друг к другу, например, в одном объекте-панели есть два присоединённых WinForm-объекта. И этот объект-панель присоединён к ещё одной панели. Для двух объектов, присоединённых к первой панели, она будет являться базовым объектом, и будет прописана в их свойствах как базовый объект, который для них является контейнером. А вот когда мы этот контейнер с двумя объектами размещаем внутри ещё одного контейнера, то для первого контейнера новый объект-панель станет базовым контейнером. А чтобы два объекта, размещённые в первом контейнере могли знать какой объект является основным в этой иерархии, нам нужно добавить ещё одно свойство — главный объект всей иерархии. Тогда два объекта, расположенные в первом контейнере будут знать свой базовый контейнер, а также и главный объект — второй контейнер, к которому прикреплён их базовый.

Доработаем класс объекта-графического элемента в файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.

В его защищённой секции объявим новые переменные:

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

//--- Создаёт (1) структуру объекта, (2) объект из структуры
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
   
private:


В приватной секции класса объявим метод для сохранения массива цветов градиентной заливки:

//--- Возвращает индекс массива, по которому фактически расположено (1) double-свойство и (2) string-свойство ордера
   int               IndexProp(ENUM_CANV_ELEMENT_PROP_DOUBLE property)  const { return(int)property-CANV_ELEMENT_PROP_INTEGER_TOTAL;                                 }
   int               IndexProp(ENUM_CANV_ELEMENT_PROP_STRING property)  const { return(int)property-CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_DOUBLE_TOTAL;  }

//--- Сохраняет цвета в массив цветов фона
   void              SaveColorsBG(color &colors[]);
   
public:


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

//--- Создаёт элемент
   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) возвращает изначальное смещение координаты (1) X, (2) Y относительно базового объекта
   void              SetCoordXRelativeInit(const int value)                            { this.m_init_relative_x=value;              }
   void              SetCoordYRelativeInit(const int value)                            { this.m_init_relative_y=value;              }
   int               CoordXRelativeInit(void)                                    const { return this.m_init_relative_x;             }
   int               CoordYRelativeInit(void)                                    const { return this.m_init_relative_y;             }
   
//--- (1) Устанавливает, (2) возвращает указатель на родительский элемент в составе связанных объектов текущей группы
   void              SetBase(CGCnvElement *element)                                    { this.m_element_base=element;               }
   CGCnvElement     *GetBase(void)                                                     { return this.m_element_base;                }
//--- (1) Устанавливает, (2) возвращает указатель на родительский элемент в составе всех групп связанных объектов
   void              SetMain(CGCnvElement *element)                                    { this.m_element_main=element;               }
   CGCnvElement     *GetMain(void)                                                     { return this.m_element_main;                }
   
//--- Возвращает указатель на объект-канвас
   CCanvas          *GetCanvasObj(void)                                                { return &this.m_canvas;                     }

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

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


Доработаем метод, устанавливающий цвет фона графического элемента:

   void              SetActiveAreaShift(const int left_shift,const int bottom_shift,const int right_shift,const int top_shift);
   void              SetColorBackground(const color colour)
                       {
                        this.m_color_bg=colour;
                        color arr[1];
                        arr[0]=colour;
                        this.SaveColorsBG(arr);
                       }

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

Чуть ниже напишем метод, устанавливающий цвета градиентной заливки фона графического элемента:

   void              SetOpacity(const uchar value,const bool redraw=false);
   void              SetColorsBackground(color &colors[])
                       {
                        this.SaveColorsBG(colors);
                        this.m_color_bg=this.m_array_colors_bg[0];
                       }

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

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

//--- Возвращает количество цветов, установленных для градиентной заливки фона
   uint              ColorsBackgroundTotal(void)         const { return this.m_array_colors_bg.Size();                                 }
//--- Возвращает (1) цвет фона, (2) непрозрачность, координату (3) правого, (4) нижнего края элемента
   color             ColorBackground(void)               const { return this.m_color_bg;                                               }
   color             ColorBackground(const uint index)   const
                       {
                        uint total=this.m_array_colors_bg.Size();
                        if(total==0)
                           return this.m_color_bg;
                        return(index>total-1 ? this.m_array_colors_bg[total-1] : this.m_array_colors_bg[index]);
                       }
   uchar             Opacity(void)                       const { return this.m_opacity;                                                }

Первый метод просто возвращает размер массива цветов объекта.

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

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

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
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_main=NULL;
   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(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.m_color_bg;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw))
     {

В защищённом конструкторе пропишем те же самые строки (здесь это повторно показывать не имеет смысла).


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

//+------------------------------------------------------------------+
//| Обновляет координаты элемента                                    |
//+------------------------------------------------------------------+
bool CGCnvElement::Move(const int x,const int y,const bool redraw=false)
  {
//--- Если элемент не перемещаемый или неактивный - уходим
   if(!this.Movable())
      return false;
//--- Если не удалось записать новые значения координат в свойства графического объекта - возвращаем false
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return false;
   //--- Если стоит флаг обновления - перерисовываем график.
   if(redraw)
      ::ChartRedraw(this.ChartID());
   //--- Возвращаем true
   return true;
  }
//+------------------------------------------------------------------+

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

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

Доработаем методы, устанавливающие новую ширину и высоту:

//+------------------------------------------------------------------+
//| Устанавливает новую ширину                                       |
//+------------------------------------------------------------------+
bool CGCnvElement::SetWidth(const int width)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width)
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
     {
      CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_WIDTH,width);
   return true;
  }
//+------------------------------------------------------------------+
//| Устанавливает новую высоту                                       |
//+------------------------------------------------------------------+
bool CGCnvElement::SetHeight(const int height)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_HEIGHT)==height)
      return true;
   if(!this.m_canvas.Resize(this.m_canvas.Width(),height))
     {
      CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,height);
   return true;
  }
//+------------------------------------------------------------------+

Ранее оба метода сразу же возвращали результат изменения размера высоты или ширины канваса:

return this.m_canvas.Resize(width,this.m_canvas.Height());

Но это не изменяло значения, записываемые в свойства объекта.

Поэтому теперь мы сначала проверяем значение, записанное в свойстве объекта на равенство переданному в метод значению и, если значения равны, то и изменять нечего — возвращаем сразу true.
Далее, если не удалось изменить размер канваса — сообщаем об этом в журнал и возвращаем false.
Если всё успешно — записываем новое значение в свойства объекта и возвращаем true.

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

//+------------------------------------------------------------------+
//| Очищает элемент с заполнением его цветом и непрозрачностью       |
//+------------------------------------------------------------------+
void CGCnvElement::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
   color arr[1];
   arr[0]=colour;
   this.SaveColorsBG(arr);
   this.m_canvas.Erase(::ColorToARGB(colour,opacity));
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Очищает элемент заливкой градиентом                              |
//+------------------------------------------------------------------+
void CGCnvElement::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Устанавливаем флаги вертикальной и циклической градиентной заливки
   this.m_gradient_v=vgradient;
   this.m_gradient_c=cycle;
//--- Проверяем размер массива цветов
   int size=::ArraySize(colors);
//--- ...

//--- ... 

//--- Сохраняем массив цветов фона
   this.SaveColorsBG(colors);
//--- Если указано - обновляем канвас
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

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

Метод, сохраняющий цвета в массив цветов фона:

//+------------------------------------------------------------------+
//| Сохраняет цвета в массив цветов фона                             |
//+------------------------------------------------------------------+
void CGCnvElement::SaveColorsBG(color &colors[])
  {
   if(this.m_array_colors_bg.Size()!=colors.Size())
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_colors_bg,colors.Size())!=colors.Size())
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE);
         CMessage::ToLog(::GetLastError(),true);
         return;
        }
     }
   ::ArrayCopy(this.m_array_colors_bg,colors);
  }
//+------------------------------------------------------------------+

Здесь: если размер переданного в метод массива цветов не совпадает с размером массива цветов объекта — изменяем размер массива цветов градиентной заливки объекта, а затем копируем переданный в метод массив в массив цветов объекта.

Так как у нас теперь есть методы, сохраняющие и возвращающие смещение в пикселях координат объекта относительно другого объекта, то в файле класса объекта-тени \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
   
//--- Размытие по-Гауссу
   bool              GaussianBlur(const uint radius);

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

public:
//--- Конструктор
                     CShadowObj(const long chart_id,
                                const int subwindow,
                                const string name,
                                const int x,
                                const int y,
                                const int w,
                                const int h);

//--- Поддерживаемые свойства объекта (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;        }

//--- Рисует тень объекта
   void              DrawShadow(const int shift_x,const int shift_y,const uchar blur_value);

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+

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

   this.m_offset_x=shift_x;
   this.m_offset_y=shift_y;

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

   CGCnvElement::Move(this.CoordX()+this.m_offset_x,this.CoordY()+this.m_offset_y);

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

//+------------------------------------------------------------------+
//| Рисует тень объекта                                              |
//+------------------------------------------------------------------+
void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value)
  {
//--- Устанавливаем в переменные значения смещения тени по осям X и Y
   this.SetCoordXRelative(shift_x);
   this.SetCoordYRelative(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.CoordXRelative(),this.CoordY()+this.CoordYRelative());
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

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


Доработаем класс объекта-формы в файле \MQL5\Include\DoEasy\Objects\Graph\Form.mqh.

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

protected:
   CArrayObj         m_list_tmp;                               // Список для размещения указателей
   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;   }
//--- Обновляет координаты привязанных объектов
   virtual bool      MoveDependentObj(const int x,const int y,const bool redraw=false);
   
public:


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

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


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

//+------------------------------------------------------------------+
//| Инициализирует переменные                                        |
//+------------------------------------------------------------------+
void CForm::Initialize(void)
  {
   this.m_list_elements.Clear();
   this.m_list_elements.Sort();
   this.m_list_tmp.Clear();
   this.m_list_tmp.Sort();
   this.m_shadow_obj=NULL;
   this.m_shadow=false;
   this.m_frame_width_right=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_left=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_top=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_bottom=DEF_FRAME_WIDTH_SIZE;
   this.m_gradient_v=true;
   this.m_gradient_c=false;
   this.m_mouse_state_flags=0;
   this.m_offset_x=0;
   this.m_offset_y=0;
   CGCnvElement::SetInteraction(false);
   this.m_animations=new CAnimations(CGCnvElement::GetObject());
   this.m_list_tmp.Add(m_animations);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
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;
   //--- ...


   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   element.SetMovable(movable);
   return element;
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//+------------------------------------------------------------------+
bool CForm::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                             CGCnvElement *main,
                             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.SetColorBackground(colour);
   obj.SetOpacity(opacity);
   obj.SetActive(activity);
   obj.SetMain(main);
   obj.SetBase(this.GetObject());
   obj.SetID(this.ID());
   obj.SetCoordXRelative(x);
   obj.SetCoordYRelative(y);
   obj.SetZorder(this.Zorder(),false);
   obj.SetCoordXRelativeInit(x);
   obj.SetCoordYRelativeInit(y);
//--- Рисуем добавленный объект и возвращаем true
   obj.Erase(colour,opacity,true);
   return true;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает стиль формы                                        |
//+------------------------------------------------------------------+
void CForm::SetFormStyle(const ENUM_FORM_STYLE style,
                         const ENUM_COLOR_THEMES theme,
                         const uchar opacity,
                         const bool shadow=false,
                         const bool use_bg_color=true,
                         const bool redraw=false)
  {
//--- Устанавливаем параметры непрозрачности и размера сторон рамки формы
   this.m_shadow=shadow;
   this.m_frame_width_top=array_form_style[style][FORM_STYLE_FRAME_WIDTH_TOP];
   this.m_frame_width_bottom=array_form_style[style][FORM_STYLE_FRAME_WIDTH_BOTTOM];
   this.m_frame_width_left=array_form_style[style][FORM_STYLE_FRAME_WIDTH_LEFT];
   this.m_frame_width_right=array_form_style[style][FORM_STYLE_FRAME_WIDTH_RIGHT];
   this.m_gradient_v=array_form_style[style][FORM_STYLE_GRADIENT_V];
   this.m_gradient_c=array_form_style[style][FORM_STYLE_GRADIENT_C];
//--- Создаём объект тени
   this.CreateShadowObj(clrNONE,(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_OPACITY]);
   
//--- Устанавливаем цветовую схему
   this.SetColorTheme(theme,opacity);
//--- Рассчитываем цвет тени с затемнением цвета
   color clr=array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW];
   color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100);
   color color_shadow=CGCnvElement::ChangeColorLightness((use_bg_color ? gray : clr),-fabs(array_form_style[style][FORM_STYLE_DARKENING_COLOR_FOR_SHADOW]));
   this.SetColorShadow(color_shadow);
   
//--- Рисуем прямоугольную тень
   int shift_x=array_form_style[style][FORM_STYLE_FRAME_SHADOW_X_SHIFT];
   int shift_y=array_form_style[style][FORM_STYLE_FRAME_SHADOW_Y_SHIFT];
   this.DrawShadow(shift_x,shift_y,color_shadow,this.OpacityShadow(),(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_BLUR]);
   
//--- Заполняем фон формы цветом и непрозрачностью
   this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
//--- В зависимости от выбранного стиля формы рисуем соответствующую рамку формы и внешнюю очерчивающую рамку
   switch(style)
     {
      case FORM_STYLE_BEVEL   :
        this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_BEVEL);
        break;
      //---FORM_STYLE_FLAT
      default:
        this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_FLAT);
        break;
     }
   this.DrawRectangle(0,0,Width()-1,Height()-1,array_color_themes[theme][COLOR_THEME_COLOR_FORM_RECT_OUTER],this.Opacity());
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Обновляет координаты элемента                                    |
//+------------------------------------------------------------------+
bool CForm::Move(const int x,const int y,const bool redraw=false)
  {
   CGCnvElement *base=this.GetBase();
   CGCnvElement *main=this.GetMain();
   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.CoordXRelative(),y-OUTER_AREA_SIZE+this.m_shadow_obj.CoordYRelative(),false))
         return false;
     }
//--- Если не удалось записать новые значения координат в свойства графического объекта - возвращаем false
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return false;
//--- Смещаем все привязанные объекты
   if(!this.MoveDependentObj(x,y,false))
      return false;
   //--- Если стоит флаг обновления, и это базовый объект - перерисовываем график.
   if(redraw && main==NULL)
      ::ChartRedraw(this.ChartID());
   //--- Возвращаем true
   return true;
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Обновляет координаты привязанных объектов                        |
//+------------------------------------------------------------------+
bool CForm::MoveDependentObj(const int x,const int y,const bool redraw=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+obj.CoordXRelative(),y+obj.CoordYRelative(),false))
         return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

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

Теперь доработаем класс WinForms-объекта "Панель" в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh.

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

//+------------------------------------------------------------------+
//| Класс объекта Panel элементов управления WForms                  |
//+------------------------------------------------------------------+
class CPanel : public CForm
  {
private:
   CGCnvElement     *m_underlay;                                     // Подложка для размещения элементов
   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];                                   // Массив промежутков всех сторон внутри элемента управления
   int               m_init_x;                                       // Координата X панели при её создании
   int               m_init_y;                                       // Координата Y панели при её создании
   int               m_init_w;                                       // Ширина панели при её создании
   int               m_init_h;                                       // Высота панели при её создании
//--- Возвращает флаги шрифта
   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);
//--- Создаёт объект-подложку
   bool              CreateUnderlayObj(void);
//--- Привязывает элемент к контейнеру
   bool              SetDockingToContainer(void);
protected:


В защищённой секции класса напишем методы для работы с координатами объекта-подложки:

protected:
//--- Устанавливает координату (1) X, (2) Y, (3) ширину, (4) высоту, (5) все параметры подложки
   bool              SetCoordXUnderlay(const int value)              { return(this.m_underlay!=NULL ? this.m_underlay.SetCoordX(value) : false);   }
   bool              SetCoordYUnderlay(const int value)              { return(this.m_underlay!=NULL ? this.m_underlay.SetCoordY(value) : false);   }
   bool              SetWidthUnderlay(const int value)               { return(this.m_underlay!=NULL ? this.m_underlay.SetWidth(value)  : false);   }
   bool              SetHeightUnderlay(const int value)              { return(this.m_underlay!=NULL ? this.m_underlay.SetHeight(value) : false);   }
   bool              SetUnderlayParams(void);
//--- Возвращает координату (1) X, (2) Y, (3) ширину, (4) высоту подложки
   int               GetCoordXUnderlay(void)                   const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordX() : 0);              }
   int               GetCoordYUnderlay(void)                   const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordY() : 0);              }
   int               GetWidthUnderlay(void)                    const { return(this.m_underlay!=NULL ?  this.m_underlay.Width()  : 0);              }
   int               GetHeightUnderlay(void)                   const { return(this.m_underlay!=NULL ?  this.m_underlay.Height() : 0);              }
//--- Возвращает координату (1) X, (2) Y подложки относительно панели
   int               GetCoordXUnderlayRelative(void)           const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordXRelative() : 0);      }
   int               GetCoordYUnderlayRelative(void)           const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordYRelative() : 0);      }

public:


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

public:
//--- Возвращает подложку
   CGCnvElement     *GetUnderlay(void)                               { return this.m_underlay;              }
//--- Обновляет координаты (сдвигает канвас)
   virtual bool      Move(const int x,const int y,const bool redraw=false);

//--- (1) Устанавливает, (2) возвращает цвет текста по умолчанию всех объектов на панели
   void              ForeColor(const color clr)                      { this.m_fore_color=clr;               }
   color             ForeColor(void)                           const { return this.m_fore_color;            }


Доработаем некоторые публичные методы.

Метод, устанавливающий режим привязки границ элемента к контейнеру:

//--- (1) Устанавливает, (2) возвращает режим привязки границ элемента к контейнеру
   void              DockMode(const ENUM_CANV_ELEMENT_DOCK_MODE mode)
                       {
                        if(m_dock_mode==mode)
                           return;
                        this.m_dock_mode=mode;
                        this.SetDockingToContainer();
                       }

Ранее метод просто устанавливал переданное в него значение в соответствующую переменную класса.

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

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

//--- Устанавливает промежуток (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);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Устанавливаем значение смещения подложки по оси X
                           this.m_underlay.SetCoordXRelative(this.PaddingLeft());
                           //--- Устанавливаем координату X и ширину подложки
                           this.SetCoordXUnderlay(this.CoordX()+this.PaddingLeft());
                           this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight());
                          }
                       }
   void              PaddingTop(const uint value)
                       {
                        this.m_padding[1]=((int)value<this.m_frame_width_top ? this.m_frame_width_top : (int)value);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Устанавливаем значение смещения подложки по оси Y
                           this.m_underlay.SetCoordYRelative(this.PaddingTop());
                           //--- Устанавливаем координату Y и высоту подложки
                           this.SetCoordYUnderlay(this.CoordY()+this.PaddingTop());
                           this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom());
                          }
                       }
   void              PaddingRight(const uint value)
                       {
                        this.m_padding[2]=((int)value<this.m_frame_width_right ? this.m_frame_width_right : (int)value);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Устанавливаем значение ширины подложки
                           this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight());
                          }
                       }
   void              PaddingBottom(const uint value)
                       {
                        this.m_padding[3]=((int)value<this.m_frame_width_bottom ? this.m_frame_width_bottom : (int)value);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Устанавливаем значение высоты подложки
                           this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom());
                          }
                       }
   void              PaddingAll(const uint value)
                       {
                        this.PaddingLeft(value); this.PaddingTop(value); this.PaddingRight(value); this.PaddingBottom(value);
                       }

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

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

//--- Устанавливает ширину рамки формы (1) слева, (2) сверху, (3) справа, (4) снизу, (5) всех сторон элемента управления
   void              FrameWidthLeft(const uint value)
                       {
                        this.m_frame_width_left=(int)value;
                        if(PaddingLeft()<FrameWidthLeft())
                           PaddingLeft(FrameWidthLeft());
                       }
   void              FrameWidthTop(const uint value)
                       {
                        this.m_frame_width_top=(int)value;
                        if(this.PaddingTop()<this.FrameWidthTop())
                           this.PaddingTop(this.FrameWidthTop());
                       }
   void              FrameWidthRight(const uint value)
                       {
                        this.m_frame_width_right=(int)value;
                        if(this.PaddingRight()<this.FrameWidthRight())
                           this.PaddingRight(this.FrameWidthRight());
                       }
   void              FrameWidthBottom(const uint value)
                       {
                        this.m_frame_width_bottom=(int)value;
                        if(this.PaddingBottom()<this.FrameWidthBottom())
                           this.PaddingBottom(this.FrameWidthBottom());
                       }
   void              FrameWidthAll(const uint value)
                       {
                        this.FrameWidthLeft(value); this.FrameWidthTop(value); this.FrameWidthRight(value); this.FrameWidthBottom(value);
                       }

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

Удалим из листинга два лишних конструктора — их объявление и реализацию за пределами тела класса:

//--- Конструкторы
                     CPanel(const long chart_id,
                           const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CPanel(const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CPanel(const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0)

Эти конструкторы нам не понадобились.

А вот в параметрическом конструкторе добавим инициализацию свойств объекта-панели, которую ранее не делали, и новых переменных:

                     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.DockMode(CANV_ELEMENT_DOCK_MODE_NONE);
                        this.BorderStyle(FRAME_STYLE_BEVEL);
                        this.AutoScroll(false);
                        this.AutoScrollMarginAll(0);
                        this.AutoSize(false);
                        this.AutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW);
                        this.Initialize();
                        this.CreateUnderlayObj();
                        this.m_init_x=0;
                        this.m_init_y=0;
                        this.m_init_w=0;
                        this.m_init_h=0;
                       }
//--- Деструктор
                    ~CPanel();
  };
//+------------------------------------------------------------------+

Точно так же инициализируем эти же переменные и в конструкторе с указанием идентификатора чарта и подокна.

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

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


Метод, создающий объект-подложку:

//+------------------------------------------------------------------+
//| Создаёт объект-подложку                                          |
//+------------------------------------------------------------------+
bool CPanel::CreateUnderlayObj(void)
  {
   this.m_underlay=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,this.ID(),this.Number(),this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Undl"),
                                     this.CoordX()+this.PaddingLeft(),this.CoordY()+this.PaddingTop(),
                                     this.Width()-this.PaddingLeft()-this.PaddingRight(),
                                     this.Height()-this.PaddingTop()-this.PaddingBottom(),
                                     CLR_CANV_NULL,0,false,false);
   if(m_underlay==NULL)
     {
      CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ);
      return false;
     }
   if(!this.m_list_tmp.Add(this.m_underlay))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
      delete this.m_underlay;
      return false;
     }
   this.SetUnderlayParams();
   return true;
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Устанавливает все параметры подложки                             |
//+------------------------------------------------------------------+
bool CPanel::SetUnderlayParams(void)
  {
//--- Устанавливаем в переменные значения смещения подложки по осям X и Y
   this.m_underlay.SetCoordXRelative(this.PaddingLeft());
   this.m_underlay.SetCoordYRelative(this.PaddingTop());
//--- Устанавливаем координаты и размеры подложки
   bool res=true;
   res &=this.SetCoordXUnderlay(this.CoordX()+this.PaddingLeft());
   res &=this.SetCoordYUnderlay(this.CoordY()+this.PaddingTop());
   res &=this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight());
   res &=this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom());
   return res;
  }
//+------------------------------------------------------------------+

Здесь всё просто: записываем смещения координат подложки относительно координат панели, и координаты и размеры объекта-подложки.


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

//+------------------------------------------------------------------+
//| Обновляет координаты элемента                                    |
//+------------------------------------------------------------------+
bool CPanel::Move(const int x,const int y,const bool redraw=false)
  {
   if(!this.m_underlay.Move(x+this.GetCoordXUnderlayRelative(),y+this.GetCoordYUnderlayRelative()))
      return false;
//--- Получаем указатели на базовый и главный объекты в иерархии связанных объектов и объект-тень
   CGCnvElement *base=this.GetBase();
   CGCnvElement *main=this.GetMain();
   CShadowObj   *shadow=this.GetShadowObj();
//--- Если элемент не перемещаемый, и это базовый объект - уходим
   if(!this.Movable() && main==NULL)
      return false;
//--- Если есть тень у объекта и не удалось записать новые значения координат в свойства объекта-тени - возвращаем false
   if(this.m_shadow)
     {
      if(shadow==NULL || !shadow.Move(x-OUTER_AREA_SIZE+shadow.CoordXRelative(),y-OUTER_AREA_SIZE+shadow.CoordYRelative(),false))
         return false;
     }
//--- Если не удалось записать новые значения координат в свойства графического объекта - возвращаем false
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return false;
//--- Смещаем все привязанные объекты
   if(!this.MoveDependentObj(x+this.GetCoordXUnderlayRelative(),y+this.GetCoordYUnderlayRelative(),false))
      return false;
   //--- Если стоит флаг обновления, и это главный объект иерархии - перерисовываем график.
   if(redraw && main==NULL)
      ::ChartRedraw(this.ChartID());
   //--- Возвращаем true
   return true;
  }
//+------------------------------------------------------------------+

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


Метод, привязывающий элемент к контейнеру:

//+------------------------------------------------------------------+
//| Привязывает элемент к контейнеру                                 |
//+------------------------------------------------------------------+
bool CPanel::SetDockingToContainer(void)
  {
//--- Получаем указатель на объект-панель, к которуму привязан этот объект
   CPanel *base=this.GetBase();
   if(base==NULL)
      return false;
//--- Объявляем переменные и получаем в них координаты и размеры базового объекта
   int x=base.GetCoordXUnderlay();
   int y=base.GetCoordYUnderlay();
   int w=base.GetWidthUnderlay();
   int h=base.GetHeightUnderlay();
//--- В зависимости от установленного режима привязки к контейнеру перемещаем объект к нужным граням базового объекта
   switch(this.DockMode())
     {
      //--- Присоединение сверху и растягивание на ширину контейнера
      case CANV_ELEMENT_DOCK_MODE_TOP :
        this.SetWidth(w);
        this.SetHeight(this.m_init_h);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(0);
        break;
      
      //--- Присоединение снизу и растягивание на ширину контейнера
      case CANV_ELEMENT_DOCK_MODE_BOTTOM :
        this.SetWidth(w);
        this.SetHeight(this.m_init_h);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay()+(base.GetHeightUnderlay()-this.Height());
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(base.GetHeightUnderlay()-this.Height());
        break;
      
      //--- Присоединение слева и растягивание на высоту контейнера
      case CANV_ELEMENT_DOCK_MODE_LEFT :
        this.SetHeight(h);
        this.SetWidth(this.m_init_w);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(0);
        break;
      
      //--- Присоединение справа и растягивание на высоту контейнера
      case CANV_ELEMENT_DOCK_MODE_RIGHT :
        this.SetHeight(h);
        this.SetWidth(this.m_init_w);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay()+(base.GetWidthUnderlay()-this.Width());
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(base.GetWidthUnderlay()-this.Width());
        this.SetCoordYRelative(0);
        break;
      
      //--- Растягивание на ширину и высоту всего контейнера
      case CANV_ELEMENT_DOCK_MODE_FILL :
        this.SetWidth(w);
        this.SetHeight(h);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(0);
        break;
      
      //--- Прикреплён к указанным координатам, размеры не меняются
      default: // CANV_ELEMENT_DOCK_MODE_NONE
        this.SetHeight(this.m_init_h);
        this.SetWidth(this.m_init_w);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay()+this.CoordXRelativeInit();
        y=base.GetCoordYUnderlay()+this.CoordYRelativeInit();
        this.Move(x,y);
        this.SetCoordXRelative(this.CoordXRelativeInit());
        this.SetCoordYRelative(this.CoordYRelativeInit());
        break;
     }
   ::ChartRedraw(this.ChartID());
   return true;
  }
//+------------------------------------------------------------------+

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

Метод всегда вызывается при установке нового значения для свойства DockMode объекта.

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

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

obj.SetColorBackground(clr[0]);

на метод SetColorsBackground():

//--- Создаёт объект-графический объект-форму на канвасе на указанном графике и подокне с заливкой вертикальным градиентом
   int               CreateFormVGradient(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 bool shadow=false,
                                         const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CForm *obj=new CForm(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.SetColorsBackground(clr);
                        obj.SetColorFrame(clr[0]);
                        obj.SetOpacity(opacity,false);
                        obj.SetShadow(shadow);
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity());
                        obj.Done();
                        obj.Erase(clr,opacity,true,false,redraw);
                        return obj.ID();
                       }

Теперь в метод передаётся не первый цвет из массива цветов, а сам массив.

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

Похожие доработки сделаем и в методах создания объектов-панелей:

//--- Создаёт объект-графический объект 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.SetColorsBackground(clr);
                        obj.SetColorFrame(clr[0]);
                        obj.BorderStyle(frame_style);
                        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(),obj.BorderStyle());
                        obj.Done();
                        return obj.ID();
                       }

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

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

Чтобы мы могли проконтролировать правильность назначения объектам, привязанным к панели, их идентификаторов и свойств ZOrder, временно пропишем код для вывода текстов на привязанные к панели объекты.

В методе, устанавливающим ZOrder в указанный элемент, а в остальных элементах корректирующем, впишем такие блоки кода:

//+------------------------------------------------------------------+
//| Устанавливает ZOrder в указанный элемент,                        |
//| а в остальных элементах корректирует                             |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj)
  {
//--- Получаем максимальный ZOrder всех графических элементов
   long max=this.GetZOrderMax();
//--- Если передан невалидный указатель на объект, или не получен максимальный ZOrder - возвращаем false
   if(obj==NULL || max<0)
      return false;
//--- Объявляем переменную для хранения результата работы метода
   bool res=true;
//--- Если максимальный ZOrder нулевой, то ZOrder будет равен 1,
//--- если максимальный ZOrder меньше значения (общее количества графических элементов)-1, то ZOrder будет на 1 больше,
//--- иначе - ZOrder будет равен значению (общее количества графических элементов)-1
   long value=(max==0 ? 1 : max<this.m_list_all_canv_elm_obj.Total()-1 ? max+1 : this.m_list_all_canv_elm_obj.Total()-1);
//--- Если не удалось выставить ZOrder объекту, переданному в метод - возвращаем false
   if(!obj.SetZorder(value,false))
      return false;
//--- Временно объявим объект-форму - для рисования на нём текста для визуального отображения его ZOrder
   CForm *form=obj;
//--- и нарисуем на форме текст с указанием ZOrder
   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,true);
   
   //--- Временно - для теста, если этот элемент - форма и выше
   if(form.Type()>=OBJECT_DE_TYPE_GFORM)
     {
      for(int j=0;j<form.ElementsTotal();j++)
        {
         CForm *pnl=form.GetElement(j);
         if(pnl==NULL || pnl.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_PANEL)
            continue;
         pnl.TextOnBG(0,"ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',pnl.Opacity());
        }
     }
   
//--- Отсортируем список графических элементов по идентификатору элемента
   this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
//--- Получим список графических элементов без объекта, идентификатор которого равен идентификатору объекта, переданного в метод
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_ID,obj.ID(),NO_EQUAL);
//--- Если отфильтрованный список получить не удалось и при этом размер списка больше одного,
//--- что означает наличие в нём других объектов помимо отфильтрованного по ID - возвращаем false
   if(list==NULL && this.m_list_all_canv_elm_obj.Total()>1)
      return false;
//--- В цикле по полученному списку оставшихся объектов-графических элементов
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект
      CGCnvElement *elm=list.At(i);
      //--- Если объект получить не удалось, или это контрольная форма управления опорными точками расширенного стандартного графического объекта
      //--- или, если ZOrder объекта нулевой - пропускаем этот объект, так как изменять его ZOrder не нужно - он самий нижний
      if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==0)
         continue;
      //--- Если не удалось установить ZOrder объекту на 1 меньше, чем он есть (ZOrder уменьшаем на 1) - добавляем к значению res значение false
      if(!elm.SetZorder(elm.Zorder()-1,false))
         res &=false;
      //--- Временно - для теста, если этот элемент - форма и выше
      if(elm.Type()>=OBJECT_DE_TYPE_GFORM)
        {
         //--- присвоим форме указатель на элемент и нарисуем на форме текст с указанием ZOrder 
         form=elm;
         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,true);
         
         for(int j=0;j<form.ElementsTotal();j++)
           {
            CForm *pnl=form.GetElement(j);
            if(pnl==NULL || pnl.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_PANEL)
               continue;
            pnl.TextOnBG(0,"ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',pnl.Opacity());
           }
        }
     }
//--- По окончании цикла возвращаем результат, записанный в res
   return res;
  }
//+------------------------------------------------------------------+

Этот код позволяет найти привязанный объект к панели и вывести на него надпись с идентификатором найденного объекта и значением его ZOrder.

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

Теперь у нас всё готово для тестирования.


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

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

Как будем тестировать. В прошлом советнике мы уже создавали одну панель, а в ней ещё несколько. Теперь же мы создадим внутри панели один объект-панель и назначим клавиши для его привязки к граням главной панели. Нажимая клавиши на клавиатуре мы будем устанавливать все возможные типы привязки зависимой панели к сторонам главной. Главной панели назначим значение Padding, равное 10 — чтобы был виден отступ от краёв панели и было понятно, как работает Padding при позиционировании одного объекта внутри другого.

Клавиши назначим такие:

  • W — для привязки к верхней грани,
  • A — для привязки к левой грани,
  • D — для привязки к правой грани,
  • X — для привязки к нижней грани,
  • S — для заполнения,
  • Z — для сброса привязки и возврата к исходным размерам и координатам.

В глобальной области назначим коды клавиш:

//+------------------------------------------------------------------+
//|                                            TestDoEasyPart104.mq5 |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- defines
#define  FORMS_TOTAL (3)   // Количество создаваемых форм
#define  START_X     (4)   // Начальная координата X фигуры
#define  START_Y     (4)   // Начальная координата Y фигуры
#define  KEY_LEFT    (65)  // (A) Влево
#define  KEY_RIGHT   (68)  // (D) Вправо
#define  KEY_UP      (87)  // (W) Вверх
#define  KEY_DOWN    (88)  // (X) Вниз
#define  KEY_CENTER  (83)  // (S) Центр
#define  KEY_ORIGIN  (90)  // (Z) По умолчанию
//--- input parameters
sinput   bool              InpMovable     =  true;          // Movable forms flag
sinput   ENUM_INPUT_YES_NO InpUseColorBG  =  INPUT_YES;     // Use chart background color to calculate shadow color
sinput   color             InpColorForm3  =  clrCadetBlue;  // Third form shadow color (if not background color) 
//--- global variables
CEngine        engine;
color          array_clr[];
//+------------------------------------------------------------------+

В OnInit() советника создадим все объекты (было ранее) и создадим внутри панели ещё одну панель:

//+------------------------------------------------------------------+
//| 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)
     {
      //--- Установим значение Padding равным 10
      pnl.PaddingAll(10);
      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());
      //--- В цикле создадим N привязанных объектов-панелей (одну панель)
      CPanel *obj=NULL;
      for(int i=0;i<1;i++)
        {
         //--- создадим объект-панель с координатами по оси X по центру, и 10 по оси Y, шириной 80 и высотой 50
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_PANEL,pnl,pnl.GetUnderlay().Width()/2-40,10,80,50,C'0xCD,0xDA,0xD7',200,true);
         //--- Для контроля за созданием привязанных объектов
         //--- получим указатель на привязанный объект по индексу цикла
         obj=pnl.GetElement(i);
         //--- из полученного объекта возьмём указатель на его базовый объект

         //--- и выведем в журнал наименование созданного привязанного объекта и имя его базового объекта
         Print
           (
            TextByLanguage("Объект ","Object "),obj.TypeElementDescription()," ",obj.Name(),
            TextByLanguage(" привязан к объекту "," is attached to object "),obj.GetBase().TypeElementDescription()," ",obj.GetBase().Name()
           );
         if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_PANEL)
           { 
            //--- Выведем на созданную панель идентификатор и zorder
            obj.TextOnBG(0,"ID "+(string)obj.ID()+", ZD "+(string)obj.Zorder(),obj.Width()/2,obj.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',obj.Opacity());
            //--- Установим цвет рамки, активную зону панели и нарисуем рамку
            obj.SetColorFrame(obj.ColorBackground());
            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_BEVEL);
            obj.Update();
           }
        }
      pnl.Update(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


В обработчике OnChartEvent() впишем такой код обработки нажатия клавиш:

   //--- Если нажата клавиша на клавиатуре
   if(id==CHARTEVENT_KEYDOWN)
     {
      CPanel *panel=engine.GetWFPanel(7).GetElement(0);
      if(panel!=NULL)
        {
         if(lparam==KEY_UP)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_TOP);
         else if(lparam==KEY_DOWN)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_BOTTOM);
         else if(lparam==KEY_LEFT)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_LEFT);
         else if(lparam==KEY_RIGHT)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_RIGHT);
         else if(lparam==KEY_CENTER)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_FILL);
         else if(lparam==KEY_ORIGIN)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_NONE);
        }

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

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


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

Что дальше

В следующей статье продолжим работу над WinForms-объектами.

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

К содержанию

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

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


Прикрепленные файлы |
MQL5.zip (4413.58 KB)
Несколько индикаторов на графике (Часть 04): Начинаем работу с советником Несколько индикаторов на графике (Часть 04): Начинаем работу с советником
В предыдущих статьях я рассказывал, как создать индикатор с несколькими подокнами — такая возможность становится интересной при использовании пользовательских индикаторов. В этот раз мы рассмотрим, как добавить несколько окон в советник.
DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления
В статье разберём создание подчинённых элементов управления, привязанных к базовому элементу, создаваемых непосредственно при помощи функционала базового элемента управления. Помимо поставленной выше задачи, немного поработаем над объектом-тенью графического элемента, так как при её использовании для любого из объектов, позволяющих иметь тень, до сих пор есть неисправленные ошибки логики
Как разработать торговую систему на основе индикатора Envelopes Как разработать торговую систему на основе индикатора Envelopes
В этой статье я поделюсь с вами еще одним методом торговли по полосам. На этот раз мы будем работать с индикатором Envelopes (Конверты, Огибающие линии). Научимся создавать стратегии на основе этого индикатора.
Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений
Сегодня мы впервые обновляем функциональность системы индикаторов. В предыдущей статье "Несколько индикаторов на одном графике" мы рассмотрели основы кода, позволяющего использовать более одного индикатора в подокне, но то, что было представлено, было лишь начальной основой для гораздо более крупной системы.