English 中文 Español Deutsch 日本語 Português
preview
Графика в библиотеке DoEasy (Часть 100): Устраняем недочёты при работе с расширенными стандартными графическими объектами

Графика в библиотеке DoEasy (Часть 100): Устраняем недочёты при работе с расширенными стандартными графическими объектами

MetaTrader 5Примеры | 2 апреля 2022, 18:44
1 324 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

Поэтому сегодня мы немного "подчистим хвосты" — устраним явные недоработки при одновременной работе с расширенными (и стандартными) графическими объектами и объектами-формами на канвасе и исправим ошибки, замеченные при тестировании в прошлой статье. И на этом завершим этот раздел описания библиотеки. Со следующей статьи откроем новый раздел, в котором начнём разработку графических объектов на канвасе, имитирующих формы Windows Forms в MS Visual Studio — эти объекты нам необходимы будут для продолжения разработки расширенных стандартных графических объектов и составных объектов на их основе.


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

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

Из справки:

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

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

В файле \MQL5\Include\DoEasy\Defines.mqh в перечислении целочисленных свойств графического элемента на канвасе добавим новое свойство и увеличим количество целочисленных свойств с 23 до 24:

//+------------------------------------------------------------------+
//| Целочисленные свойства графического элемента на канвасе          |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_INTEGER
  {
   CANV_ELEMENT_PROP_ID = 0,                          // Идентификатор элемента
   CANV_ELEMENT_PROP_TYPE,                            // Тип графического элемента
   CANV_ELEMENT_PROP_BELONG,                          // Принадлежность графического элемента
   CANV_ELEMENT_PROP_NUM,                             // Номер элемента в списке
   CANV_ELEMENT_PROP_CHART_ID,                        // Идентификатор графика
   CANV_ELEMENT_PROP_WND_NUM,                         // Номер подокна графика
   CANV_ELEMENT_PROP_COORD_X,                         // X-координата формы на графике
   CANV_ELEMENT_PROP_COORD_Y,                         // Y-координата формы на графике
   CANV_ELEMENT_PROP_WIDTH,                           // Ширина элемента
   CANV_ELEMENT_PROP_HEIGHT,                          // Высота элемента
   CANV_ELEMENT_PROP_RIGHT,                           // Правая граница элемента
   CANV_ELEMENT_PROP_BOTTOM,                          // Нижняя граница элемента
   CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,                  // Отступ активной зоны от левого края элемента
   CANV_ELEMENT_PROP_ACT_SHIFT_TOP,                   // Отступ активной зоны от верхнего края элемента
   CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,                 // Отступ активной зоны от правого края элемента
   CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,                // Отступ активной зоны от нижнего края элемента
   CANV_ELEMENT_PROP_MOVABLE,                         // Флаг перемещаемости элемента
   CANV_ELEMENT_PROP_ACTIVE,                          // Флаг активности элемента
   CANV_ELEMENT_PROP_INTERACTION,                     // Флаг взаимодействия элемента со внешней средой
   CANV_ELEMENT_PROP_COORD_ACT_X,                     // X-координата активной зоны элемента
   CANV_ELEMENT_PROP_COORD_ACT_Y,                     // Y-координата активной зоны элемента
   CANV_ELEMENT_PROP_ACT_RIGHT,                       // Правая граница активной зоны элемента
   CANV_ELEMENT_PROP_ACT_BOTTOM,                      // Нижняя граница активной зоны элемента
   CANV_ELEMENT_PROP_ZORDER,                          // Приоритет графического объекта на получение события нажатия мышки на графике
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (24)          // Общее количество целочисленных свойств
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Количество неиспользуемых в сортировке целочисленных свойств
//+------------------------------------------------------------------+


И добавим это новое свойство в список возможных критериев сортировки графических элементов на канвасе:

//+------------------------------------------------------------------+
//| Возможные критерии сортировки графических элементов на канвасе   |
//+------------------------------------------------------------------+
#define FIRST_CANV_ELEMENT_DBL_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP)
#define FIRST_CANV_ELEMENT_STR_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_CANV_ELEMENT_MODE
  {
//--- Сортировка по целочисленным свойствам
   SORT_BY_CANV_ELEMENT_ID = 0,                       // Сортировать по идентификатору элемента
   SORT_BY_CANV_ELEMENT_TYPE,                         // Сортировать по типу графического элемента
   SORT_BY_CANV_ELEMENT_BELONG,                       // Сортировать по принадлежности графического элемента
   SORT_BY_CANV_ELEMENT_NUM,                          // Сортировать по номеру формы в списке
   SORT_BY_CANV_ELEMENT_CHART_ID,                     // Сортировать по идентификатору графика
   SORT_BY_CANV_ELEMENT_WND_NUM,                      // Сортировать по номеру окна графика
   SORT_BY_CANV_ELEMENT_COORD_X,                      // Сортировать по X-координате элемента на графике
   SORT_BY_CANV_ELEMENT_COORD_Y,                      // Сортировать по Y-координате элемента на графике
   SORT_BY_CANV_ELEMENT_WIDTH,                        // Сортировать по ширине элемента
   SORT_BY_CANV_ELEMENT_HEIGHT,                       // Сортировать по высоте элемента
   SORT_BY_CANV_ELEMENT_RIGHT,                        // Сортировать по правой границе элемента
   SORT_BY_CANV_ELEMENT_BOTTOM,                       // Сортировать по нижней границе элемента
   SORT_BY_CANV_ELEMENT_ACT_SHIFT_LEFT,               // Сортировать по отступу активной зоны от левого края элемента
   SORT_BY_CANV_ELEMENT_ACT_SHIFT_TOP,                // Сортировать по отступу активной зоны от верхнего края элемента
   SORT_BY_CANV_ELEMENT_ACT_SHIFT_RIGHT,              // Сортировать по отступу активной зоны от правого края элемента
   SORT_BY_CANV_ELEMENT_ACT_SHIFT_BOTTOM,             // Сортировать по отступу активной зоны от нижнего края элемента
   SORT_BY_CANV_ELEMENT_MOVABLE,                      // Сортировать по флагу перемещаемости элемента
   SORT_BY_CANV_ELEMENT_ACTIVE,                       // Сортировать по флагу активности элемента
   SORT_BY_CANV_ELEMENT_INTERACTION,                  // Сортировать по флагу взаимодействия элемента со внешней средой
   SORT_BY_CANV_ELEMENT_COORD_ACT_X,                  // Сортировать по X-координате активной зоны элемента
   SORT_BY_CANV_ELEMENT_COORD_ACT_Y,                  // Сортировать по Y-координате активной зоны элемента
   SORT_BY_CANV_ELEMENT_ACT_RIGHT,                    // Сортировать по правой границе активной зоны элемента
   SORT_BY_CANV_ELEMENT_ACT_BOTTOM,                   // Сортировать по нижней границе активной зоны элемента
   SORT_BY_CANV_ELEMENT_ZORDER,                       // Сортировать по приоритету графического объекта на получение события нажатия мышки на графике
//--- Сортировка по вещественным свойствам

//--- Сортировка по строковым свойствам
   SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,// Сортировать по имени объекта-элемента
   SORT_BY_CANV_ELEMENT_NAME_RES,                     // Сортировать по имени графического ресурса
  };
//+------------------------------------------------------------------+

Теперь мы сможем выбирать и сортировать графические элементы на канвасе по этому свойству.


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

//--- Устанавливает флаг "Запрет на показ имени графического объекта в списке объектов терминала"
   bool              SetFlagHidden(const bool flag,const bool only_prop)
                       {
                        ::ResetLastError();
                        if((!only_prop && ::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_SELECTABLE,flag)) || only_prop)
                          {
                           this.m_hidden=flag;
                           return true;
                          }
                        else
                           CMessage::ToLog(DFUN,::GetLastError(),true);
                        return false;
                       }
//--- Устанавливает Приоритет графического объекта на получение события нажатия мышки на графике 
   virtual bool      SetZorder(const long value,const bool only_prop)
                       {
                        ::ResetLastError();
                        if((!only_prop && ::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_ZORDER,value)) || only_prop)
                          {
                           this.m_zorder=value;
                           return true;
                          }
                        else
                           CMessage::ToLog(DFUN,::GetLastError(),true);
                        return false;
                       }
//--- Устанавливает видимость/невидимость объекта на всех таймфреймах
   bool              SetVisible(const bool flag,const bool only_prop)   
                       {
                        long value=(flag ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS);
                        ::ResetLastError();
                        if((!only_prop && ::ObjectSetInteger(this.m_chart_id,this.m_name,OBJPROP_TIMEFRAMES,value)) || only_prop)
                          {
                           this.m_visible=flag;
                           return true;
                          }
                        else
                           CMessage::ToLog(DFUN,::GetLastError(),true);
                        return false;
                       }
//--- Устанавливает флаги видимости на таймфреймах, записанных как флаги

...

//--- Возврат значений переменных класса
   ENUM_GRAPH_ELEMENT_TYPE TypeGraphElement(void)        const { return this.m_type_element;       }
   ENUM_GRAPH_OBJ_BELONG   Belong(void)                  const { return this.m_belong;             }
   ENUM_GRAPH_OBJ_SPECIES  Species(void)                 const { return this.m_species;            }
   ENUM_OBJECT       TypeGraphObject(void)               const { return this.m_type_graph_obj;     }
   datetime          TimeCreate(void)                    const { return this.m_create_time;        }
   string            Name(void)                          const { return this.m_name;               }
   long              ChartID(void)                       const { return this.m_chart_id;           }
   long              ObjectID(void)                      const { return this.m_object_id;          }
   virtual long      Zorder(void)                        const { return this.m_zorder;             }
   int               SubWindow(void)                     const { return this.m_subwindow;          }
   int               ShiftY(void)                        const { return this.m_shift_y;            }
   int               VisibleOnTimeframes(void)           const { return this.m_timeframes_visible; }
   int               Digits(void)                        const { return this.m_digits;             }
   int               Group(void)                         const { return this.m_group;              }
   bool              IsBack(void)                        const { return this.m_back;               }
   bool              IsSelected(void)                    const { return this.m_selected;           }
   bool              IsSelectable(void)                  const { return this.m_selectable;         }
   bool              IsHidden(void)                      const { return this.m_hidden;             }
   bool              IsVisible(void)                     const { return this.m_visible;            }

//--- Возвращает тип графического объекта (ENUM_OBJECT), рассчитанного из значения типа объекта (ENUM_OBJECT_DE_TYPE), переданного в метод

Эти же методы у нас есть и в объектах-наследниках класса базового объекта всех графических объектов библиотеки.
В них тоже нужно сделать эти методы виртуальными.

В файле класса абстрактного стандартного графического объекта \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh:

//--- Объект на заднем плане
   bool              Back(void)                    const { return (bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,0);                          }
   bool              SetFlagBack(const bool flag,const bool only_prop)
                       {
                        if(!CGBaseObj::SetFlagBack(flag,only_prop))
                           return false;
                        this.SetProperty(GRAPH_OBJ_PROP_BACK,0,flag);
                        return true;
                       }
//--- Приоритет графического объекта на получение события нажатия мышки на графике
   virtual long      Zorder(void)                  const { return this.GetProperty(GRAPH_OBJ_PROP_ZORDER,0);                              }
   virtual bool      SetZorder(const long value,const bool only_prop)
                       {
                        if(!CGBaseObj::SetZorder(value,only_prop))
                           return false;
                        this.SetProperty(GRAPH_OBJ_PROP_ZORDER,0,value);
                        return true;
                       }
//--- Запрет на показ имени графического объекта в списке объектов терминала
   bool              Hidden(void)                  const { return (bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,0);                        }
   bool              SetFlagHidden(const bool flag,const bool only_prop)
                       {
                        if(!CGBaseObj::SetFlagHidden(flag,only_prop))
                           return false;
                        this.SetProperty(GRAPH_OBJ_PROP_HIDDEN,0,flag);
                        return true;
                       }
//--- Выделенность объекта


В файле класса объекта графического элемента \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh добавим точно такие же виртуальные методы:

//--- (1) Показывает, (2) скрывает элемент
   virtual void      Show(void)                                { CGBaseObj::SetVisible(true,false);                                    }
   virtual void      Hide(void)                                { CGBaseObj::SetVisible(false,false);                                   }
   
//--- Приоритет графического объекта на получение события нажатия мышки на графике
   virtual long      Zorder(void)                        const { return this.GetProperty(CANV_ELEMENT_PROP_ZORDER);                    }
   virtual bool      SetZorder(const long value,const bool only_prop)
                       {
                        if(!CGBaseObj::SetZorder(value,only_prop))
                           return false;
                        this.SetProperty(CANV_ELEMENT_PROP_ZORDER,value);
                        return true;
                       }
//+------------------------------------------------------------------+
//| Методы получения растровых данных                                |
//+------------------------------------------------------------------+

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

//--- Создаёт (1) структуру объекта, (2) объект из структуры
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
   
private:
   struct SData
     {
      //--- Целочисленные свойства объекта
      int            id;                                       // Идентификатор элемента
      int            type;                                     // Тип графического элемента
      int            number;                                   // Номер элемента в списке
      long           chart_id;                                 // Идентификатор графика
      int            subwindow;                                // Номер подокна графика
      int            coord_x;                                  // X-координата формы на графике
      int            coord_y;                                  // Y-координата формы на графике
      int            width;                                    // Ширина элемента
      int            height;                                   // Высота элемента
      int            edge_right;                               // Правая граница элемента
      int            edge_bottom;                              // Нижняя граница элемента
      int            act_shift_left;                           // Отступ активной зоны от левого края элемента
      int            act_shift_top;                            // Отступ активной зоны от верхнего края элемента
      int            act_shift_right;                          // Отступ активной зоны от правого края элемента
      int            act_shift_bottom;                         // Отступ активной зоны от нижнего края элемента
      uchar          opacity;                                  // Непрозрачность элемента
      color          color_bg;                                 // Цвет фона элемента
      bool           movable;                                  // Флаг перемещаемости элемента
      bool           active;                                   // Флаг активности элемента
      bool           interaction;                              // Флаг взаимодействия элемента с внешней средой
      int            coord_act_x;                              // X-координата активной зоны элемента
      int            coord_act_y;                              // Y-координата активной зоны элемента
      int            coord_act_right;                          // Правая граница активной зоны элемента
      int            coord_act_bottom;                         // Нижняя граница активной зоны элемента
      long           zorder;                                   // Приоритет графического объекта на получение события нажатия мышки на графике
      //--- Вещественные свойства объекта

      //--- Строковые свойства объекта
      uchar          name_obj[64];                             // Имя объекта-графического элемента
      uchar          name_res[64];                             // Имя графического ресурса
     };
   SData             m_struct_obj;                             // Структура объекта


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

//+------------------------------------------------------------------+
//| Создаёт структуру объекта                                        |
//+------------------------------------------------------------------+
bool CGCnvElement::ObjectToStruct(void)
  {
//--- Сохранение целочисленных свойств
   this.m_struct_obj.id=(int)this.GetProperty(CANV_ELEMENT_PROP_ID);                            // Идентификатор элемента
   this.m_struct_obj.type=(int)this.GetProperty(CANV_ELEMENT_PROP_TYPE);                        // Тип графического элемента
   this.m_struct_obj.number=(int)this.GetProperty(CANV_ELEMENT_PROP_NUM);                       // Номер элемента в списке
   this.m_struct_obj.chart_id=this.GetProperty(CANV_ELEMENT_PROP_CHART_ID);                     // Идентификатор графика
   this.m_struct_obj.subwindow=(int)this.GetProperty(CANV_ELEMENT_PROP_WND_NUM);                // Номер подокна графика
   this.m_struct_obj.coord_x=(int)this.GetProperty(CANV_ELEMENT_PROP_COORD_X);                  // X-координата формы на графике
   this.m_struct_obj.coord_y=(int)this.GetProperty(CANV_ELEMENT_PROP_COORD_Y);                  // Y-координата формы на графике
   this.m_struct_obj.width=(int)this.GetProperty(CANV_ELEMENT_PROP_WIDTH);                      // Ширина элемента
   this.m_struct_obj.height=(int)this.GetProperty(CANV_ELEMENT_PROP_HEIGHT);                    // Высота элемента
   this.m_struct_obj.edge_right=(int)this.GetProperty(CANV_ELEMENT_PROP_RIGHT);                 // Правая граница элемента
   this.m_struct_obj.edge_bottom=(int)this.GetProperty(CANV_ELEMENT_PROP_BOTTOM);               // Нижняя граница элемента
   this.m_struct_obj.act_shift_left=(int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT);    // Отступ активной зоны от левого края элемента
   this.m_struct_obj.act_shift_top=(int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP);      // Отступ активной зоны от верхнего края элемента
   this.m_struct_obj.act_shift_right=(int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT);  // Отступ активной зоны от правого края элемента
   this.m_struct_obj.act_shift_bottom=(int)this.GetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM);// Отступ активной зоны от нижнего края элемента
   this.m_struct_obj.movable=(bool)this.GetProperty(CANV_ELEMENT_PROP_MOVABLE);                 // Флаг перемещаемости элемента
   this.m_struct_obj.active=(bool)this.GetProperty(CANV_ELEMENT_PROP_ACTIVE);                   // Флаг активности элемента
   this.m_struct_obj.interaction=(bool)this.GetProperty(CANV_ELEMENT_PROP_INTERACTION);         // Флаг взаимодействия элемента с внешней средой
   this.m_struct_obj.coord_act_x=(int)this.GetProperty(CANV_ELEMENT_PROP_COORD_ACT_X);          // X-координата активной зоны элемента
   this.m_struct_obj.coord_act_y=(int)this.GetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y);          // Y-координата активной зоны элемента
   this.m_struct_obj.coord_act_right=(int)this.GetProperty(CANV_ELEMENT_PROP_ACT_RIGHT);        // Правая граница активной зоны элемента
   this.m_struct_obj.coord_act_bottom=(int)this.GetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM);      // Нижняя граница активной зоны элемента
   this.m_struct_obj.color_bg=this.m_color_bg;                                                  // Цвет фона элемента
   this.m_struct_obj.opacity=this.m_opacity;                                                    // Непрозрачность элемента
   this.m_struct_obj.zorder=this.m_zorder;                                                      // Приоритет графического объекта на получение события нажатия мышки на графике
//--- Сохранение вещественных свойств

//--- Сохранение строковых свойств
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ),this.m_struct_obj.name_obj);// Имя объекта-графического элемента
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_RES),this.m_struct_obj.name_res);// Имя графического ресурса
   //--- Сохранение структуры в uchar-массив
   ::ResetLastError();
   if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

А в методе восстановления объекта из структуры добавим чтение этого свойства в объект:

//+------------------------------------------------------------------+
//| Создаёт объект из структуры                                      |
//+------------------------------------------------------------------+
void CGCnvElement::StructToObject(void)
  {
//--- Сохранение целочисленных свойств
   this.SetProperty(CANV_ELEMENT_PROP_ID,this.m_struct_obj.id);                                 // Идентификатор элемента
   this.SetProperty(CANV_ELEMENT_PROP_TYPE,this.m_struct_obj.type);                             // Тип графического элемента
   this.SetProperty(CANV_ELEMENT_PROP_NUM,this.m_struct_obj.number);                            // Номер элемента в списке
   this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,this.m_struct_obj.chart_id);                     // Идентификатор графика
   this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,this.m_struct_obj.subwindow);                     // Номер подокна графика
   this.SetProperty(CANV_ELEMENT_PROP_COORD_X,this.m_struct_obj.coord_x);                       // X-координата формы на графике
   this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,this.m_struct_obj.coord_y);                       // Y-координата формы на графике
   this.SetProperty(CANV_ELEMENT_PROP_WIDTH,this.m_struct_obj.width);                           // Ширина элемента
   this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,this.m_struct_obj.height);                         // Высота элемента
   this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.m_struct_obj.edge_right);                      // Правая граница элемента
   this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.m_struct_obj.edge_bottom);                    // Нижняя граница элемента
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,this.m_struct_obj.act_shift_left);         // Отступ активной зоны от левого края элемента
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,this.m_struct_obj.act_shift_top);           // Отступ активной зоны от верхнего края элемента
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,this.m_struct_obj.act_shift_right);       // Отступ активной зоны от правого края элемента
   this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,this.m_struct_obj.act_shift_bottom);     // Отступ активной зоны от нижнего края элемента
   this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,this.m_struct_obj.movable);                       // Флаг перемещаемости элемента
   this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,this.m_struct_obj.active);                         // Флаг активности элемента
   this.SetProperty(CANV_ELEMENT_PROP_INTERACTION,this.m_struct_obj.interaction);               // Флаг взаимодействия элемента с внешней средой
   this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_X,this.m_struct_obj.coord_act_x);               // X-координата активной зоны элемента
   this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y,this.m_struct_obj.coord_act_y);               // Y-координата активной зоны элемента
   this.SetProperty(CANV_ELEMENT_PROP_ACT_RIGHT,this.m_struct_obj.coord_act_right);             // Правая граница активной зоны элемента
   this.SetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM,this.m_struct_obj.coord_act_bottom);           // Нижняя граница активной зоны элемента
   this.m_color_bg=this.m_struct_obj.color_bg;                                                  // Цвет фона элемента
   this.m_opacity=this.m_struct_obj.opacity;                                                    // Непрозрачность элемента
   this.m_zorder=this.m_struct_obj.zorder;                                                      // Приоритет графического объекта на получение события нажатия мышки на графике
//--- Сохранение вещественных свойств

//--- Сохранение строковых свойств
   this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,::CharArrayToString(this.m_struct_obj.name_obj));// Имя объекта-графического элемента
   this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,::CharArrayToString(this.m_struct_obj.name_res));// Имя графического ресурса
  }
//+------------------------------------------------------------------+

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


Вернёмся к файлу класса абстрактного стандартного графического объекта \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.

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

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

//--- Возвращает (1) список зависимых объектов, (2) зависимый графический объект по индексу, (3) количество зависимых объектов
   CArrayObj        *GetListDependentObj(void)        { return &this.m_list;           }
   CGStdGraphObj    *GetDependentObj(const int index) { return this.m_list.At(index);  }
   int               GetNumDependentObj(void)         { return this.m_list.Total();    }
//--- Возвращает имя зависимого графического объекта по индексу
   string            NameDependent(const int index);
//--- Добавляет зависимый графический объект в список
   bool              AddDependentObj(CGStdGraphObj *obj);
//--- Изменяет координаты X и Y текущего и всех зависимых объектов
   bool              ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false);
//--- Устанавливает координаты X и Y в связанные опорные точки указанного подчинённого объекта
   bool              SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj);

//--- Возвращает (1) минимальную, (2) максимальную экранную координату XY указанной опорной точки
   bool              GetMinCoordXY(int &x,int &y);
   bool              GetMaxCoordXY(int &x,int &y);
   
//--- Возвращает объект данных опорных точек
   CLinkedPivotPoint*GetLinkedPivotPoint(void)        { return &this.m_linked_pivots;  }

За пределами тела класса напишем их реализацию.

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

//+------------------------------------------------------------------+
//| Возвращает минимальную экранную координату XY                    |
//| из всех опорных точек                                            |
//+------------------------------------------------------------------+
bool CGStdGraphObj::GetMinCoordXY(int &x,int &y)
  {
//--- Объявляем переменные, хранящие минимальное время и максимальную цену
   datetime time_min=LONG_MAX;
   double   price_max=0;
//--- В зависимости от типа объекта
   switch(this.TypeGraphObject())
     {
      //--- Объекты, строящиеся в экранных координатах
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
         //--- Записываем экранные координаты XY в переменные x и y и возвращаем true
         x=this.XDistance();
         y=this.YDistance();
         return true;
      
      //--- Объекты, строящиеся в координатах время/цена
      default                    :
         //--- В цикле по всем опорным точкам
         for(int i=0;i<this.Pivots();i++)
           {
            //--- Определяем минимальное время
            if(this.Time(i)<time_min)
               time_min=this.Time(i);
            //--- Определяем максимальную цену
            if(this.Price(i)>price_max)
               price_max=this.Price(i);
           }
         //--- Возвращаем результат преобразования координат время/цена в экранные координаты XY
         return(::ChartTimePriceToXY(this.ChartID(),this.SubWindow(),time_min,price_max,x,y) ? true : false);
     }
   return false;
  }
//+------------------------------------------------------------------+

Для объектов, строящихся в экранных координатах, искать максимальную/минимальную координату нет необходимости, так как у таких объектов всего одна точка привязки к графику, и она уже в экранных координатах — просто возвращаем их.
А вот для объектов, строящихся по координатам время/цена, нам необходимо получить эти координаты функцией ChartTimePriceToXY(). Но при этом сначала необходимо найти минимальную координату времени из всех опорных точек, имеющихся у объекта, и максимальную координату цены из тех же опорных точек. Почему для цены мы ищем максимальную координату, если нам необходима минимальная координата на графике? Ответ прост: цена на графике возрастает снизу-вверх, тогда как отсчёт координат в пикселях на графике идёт от верхнего левого угла. Соответственно, чем выше цена, тем меньшее значение будет у координаты в пикселях для этой точки цены.
В цикле по всем опорным точками находим минимальное время и максимальную цену, которые в итоге и передаём в функцию ChartTimePriceToXY() для получения координат в пикселях графика.

Аналогичный метод и для получения максимальной экранной координаты XY из всех опорных точек:

//+------------------------------------------------------------------+
//| Возвращает максимальную экранную координату XY                   |
//| из всех опорных точек                                            |
//+------------------------------------------------------------------+
bool CGStdGraphObj::GetMaxCoordXY(int &x,int &y)
  {
//--- Объявляем переменные, хранящие максимальное время и минимальную цену
   datetime time_max=0;
   double   price_min=DBL_MAX;
//--- В зависимости от типа объекта
   switch(this.TypeGraphObject())
     {
      //--- Объекты, строящиеся в экранных координатах
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
         //--- Записываем экранные координаты XY в переменные x и y и возвращаем true
         x=this.XDistance();
         y=this.YDistance();
         return true;
      
      //--- Объекты, строящиеся в координатах время/цена
      default                    :
         //--- В цикле по всем опорным точкам
         for(int i=0;i<this.Pivots();i++)
           {
            //--- Определяем максимальное время
            if(this.Time(i)>time_max)
               time_max=this.Time(i);
            //--- Определяем минимальную цену
            if(this.Price(i)<price_min)
               price_min=this.Price(i);
           }
         //--- Возвращаем результат преобразования координат время/цена в экранные координаты XY
         return(::ChartTimePriceToXY(this.ChartID(),this.SubWindow(),time_max,price_min,x,y) ? true : false);
     }
   return false;
  }
//+------------------------------------------------------------------+

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

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


В классе инструментария расширенного стандартного графического объекта \MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh немного поправим метод, возвращающий координаты X и Y указанной опорной точки графического объекта в экранных координатах. Дело в том, что нам необходимо распределить графические объекты по кейсам оператора-переключателя switch таким образом, чтобы в каждом кейсе находились графические объекты по количеству опорных точек для тех объектов, которые строятся по координатам время/цена. Распределим их в нужном порядке:

//+------------------------------------------------------------------+
//| Возвращает координаты X и Y указанной опорной точки              |
//| графического объекта в экранных координатах                      |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y)
  {
//--- Объявим объекты-формы, от которых будем получать их экранные координаты
   CFormControl *form0=NULL, *form1=NULL;
//--- Установим X и Y в ноль - при неудаче будут возвращены эти значения
   x=0;
   y=0;
//--- В зависимости от типа графического объекта
   switch(this.m_base_type)
     {
      //--- Объекты, рисуемые по экранным координатам
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
        //--- Записываем экранные координаты объекта и возвращаем true
        x=this.m_base_x;
        y=this.m_base_y;
        return true;
      
      //--- Одна опорная точка (только цена)
      case OBJ_HLINE             : break;
      
      //--- Одна опорная точка (только время)
      case OBJ_VLINE             :
      case OBJ_EVENT             : break;
      
      //--- Одна опорная точка (время/цена)
      case OBJ_TEXT              :
      case OBJ_BITMAP            : break;
      
      //--- Две опорные точки и центральная
      //--- Линии
      case OBJ_TREND             :
      case OBJ_TRENDBYANGLE      :
      case OBJ_CYCLES            :
      case OBJ_ARROWED_LINE      :
      //--- Каналы
      case OBJ_CHANNEL           :
      case OBJ_STDDEVCHANNEL     :
      case OBJ_REGRESSION        :
      //--- Ганн
      case OBJ_GANNLINE          :
      case OBJ_GANNGRID          :
      //--- Фибоначчи
      case OBJ_FIBO              :
      case OBJ_FIBOTIMES         :
      case OBJ_FIBOFAN           :
      case OBJ_FIBOARC           :
      case OBJ_FIBOCHANNEL       :
      case OBJ_EXPANSION         :
        //--- Расчёт координат для форм на опорных точках линии
        if(index<this.m_base_pivots)
           return(::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y) ? true : false);
        //--- Расчёт координат для центральной формы, находящейся между опорных точек линии
        else
          {
           form0=this.GetControlPointForm(0);
           form1=this.GetControlPointForm(1);
           if(form0==NULL || form1==NULL)
              return false;
           x=(form0.CoordX()+this.m_shift+form1.CoordX()+this.m_shift)/2;
           y=(form0.CoordY()+this.m_shift+form1.CoordY()+this.m_shift)/2;
           return true;
          }

      //--- Каналы
      case OBJ_PITCHFORK         : break;
      
      //--- Ганн
      case OBJ_GANNFAN           : break;
      
      //--- Эллиотт
      case OBJ_ELLIOTWAVE5       : break;
      case OBJ_ELLIOTWAVE3       : break;
      
      //--- Фигуры
      case OBJ_RECTANGLE         : break;
      case OBJ_TRIANGLE          : break;
      case OBJ_ELLIPSE           : break;
      
      //--- Стрелки
      case OBJ_ARROW_THUMB_UP    : break;
      case OBJ_ARROW_THUMB_DOWN  : break;
      case OBJ_ARROW_UP          : break;
      case OBJ_ARROW_DOWN        : break;
      case OBJ_ARROW_STOP        : break;
      case OBJ_ARROW_CHECK       : break;
      case OBJ_ARROW_LEFT_PRICE  : break;
      case OBJ_ARROW_RIGHT_PRICE : break;
      case OBJ_ARROW_BUY         : break;
      case OBJ_ARROW_SELL        : break;
      case OBJ_ARROW             : break;
      
      //---
      default                    : break;
     }
   return false;
  }
//+------------------------------------------------------------------+

Объекты, рисуемые изначально в экранных координатах, просто возвращают экранные координаты своего базового объекта (к которому они прикреплены).

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

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


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

Для того, чтобы поднять графический объект на передний план, нужно последовательно скрыть его и заново отобразить. Это делается при помощи сброса и последующей установки флага видимости графического объекта. Для скрытия объекта нужно установить функцией ObjectSetInteger() для свойства OBJPROP_TIMEFRAMES флаг OBJ_NO_PERIODS. Для отображения — флаг OBJ_ALL_PERIODS. И если мы будем это делать для всех GUI-объектов в той последовательности, в которой они располагаются в списке-коллекции графических элементов, то мы потеряем последовательность их расположения на графике. То есть объекты расположатся так, что самый первый в списке будет самым нижним на графике, а самый последний — выше всех. Но эти объекты могли быть расположены в совсем иной последовательности, которая будет изменена при перерисовке. И вот тут мы обращаемся к новому свойству, добавленному нами сегодня, — ZOrder. Нам нужно список графических элементов отсортировать по порядку возрастания свойства ZOrder, и тогда перерисовка объектов будет производиться в правильной последовательности.

Откроем файл класса-коллекции графических элементов \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh и внесём в него все необходимые доработки.

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

//--- Обработчик событий
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

private:
//--- Перемещает все объекты на канвасе на передний план
   void              BringToTopAllCanvElm(void);
//--- (1) Возвращает максимальный ZOrder всех CCanvas-елементов,
//--- (2) Устанавливает ZOrder в указанный элемент, а в остальных элементах корректирует
   long              GetZOrderMax(void);
   bool              SetZOrderMAX(CGCnvElement *obj);
//--- Обрабатывает удаление расширенных графических объектов
   void              DeleteExtendedObj(CGStdGraphObj *obj);

За пределами тела класса напишем их реализацию.

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

//+------------------------------------------------------------------+
//| Перемещает все объекты на канвасе на передний план               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::BringToTopAllCanvElm(void)
  {
//--- Создаём новый список, сбрасываем флаг управления памятью и сортируем по ZOrder
   CArrayObj *list=new CArrayObj();
   list.FreeMode(false);
   list.Sort(SORT_BY_CANV_ELEMENT_ZORDER);
//--- Объявляем объект-графический элемент
   CGCnvElement *obj=NULL;
//--- В цикле по общему количеству всех графических элементов
   for(int i=0;i<this.m_list_all_canv_elm_obj.Total();i++)
     {
      //--- получаем очередной объект
      obj=this.m_list_all_canv_elm_obj.At(i);
      if(obj==NULL)
         continue;
      //--- и вставляем его в список в порядке сортировки по ZOrder
      list.InsertSort(obj);
     }
//--- В цикле по созданному списку, отсортированному по ZOrder
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект
      obj=list.At(i);
      if(obj==NULL)
         continue;
      //--- и перемещаем его на передний план
      obj.BringToTop();
     }
//--- Удаляем созданный по new список
   delete list;
  }
//+------------------------------------------------------------------+

Итак, что мы тут видим. Мы объявляем новый объект-список CArrayObj и сбрасываем его флаг управления памятью. Затем устанавливаем списку флаг сортировки по свойству ZOrder. Для чего это всё? Для того, чтобы мы могли сортировать объекты CObject по какому-либо свойству, необходимо значение этого свойства установить как флаг сортировки. Тогда виртуальный метод Compare(), реализованный во всех объектах-наследниках класса CObject, будет возвращать требуемое для работы метода Sort() значение сравнения одинаковых свойств (свойство, установленное как режим сортировки) двух объектов.

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

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

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

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

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

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

Далее мы проходимся в цикле по списку-коллекции графических элементов и вставляем каждый очередной объект в новый список в порядке сортировки.

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

Метод, возвращающий максимальный ZOrder всех CCanvas-элементов:

//+------------------------------------------------------------------+
//| Возвращает максимальный ZOrder всех CCanvas-елементов            |
//+------------------------------------------------------------------+
long CGraphElementsCollection::GetZOrderMax(void)
  {
   this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ZORDER);
   int index=CSelect::FindGraphCanvElementMax(this.GetListCanvElm(),CANV_ELEMENT_PROP_ZORDER);
   CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(index);
   return(obj!=NULL ? obj.Zorder() : WRONG_VALUE);
  }
//+------------------------------------------------------------------+

Здесь: сортируем список по свойству ZOrder и получаем индекс элемента в списке с максимальным значением свойства.
Получаем из списка указатель на элемент по полученному индексу и возвращаем свойство ZOrder этого элемента.
Если элемент не был получен, то метод вернёт -1.

Если у нас есть, например, три GUI-объекта, то для них логично иметь три значения ZOrder. При этом все объекты изначально имеют нулевое значение ZOrder — все они находятся в самом низу. Как только мы захватываем какой-либо объект, его ZOrder должен увеличиться на 1. Но сначала нужно посмотреть, а не имеет ли какой-то другой объект значение ZOrder равное, или  больше 1, ведь последний выбранный объект должен стать выше всех остальных — на 1 больше максимального ZOrder из всех имеющихся. Мы, конечно, можем получить максимальный ZOrder и просто его увеличить на 1, и дальше ни о чём не задумываться. Но это как-то неаккуратно, и мы лучше сделаем так, чтобы из трёх объектов мы могли иметь значения ZOrder только в пределах 0 — 2.

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

Метод, устанавливающий 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,TextByLanguage("Тест: ID ","Test. ID ")+(string)form.ID()+", ZOrder "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
//--- Отсортируем список графических элементов по идентификатору элемента
   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,TextByLanguage("Тест: ID ","Test. ID ")+(string)form.ID()+", ZOrder "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
        }
     }
//--- По окончании цикла возвращаем результат, записанный в res
   return res;
  }
//+------------------------------------------------------------------+

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

У нас есть два случая, при которых нам нужно будет переместить GUI-объекты выше вновь построенных графических объектов:

  1. Добавление стандартного графического объекта на график вручную,
  2. Программное добавление графического объекта (стандартный или расширенный составной).

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

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

//--- Добавляет созданный стандартный графический объект в список
   bool              AddCreatedObjToList(const string source,const long chart_id,const string name,CGStdGraphObj *obj)
                       {

                        if(!this.m_list_all_graph_obj.Add(obj))
                          {
                           CMessage::ToLog(source,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
                           ::ObjectDelete(chart_id,name);
                           delete obj;
                           return false;
                          }
                        //--- Переносим все формы выше созданного объекта на графике и перерисовываем график
                        this.BringToTopAllCanvElm();
                        ::ChartRedraw(chart_id);
                        return true;
                       }

После вызова метода нужно обновить график для немедленного отображения изменений.


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

В блоке кода, где определяется добавление нового графического объекта, впишем вызов метода, переносящего GUI-объекты на передний план:

//+------------------------------------------------------------------+
//| Обновляет список всех графических объектов                       |
//+------------------------------------------------------------------+
void CGraphElementsCollection::Refresh(void)
  {
   this.RefreshForExtraObjects();
//--- Объявим переменные для поиска графиков
   long chart_id=0;
   int i=0;
//--- В цикле по всем открытым графикам терминала (не более 100)
   while(i<CHARTS_MAX)
     {
      //--- Получим идентификатор графика
      chart_id=::ChartNext(chart_id);
      if(chart_id<0)
         break;
      //--- Получаем указатель на объект управления графическими объектами
      //--- и обновляем список графических объектов по идентификатору чарта
      CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id);
      //--- Если указатель получить не удалось - идём к следующему графику
      if(obj_ctrl==NULL)
         continue;
      //--- Если есть событие изменения количества объектов на графике
      if(obj_ctrl.IsEvent())
        {
         //--- Если добавлен графический объект на график
         if(obj_ctrl.Delta()>0)
           {
            //--- Получаем список добавленных графических объектов и перемещаем их в список-коллекцию
            //--- (если объект поместить в коллекцию не удалось - идём к следующему объекту)
            if(!this.AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl))
               continue;
            //--- Переносим все формы выше созданного объекта на графике и перерисовываем график
            this.BringToTopAllCanvElm();
            ::ChartRedraw(obj_ctrl.ChartID());
           }
         //--- Если удалён графический объект
         else if(obj_ctrl.Delta()<0)
           {
            int index=WRONG_VALUE;
            //--- В цикле по количеству удалённых графических объектов
            for(int j=0;j<-obj_ctrl.Delta();j++)
              {
               // Найдём лишний объект в списке
               CGStdGraphObj *obj=this.FindMissingObj(chart_id,index);
               if(obj!=NULL)
                 {
                  //--- Получим параметры удалённого объекта
                  long   lparam=obj.ChartID();
                  string sparam=obj.Name();
                  double dparam=(double)obj.TimeCreate();
                  //--- Если это расширенный графический объект
                  if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
                    {
                     this.DeleteExtendedObj(obj);
                    }
                  //--- Переместим объект класса графического объекта в список удалённых объектов
                  //--- и отправим событие на график управляющей программы
                  if(this.MoveGraphObjToDeletedObjList(index))
                     ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DELETE,lparam,dparam,sparam);
                 }
              }
           }
        }
      //--- Увеличиваем индекс цикла
      i++;
     }
  }
//+------------------------------------------------------------------+


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

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

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

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

                           //--- Если форма - центральная для управления всеми опорными точками графического объекта
                           else
                             {
                              //--- Получаем и записываем в структуру m_data_pivot_point экранные координаты всех опорных точек объекта
                              if(this.GetPivotPointCoordsAll(ext,m_data_pivot_point))
                                {
                                 //--- В цикле по количеству опорных точек объекта
                                 for(int i=0;i<(int)this.m_data_pivot_point.Size();i++)
                                   {
                                    //--- ограничиваем экранные координаты текущей опорной точки так, чтобы они не выходили за пределы графика
                                    //--- По X-координае
                                    if(x+shift-::fabs(this.m_data_pivot_point[i].ShiftX)<0)
                                       x=-shift+::fabs(m_data_pivot_point[i].ShiftX);
                                    if(x+shift+::fabs(this.m_data_pivot_point[i].ShiftX)>chart_width)
                                       x=chart_width-shift-::fabs(this.m_data_pivot_point[i].ShiftX);
                                    //--- По Y-координае
                                    if(y+shift-::fabs(this.m_data_pivot_point[i].ShiftY)<0)
                                       y=-shift+::fabs(this.m_data_pivot_point[i].ShiftY);
                                    if(y+shift+::fabs(this.m_data_pivot_point[i].ShiftY)>chart_height)
                                       y=chart_height-shift-::fabs(this.m_data_pivot_point[i].ShiftY);
                                    //--- устанавливаем в текущую опорную точку объекта рассчитанные координаты
                                    ext.ChangeCoordsExtendedObj(x+shift+this.m_data_pivot_point[i].ShiftX,y+shift+this.m_data_pivot_point[i].ShiftY,i);
                                   }
                                }
                                
                              //--- Выводим на график отладочные комментарии
                              if(m_data_pivot_point.Size()>=2)
                                {
                                 int max_x,min_x,max_y,min_y;
                                 if(ext.GetMinCoordXY(min_x,min_y) && ext.GetMaxCoordXY(max_x,max_y))
                                    Comment
                                      (
                                       "MinX=",min_x,", MaxX=",max_x,"\n",
                                       "MaxY=",min_y,", MaxY=",max_y
                                      );
                                }
                                
                             }


Как только мы нажимаем кнопку мышки на GUI-объекте, нам нужно его переместить на передний план и установить для него максимальный ZOrder.

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

            //--- Обработчик события Курсор в пределах активной области, нажата кнопка мышки (любая)
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move)
              {
               pressed_form=true;   // флаг удержания кнопки мышки на форме
               //--- Если зажата левая кнопка мышки
               if(this.m_mouse.IsPressedButtonLeft())
                 {
                  //--- Установим флаги и параметры формы
                  move=true;                                            // флаг перемещения
                  form.SetInteraction(true);                            // флаг взаимодействия формы со внешней средой
                  form.BringToTop();                                    // форма на передний план - поверх всех остальных
                  form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); // Смещение курсора относительно координаты X
                  form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); // Смещение курсора относительно координаты Y
                  this.ResetAllInteractionExeptOne(form);               // Сбросим флаги взаимодействия для всех форм, кроме текущей
                  
                  //--- Получаем максимальный ZOrder
                  long zmax=this.GetZOrderMax();
                  //--- Если максимальный ZOrder получен, при этом ZOrder формы меньше максимального, или максимальный ZOrder всех форм равен нулю
                  if(zmax>WRONG_VALUE && (form.Zorder()<zmax || zmax==0))
                    {
                     //--- Если форма - не контрольная точка управления расширенным стандартным графическим объектом -
                     //--- ставим ZOrder формы выше всех
                     if(form.Type()!=OBJECT_DE_TYPE_GFORM_CONTROL)
                        this.SetZOrderMAX(form);
                    }
                 }
              }

Здесь мы устанавливаем ZOrder для объектов-форм, которые не являются объектами управления расширенными стандартными графическими объектами, — для них это не требуется.

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

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

//+------------------------------------------------------------------+
//| Возвращает экранные координаты                                   |
//| каждой опорной точки графического объекта                        |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[])
  {
//--- Координаты центральной точки
   int xc=0, yc=0;
//--- Если не удалось увеличить массив структур под количество опорных точек,
//--- сообщаем об этом в журнал и возвращаем false
   if(::ArrayResize(array_pivots,obj.Pivots())!=obj.Pivots())
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      return false;
     }
//--- В цикле по количеству опорных точек графического объекта
   for(int i=0;i<obj.Pivots();i++)
     {
      //--- Преобразуем координаты объекта в экранные и, если это не получилось - сообщаем об этом в журнал и возвращаем false
      if(!::ChartTimePriceToXY(obj.ChartID(),obj.SubWindow(),obj.Time(i),obj.Price(i),array_pivots[i].X,array_pivots[i].Y))
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY);
         return false;
        }
     }
//--- В зависимости от типа графического объекта
   switch(obj.TypeGraphObject())
     {
      //--- Одна опорная точка в экранных координатах
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             : break;
      
      //--- Одна опорная точка (только цена)
      case OBJ_HLINE             : break;
      
      //--- Одна опорная точка (только время)
      case OBJ_VLINE             :
      case OBJ_EVENT             : break;
      
      //--- Две опорные точки и центральная
      //--- Линии
      case OBJ_TREND             :
      case OBJ_TRENDBYANGLE      :
      case OBJ_CYCLES            :
      case OBJ_ARROWED_LINE      :
      //--- Каналы
      case OBJ_CHANNEL           :
      case OBJ_STDDEVCHANNEL     :
      case OBJ_REGRESSION        :
      //--- Ганн
      case OBJ_GANNLINE          :
      case OBJ_GANNGRID          :
      //--- Фибоначчи
      case OBJ_FIBO              :
      case OBJ_FIBOTIMES         :
      case OBJ_FIBOFAN           :
      case OBJ_FIBOARC           :
      case OBJ_FIBOCHANNEL       :
      case OBJ_EXPANSION         :
        //--- Рассчитаем координаты центральной точки
        xc=(array_pivots[0].X+array_pivots[1].X)/2;
        yc=(array_pivots[0].Y+array_pivots[1].Y)/2;
        //--- Рассчитываем и записываем в массив-структуру смещения всех опорных точек от центральной
        array_pivots[0].ShiftX=array_pivots[0].X-xc;  // Смещение по X от точки 0 до центральной
        array_pivots[1].ShiftX=array_pivots[1].X-xc;  // Смещение по X от точки 1 до центральной
        array_pivots[0].ShiftY=array_pivots[0].Y-yc;  // Смещение по Y от точки 0 до центральной
        array_pivots[1].ShiftY=array_pivots[1].Y-yc;  // Смещение по Y от точки 1 до центральной
        return true;

      //--- Каналы
      case OBJ_PITCHFORK         : break;
      //--- Ганн
      case OBJ_GANNFAN           : break;
      //--- Эллиотт
      case OBJ_ELLIOTWAVE5       : break;
      case OBJ_ELLIOTWAVE3       : break;
      //--- Фигуры
      case OBJ_RECTANGLE         : break;
      case OBJ_TRIANGLE          : break;
      case OBJ_ELLIPSE           : break;
      //--- Стрелки
      case OBJ_ARROW_THUMB_UP    : break;
      case OBJ_ARROW_THUMB_DOWN  : break;
      case OBJ_ARROW_UP          : break;
      case OBJ_ARROW_DOWN        : break;
      case OBJ_ARROW_STOP        : break;
      case OBJ_ARROW_CHECK       : break;
      case OBJ_ARROW_LEFT_PRICE  : break;
      case OBJ_ARROW_RIGHT_PRICE : break;
      case OBJ_ARROW_BUY         : break;
      case OBJ_ARROW_SELL        : break;
      case OBJ_ARROW             : break;
      
      //--- Графические объекты с координатами время/цена
      case OBJ_TEXT              : break;
      case OBJ_BITMAP            : break;
      //---
      default: break;
     }
   return false;
  }
//+------------------------------------------------------------------+

Теперь все смещения и ограничения должны рассчитываться верно, что и посмотрим при тестировании.


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

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

Так как при тестировании мы хотим сразу же на каждой форме видеть значение её свойства ZOrder, то в обработчике OnInit() впишем установку нулевого значения этого свойства для формы (объект в самом низу) и добавим вывод идентификатора объекта-формы и значение его свойства ZOrder:

//+------------------------------------------------------------------+
//| 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); // Краткие описания
//--- Создадим объекты-формы
   CForm *form=NULL;
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      //--- При создании объекта передаём в него все требуемые параметры
      form=new CForm("Form_0"+string(i+1),30,(form==NULL ? 100 : form.BottomEdge()+20),100,30);
      if(form==NULL)
         continue;
      //--- Установим форме флаги активности, и перемещаемости
      form.SetActive(true);
      form.SetMovable(true);
      //--- Установим форме её идентификатор и номер в списке объектов
      form.SetID(i);
      form.SetNumber(0);   // (0 - означает главный объект-форма) К главному объекту могут прикрепляться второстепенные, которыми он будет управлять
      //--- Установим непрозрачность 200
      form.SetOpacity(245);
      //--- Цвет фона формы зададим как первый цвет из массива цветов
      form.SetColorBackground(array_clr[0]);
      //--- Цвет очерчивающей рамки формы
      form.SetColorFrame(clrDarkBlue);
      //--- Установим флаг рисования тени
      form.SetShadow(false);
      //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный
      color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
      //--- Если в настройках задано использовать цвет фона графика, то затемним монохромный цвет на 20 единиц
      //--- Иначе - будем использовать для рисования тени заданный в настройках цвет
      color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3);
      //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
      //--- Непрозрачность тени при этом установим равной 200, а радиус размытия равный 4
      form.DrawShadow(3,3,clr,200,4);
      //--- Зальём фон формы вертикальным градиентом
      form.Erase(array_clr,form.Opacity(),true);
      //--- Нарисуем очерчивающий прямоугольник по краям формы
      form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
      form.Done();
      
      //--- Установим ZOrder в ноль и выведем текст с описанием типа градиента и обновим форму
      //--- Параметры текста: координаты текста в центре формы и точка привязки - тоже по центру
      //--- Создаём новый кадр текстовой анимации с идентификатором 0 и выводим текст на форму
      form.SetZorder(0,false);
      form.TextOnBG(0,TextByLanguage("Тест: ID ","Test: ID ")+(string)form.ID()+", ZOrder "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
      //--- Добавим форму в список
      if(!engine.GraphAddCanvElmToCollection(form))
         delete form;
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


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


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

Что дальше

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

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

К содержанию

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

Графика в библиотеке DoEasy (Часть 93): Готовим функционал для создания составных графических объектов
Графика в библиотеке DoEasy (Часть 94): Составные графические объекты, перемещение и удаление
Графика в библиотеке DoEasy (Часть 95): Элементы управления составными графическими объектами
Графика в библиотеке DoEasy (Часть 96): Работа с событиями мышки и графика в объектах-формах
Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм
Графика в библиотеке DoEasy (Часть 98): Перемещаем опорные точки расширенных стандартных графических объектов
Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный стандартный графический объект целиком одной контрольной точкой управления

Прикрепленные файлы |
MQL5.zip (4450.58 KB)
WebSocket для MetaTrader 5 — Использование Windows API WebSocket для MetaTrader 5 — Использование Windows API
В этой статье мы используем WinHttp.dll, чтобы создать клиент WebSocket для MetaTrader 5-программ. В конечном итоге клиент должен быть выполнен в виде класса и протестирован во взаимодействии с WebSocket API от Binary.com.
Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный графический объект одной контрольной точкой Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный графический объект одной контрольной точкой
В прошлой статье мы создали возможность перемещения опорных точек расширенного графического объекта при помощи форм управления. Теперь сделаем перемещение составного графического объекта при помощи одной точки (формы) управления графическим объектом.
Как разработать торговую систему на основе Bollinger Bands Как разработать торговую систему на основе Bollinger Bands
В этой статье мы поговорим о полосах Боллинджера (Bollinger Bands) — одном из самых популярных индикаторов в мире трейдинга. Мы обсудим технический анализ, а также научимся разрабатывать системы алгоритмической торговли на основе индикатора Bollinger Bands.
Как разрабатывать системы на основе скользящих средних Как разрабатывать системы на основе скользящих средних
Есть множество различных способов для фильтрации сигналов, генерируемых какой-либо стратегией. Вероятно, простейший из них — это использование скользящей средней. Об этом мы и поговорим в этой статье.