Графика в библиотеке DoEasy (Часть 100): Устраняем недочёты при работе с расширенными стандартными графическими объектами
Содержание
Концепция
На протяжении последних статей мы разрабатываем функционал для работы с составными графическими объектами, создаваемыми на основе расширенных стандартных графических объектов. Постепенно, статья за статьёй, мы продвигаемся по процессу создания этого функционала. И нам даже приходилось несколько раз переходить от одной темы к другой: сначала создавали объекты на основе класса 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-объекты выше вновь построенных графических объектов:
- Добавление стандартного графического объекта на график вручную,
- Программное добавление графического объекта (стандартный или расширенный составной).
Эти ситуации обрабатываются в библиотеке независимо друг от друга.
Поэтому нам нужно в разных местах добавить вызов метода, поднимающего 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.
Но это не означает, что расширенные графические объекты и составные объекты на их основе далее не будут развиваться — просто для их дальнейшей разработки нам потребуются полноценные элементы управления. Соответственно, по мере развития следующего раздела библиотеки, мы постепенно будем и далее разрабатывать и дорабатывать расширенные графические объекты.
*Статьи этой серии:
Графика в библиотеке DoEasy (Часть 93): Готовим функционал для создания составных графических объектов
Графика в библиотеке DoEasy (Часть 94): Составные графические объекты, перемещение и удаление
Графика в библиотеке DoEasy (Часть 95): Элементы управления составными графическими объектами
Графика в библиотеке DoEasy (Часть 96): Работа с событиями мышки и графика в объектах-формах
Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм
Графика в библиотеке DoEasy (Часть 98): Перемещаем опорные точки расширенных стандартных графических объектов
Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный стандартный графический объект целиком одной контрольной точкой управления
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования